1use std::{fmt, num::Wrapping, ops, sync::Arc};
4
5use super::text_properties::*;
6use atomic::{Atomic, Ordering};
7use parking_lot::RwLock;
8use zng_app::render::FontSynthesis;
9use zng_app_context::{MappedRwLockWriteGuardOwned, RwLockReadGuardOwned, RwLockWriteGuardOwned};
10use zng_ext_font::{CaretIndex, FontFaceList, FontList, SegmentedText, ShapedLine, ShapedText, TextOverflowInfo};
11use zng_ext_input::{
12 focus::FOCUS_CHANGED_EVENT,
13 keyboard::{KEY_INPUT_EVENT, Key, KeyState},
14 mouse::MOUSE_INPUT_EVENT,
15 touch::{TOUCH_INPUT_EVENT, TOUCH_LONG_PRESS_EVENT},
16};
17use zng_ext_window::WINDOW_Ext as _;
18use zng_view_api::{mouse::ButtonState, touch::TouchPhase};
19use zng_wgt::prelude::*;
20use zng_wgt_data::{DATA, DataNoteHandle};
21use zng_wgt_layer::{
22 AnchorMode, AnchorTransform,
23 popup::{ContextCapture, POPUP, PopupState},
24};
25
26mod rich;
27pub use rich::*;
28
29mod resolve;
30pub use resolve::*;
31
32mod layout;
33pub use layout::*;
34
35mod render;
36pub use render::*;
37
38mod caret;
39pub use caret::*;
40
41#[derive(Clone, Debug)]
43#[non_exhaustive]
44pub struct RichCaretInfo {
45 pub index: Option<WidgetId>,
49 pub selection_index: Option<WidgetId>,
53}
54
55#[derive(Clone)]
57#[non_exhaustive]
58pub struct CaretInfo {
59 pub opacity: Var<Factor>,
66
67 pub index: Option<CaretIndex>,
71
72 pub selection_index: Option<CaretIndex>,
74
75 pub initial_selection: Option<(ops::Range<CaretIndex>, bool)>,
78
79 pub index_version: Wrapping<u8>,
86
87 pub used_retained_x: bool,
91
92 pub skip_next_scroll: bool,
96}
97impl fmt::Debug for CaretInfo {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 f.debug_struct("CaretInfo")
100 .field("opacity", &self.opacity.get_debug(false))
101 .field("index", &self.index)
102 .field("index_version", &self.index_version)
103 .field("used_retained_x", &self.used_retained_x)
104 .finish()
105 }
106}
107impl CaretInfo {
108 pub fn set_index(&mut self, index: CaretIndex) {
110 self.index = Some(index);
111 self.index_version += 1;
112 }
113
114 pub fn set_selection(&mut self, start: CaretIndex, end: CaretIndex) {
118 self.selection_index = Some(start);
119 self.set_index(end);
120 }
121
122 pub fn clear_selection(&mut self) {
124 self.selection_index = None;
125 self.initial_selection = None;
126 self.index_version += 1;
127 }
128
129 pub fn set_char_index(&mut self, index: usize) {
133 if let Some(i) = &mut self.index {
134 i.index = index;
135 } else {
136 self.index = Some(CaretIndex { index, line: 0 });
137 }
138 self.index_version += 1;
139 }
140
141 pub fn set_char_selection(&mut self, start: usize, end: usize) {
147 if let Some(s) = &mut self.selection_index {
148 s.index = start;
149 } else {
150 self.selection_index = Some(CaretIndex { index: start, line: 0 });
151 }
152 self.set_char_index(end);
153 }
154
155 pub fn selection_range(&self) -> Option<ops::Range<CaretIndex>> {
160 let a = self.index?;
161 let b = self.selection_index?;
162
163 use std::cmp::Ordering;
164 match a.index.cmp(&b.index) {
165 Ordering::Less => Some(a..b),
166 Ordering::Equal => None,
167 Ordering::Greater => Some(b..a),
168 }
169 }
170
171 pub fn selection_char_range(&self) -> Option<ops::Range<usize>> {
176 self.selection_range().map(|r| r.start.index..r.end.index)
177 }
178}
179
180#[derive(Clone)]
182#[non_exhaustive]
183pub struct ImePreview {
184 pub txt: Txt,
186
187 pub prev_caret: CaretIndex,
189 pub prev_selection: Option<CaretIndex>,
193}
194
195pub struct TEXT;
200
201impl TEXT {
202 pub fn try_rich(&self) -> Option<RwLockReadGuardOwned<RichText>> {
204 if RICH_TEXT.is_default() {
205 None
206 } else {
207 Some(RICH_TEXT.read_recursive())
208 }
209 }
210
211 pub fn try_resolved(&self) -> Option<RwLockReadGuardOwned<ResolvedText>> {
215 if RESOLVED_TEXT.is_default() {
216 None
217 } else {
218 Some(RESOLVED_TEXT.read_recursive())
219 }
220 }
221
222 pub fn rich(&self) -> RwLockReadGuardOwned<RichText> {
230 RICH_TEXT.read_recursive()
231 }
232
233 pub fn resolved(&self) -> RwLockReadGuardOwned<ResolvedText> {
239 RESOLVED_TEXT.read_recursive()
240 }
241
242 pub fn try_laidout(&self) -> Option<RwLockReadGuardOwned<LaidoutText>> {
246 if LAIDOUT_TEXT.is_default() {
247 None
248 } else {
249 Some(LAIDOUT_TEXT.read_recursive())
250 }
251 }
252
253 pub fn laidout(&self) -> RwLockReadGuardOwned<LaidoutText> {
259 LAIDOUT_TEXT.read_recursive()
260 }
261
262 pub fn resolve_caret(&self) -> MappedRwLockWriteGuardOwned<ResolvedText, CaretInfo> {
270 RwLockWriteGuardOwned::map(self.resolve(), |ctx| &mut ctx.caret)
271 }
272
273 pub fn resolve_rich_caret(&self) -> MappedRwLockWriteGuardOwned<RichText, RichCaretInfo> {
283 RwLockWriteGuardOwned::map(RICH_TEXT.write(), |ctx| &mut ctx.caret)
284 }
285
286 pub fn set_caret_retained_x(&self, x: Px) {
291 self.layout().caret_retained_x = x;
292 }
293
294 pub(crate) fn resolve(&self) -> RwLockWriteGuardOwned<ResolvedText> {
295 RESOLVED_TEXT.write()
296 }
297
298 fn layout(&self) -> RwLockWriteGuardOwned<LaidoutText> {
299 LAIDOUT_TEXT.write()
300 }
301
302 pub(crate) fn take_rich_selection_started_by_alt(&self) -> bool {
303 std::mem::take(&mut RICH_TEXT.write().selection_started_by_alt)
304 }
305
306 pub(crate) fn flag_rich_selection_started_by_alt(&self) {
307 RICH_TEXT.write().selection_started_by_alt = true;
308 }
309}
310
311#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, bytemuck::NoUninit)]
315#[repr(u8)]
316pub enum SelectionBy {
317 Command = 0,
319 Keyboard = 1,
321 Mouse = 2,
323 Touch = 3,
325}
326impl SelectionBy {
327 pub fn matches_interactive_mode(self, mode: InteractiveCaretMode) -> bool {
329 match mode {
330 InteractiveCaretMode::TouchOnly => matches!(self, SelectionBy::Touch),
331 InteractiveCaretMode::Enabled => true,
332 InteractiveCaretMode::Disabled => false,
333 }
334 }
335}
336
337#[non_exhaustive]
341pub struct ResolvedText {
342 pub txt: Var<Txt>,
344 pub ime_preview: Option<ImePreview>,
347
348 pub segmented_text: SegmentedText,
350 pub faces: FontFaceList,
352 pub synthesis: FontSynthesis,
354
355 pub pending_layout: PendingLayout,
360
361 pub pending_edit: bool,
363
364 pub caret: CaretInfo,
366
367 pub selection_by: SelectionBy,
369
370 pub selection_toolbar_is_open: bool,
372}
373impl fmt::Debug for ResolvedText {
374 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
375 f.debug_struct("ResolvedText")
376 .field("segmented_text", &self.segmented_text)
377 .field("faces", &self.faces)
378 .field("synthesis", &self.synthesis)
379 .field("pending_layout", &self.pending_layout)
380 .field("pending_edit", &self.pending_edit)
381 .field("caret", &self.caret)
382 .field("selection_by", &self.selection_by)
383 .field("selection_toolbar_is_open", &self.selection_toolbar_is_open)
384 .finish_non_exhaustive()
385 }
386}
387impl ResolvedText {
388 fn no_context() -> Self {
389 panic!("no `ResolvedText` in context, only available inside `resolve_text`")
390 }
391}
392
393#[derive(Debug, Clone)]
395#[non_exhaustive]
396pub struct RenderInfo {
397 pub transform: PxTransform,
399 pub scale_factor: Factor,
401}
402impl Default for RenderInfo {
403 fn default() -> Self {
405 Self {
406 transform: PxTransform::identity(),
407 scale_factor: 1.fct(),
408 }
409 }
410}
411
412#[derive(Debug)]
416#[non_exhaustive]
417pub struct LaidoutText {
418 pub fonts: FontList,
422
423 pub shaped_text: ShapedText,
425
426 pub overflow: Option<TextOverflowInfo>,
428
429 pub overflow_suffix: Option<ShapedText>,
431
432 pub shaped_text_version: u32,
434
435 pub overlines: Vec<(PxPoint, Px)>,
441
442 pub overline_thickness: Px,
444
445 pub strikethroughs: Vec<(PxPoint, Px)>,
451 pub strikethrough_thickness: Px,
453
454 pub underlines: Vec<(PxPoint, Px)>,
461 pub underline_thickness: Px,
463
464 pub ime_underlines: Vec<(PxPoint, Px)>,
470 pub ime_underline_thickness: Px,
472
473 pub caret_origin: Option<PxPoint>,
475
476 pub caret_selection_origin: Option<PxPoint>,
478
479 pub caret_retained_x: Px,
481
482 pub render_info: RenderInfo,
484
485 pub viewport: PxSize,
487}
488impl LaidoutText {
489 fn no_context() -> Self {
490 panic!("no `LaidoutText` in context, only available inside `layout_text`")
491 }
492}
493
494#[non_exhaustive]
498pub struct RichText {
499 pub root_id: WidgetId,
501
502 pub caret: RichCaretInfo,
504
505 selection_started_by_alt: bool,
506}
507impl RichText {
508 fn no_context() -> Self {
509 panic!("no `RichText` in context, only available inside `rich_text`")
510 }
511}
512
513context_local! {
514 static RICH_TEXT: RwLock<RichText> = RwLock::new(RichText::no_context());
516 static RESOLVED_TEXT: RwLock<ResolvedText> = RwLock::new(ResolvedText::no_context());
518 static LAIDOUT_TEXT: RwLock<LaidoutText> = RwLock::new(LaidoutText::no_context());
520}
521
522bitflags! {
523 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
525 pub struct PendingLayout: u8 {
526 const UNDERLINE = 0b0000_0001;
528 const STRIKETHROUGH = 0b0000_0010;
530 const OVERLINE = 0b0000_0100;
532 const CARET = 0b0000_1000;
534 const OVERFLOW = 0b0001_0000;
536 const RESHAPE_LINES = 0b0111_1111;
538 const RESHAPE = 0b1111_1111;
540 }
541}
542
543pub fn line_placeholder(width: impl IntoVar<Length>) -> UiNode {
549 let child = layout_text(FillUiNode);
550 let child = resolve_text(child, " ");
551 zng_wgt_size_offset::width(child, width)
552}
553
554pub(super) fn get_caret_index(child: impl IntoUiNode, index: impl IntoVar<Option<CaretIndex>>) -> UiNode {
555 let index = index.into_var();
556 match_node(child, move |c, op| {
557 let mut u = false;
558 match op {
559 UiNodeOp::Init => {
560 c.init();
561 index.set(TEXT.resolved().caret.index);
562 }
563 UiNodeOp::Deinit => {
564 index.set(None);
565 }
566 UiNodeOp::Update { updates } => {
567 c.update(updates);
568 u = true;
569 }
570 _ => {}
571 }
572 if u {
573 let t = TEXT.resolved();
574 let idx = t.caret.index;
575 if !t.pending_edit && index.get() != idx {
576 index.set(idx);
577 }
578 }
579 })
580}
581
582pub(super) fn get_caret_status(child: impl IntoUiNode, status: impl IntoVar<CaretStatus>) -> UiNode {
583 let status = status.into_var();
584 match_node(child, move |c, op| {
585 let mut u = false;
586 match op {
587 UiNodeOp::Init => {
588 c.init();
589 let t = TEXT.resolved();
590 status.set(match t.caret.index {
591 None => CaretStatus::none(),
592 Some(i) => CaretStatus::new(i.index, &t.segmented_text),
593 });
594 }
595 UiNodeOp::Deinit => {
596 status.set(CaretStatus::none());
597 }
598 UiNodeOp::Update { updates } => {
599 c.update(updates);
600 u = true;
601 }
602 _ => {}
603 }
604 if u {
605 let t = TEXT.resolved();
606 let idx = t.caret.index;
607 if !t.pending_edit && status.get().index() != idx.map(|ci| ci.index) {
608 status.set(match idx {
609 None => CaretStatus::none(),
610 Some(i) => CaretStatus::new(i.index, &t.segmented_text),
611 });
612 }
613 }
614 })
615}
616
617pub(super) fn get_lines_len(child: impl IntoUiNode, len: impl IntoVar<usize>) -> UiNode {
618 let len = len.into_var();
619 match_node(child, move |c, op| match op {
620 UiNodeOp::Deinit => {
621 len.set(0usize);
622 }
623 UiNodeOp::Layout { wl, final_size } => {
624 *final_size = c.layout(wl);
625 let t = TEXT.laidout();
626 let l = t.shaped_text.lines_len();
627 if l != len.get() {
628 len.set(t.shaped_text.lines_len());
629 }
630 }
631 _ => {}
632 })
633}
634
635pub(super) fn get_lines_wrap_count(child: impl IntoUiNode, lines: impl IntoVar<super::LinesWrapCount>) -> UiNode {
636 let lines = lines.into_var();
637 let mut version = 0;
638 match_node(child, move |c, op| match op {
639 UiNodeOp::Deinit => {
640 lines.set(super::LinesWrapCount::NoWrap(0));
641 }
642 UiNodeOp::Layout { wl, final_size } => {
643 *final_size = c.layout(wl);
644 let t = TEXT.laidout();
645 if t.shaped_text_version != version {
646 version = t.shaped_text_version;
647 if let Some(update) = lines.with(|l| lines_wrap_count(l, &t.shaped_text)) {
648 lines.set(update);
649 }
650 }
651 }
652 _ => {}
653 })
654}
655fn lines_wrap_count(prev: &super::LinesWrapCount, txt: &ShapedText) -> Option<super::LinesWrapCount> {
657 match prev {
658 super::LinesWrapCount::NoWrap(len) => {
659 let mut counter = lines_wrap_counter(txt);
660 let mut l = 0;
661 for c in &mut counter {
662 if c != 1 {
663 let mut wrap = vec![1; l];
665 wrap.push(c);
666 wrap.extend(&mut counter);
667 return Some(super::LinesWrapCount::Wrap(wrap));
668 }
669 l += 1;
670 }
671 if l != *len {
672 Some(super::LinesWrapCount::NoWrap(l))
674 } else {
675 None
676 }
677 }
678 super::LinesWrapCount::Wrap(counts) => {
679 let mut prev_counts = counts.iter();
682 let mut new_counts = lines_wrap_counter(txt);
683 let mut eq_l = 0;
684 let mut eq_wrap = false;
685 for c in &mut new_counts {
686 if prev_counts.next() == Some(&c) {
687 eq_l += 1;
688 eq_wrap |= c != 1;
689 } else if eq_wrap || c != 1 {
690 let mut wrap = counts[..eq_l].to_vec();
692 wrap.push(c);
693 wrap.extend(&mut new_counts);
694 return Some(super::LinesWrapCount::Wrap(wrap));
695 } else {
696 let mut l = eq_l + 1; for c in &mut new_counts {
699 if c != 1 {
700 let mut wrap = vec![1; l];
702 wrap.push(c);
703 wrap.extend(&mut new_counts);
704 return Some(super::LinesWrapCount::Wrap(wrap));
705 }
706 l += 1;
707 }
708 return Some(super::LinesWrapCount::NoWrap(l));
710 }
711 }
712 if prev_counts.next().is_some() {
713 Some(super::LinesWrapCount::Wrap(counts[..eq_l].to_vec()))
714 } else {
715 None
716 }
717 }
718 }
719}
720fn lines_wrap_counter(txt: &ShapedText) -> impl Iterator<Item = u32> + '_ {
721 struct Counter<I> {
722 lines: I,
723 count: u32,
724 }
725 impl<'a, I: Iterator<Item = ShapedLine<'a>>> Iterator for Counter<I> {
726 type Item = u32;
727
728 fn next(&mut self) -> Option<u32> {
729 loop {
730 let line = self.lines.next()?;
731 if line.ended_by_wrap() {
732 self.count += 1;
733 continue;
734 }
735
736 let c = self.count;
737 self.count = 1;
738
739 return Some(c);
740 }
741 }
742 }
743 Counter {
744 lines: txt.lines(),
745 count: 1,
746 }
747}
748
749pub(super) fn parse_text<T>(child: impl IntoUiNode, value: impl IntoVar<T>) -> UiNode
750where
751 T: super::TxtParseValue,
752{
753 let value = value.into_var();
754
755 let error = var(Txt::from_static(""));
756 let mut _error_note = DataNoteHandle::dummy();
757
758 #[derive(Clone, Copy, bytemuck::NoUninit)]
759 #[repr(u8)]
760 enum State {
761 Sync,
762 Requested,
763 Pending,
764 }
765 let state = Arc::new(Atomic::new(State::Sync));
766
767 match_node(child, move |_, op| match op {
768 UiNodeOp::Init => {
769 let ctx = TEXT.resolved();
770
771 ctx.txt.set_from_map(&value, |val| val.to_txt());
773
774 let live = TXT_PARSE_LIVE_VAR.current_context();
782 let is_pending = TXT_PARSE_PENDING_VAR.current_context();
783 let cmd_handle = Arc::new(super::cmd::PARSE_CMD.scoped(WIDGET.id()).subscribe(false));
784
785 let binding = ctx.txt.bind_filter_map_bidi(
786 &value,
787 clmv!(state, error, is_pending, cmd_handle, |txt| {
788 if live.get() || matches!(state.load(Ordering::Relaxed), State::Requested) {
789 if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
792 is_pending.set(false);
794 cmd_handle.enabled().set(false);
795 }
796
797 match T::from_txt(txt) {
799 Ok(val) => {
800 error.set(Txt::from_static(""));
801 Some(val)
802 }
803 Err(e) => {
804 error.set(e);
805 None
806 }
807 }
808 } else {
809 if !matches!(state.swap(State::Pending, Ordering::Relaxed), State::Pending) {
812 is_pending.set(true);
814 cmd_handle.enabled().set(true);
815 }
816
817 None
819 }
820 }),
821 clmv!(state, error, |val| {
822 error.set(Txt::from_static(""));
825
826 if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
827 is_pending.set(false);
828 cmd_handle.enabled().set(false);
829 }
830
831 Some(val.to_txt())
832 }),
833 );
834
835 WIDGET.sub_var(&TXT_PARSE_LIVE_VAR).sub_var(&error).push_var_handles(binding);
838 }
839 UiNodeOp::Deinit => {
840 _error_note = DataNoteHandle::dummy();
841 }
842 UiNodeOp::Update { .. } => {
843 if matches!(state.load(Ordering::Relaxed), State::Pending) {
844 super::cmd::PARSE_CMD.scoped(WIDGET.id()).each_update(true, false, |args| {
845 state.store(State::Requested, Ordering::Relaxed);
848 TEXT.resolved().txt.update();
849 args.propagation.stop();
850 });
851 }
852
853 if let Some(true) = TXT_PARSE_LIVE_VAR.get_new()
854 && matches!(state.load(Ordering::Relaxed), State::Pending)
855 {
856 TEXT.resolved().txt.update();
859 }
860
861 if let Some(error) = error.get_new() {
862 _error_note = if error.is_empty() {
865 DataNoteHandle::dummy()
866 } else {
867 DATA.invalidate(error)
868 };
869 }
870 }
871 _ => {}
872 })
873}
874
875pub(super) fn on_change_stop(child: impl IntoUiNode, handler: Handler<ChangeStopArgs>) -> UiNode {
876 let mut handler = handler.into_wgt_runner();
877 let mut pending = None;
878 match_node(child, move |c, op| match op {
879 UiNodeOp::Deinit => {
880 handler.deinit();
881 }
882 UiNodeOp::Update { updates } => {
883 if TEXT.resolved().txt.is_new() {
884 let deadline = TIMERS.deadline(CHANGE_STOP_DELAY_VAR.get());
885 deadline.subscribe(UpdateOp::Update, WIDGET.id()).perm();
886 pending = Some(deadline);
887 } else if let Some(p) = &pending
888 && p.get().has_elapsed()
889 {
890 pending = None;
891
892 handler.event(&ChangeStopArgs {
893 cause: ChangeStopCause::DelayElapsed,
894 });
895 }
896
897 c.update(updates);
898 handler.update();
899
900 if pending.is_none() {
901 return;
902 }
903
904 KEY_INPUT_EVENT.each_update(false, |args| {
905 if let KeyState::Pressed = args.state
906 && let Key::Enter = &args.key
907 && !ACCEPTS_ENTER_VAR.get()
908 {
909 pending = None;
910 handler.event(&ChangeStopArgs {
911 cause: ChangeStopCause::Enter,
912 });
913 }
914 });
915 FOCUS_CHANGED_EVENT.each_update(true, |args| {
916 let target = WIDGET.id();
917 if args.is_blur(target) {
918 pending = None;
919 handler.event(&ChangeStopArgs {
920 cause: ChangeStopCause::Blur,
921 });
922 }
923 });
924 }
925 _ => {}
926 })
927}
928
929pub fn selection_toolbar_node(child: impl IntoUiNode) -> UiNode {
931 use super::node::*;
932
933 let mut selection_range = None;
934 let mut popup_state = None::<Var<PopupState>>;
935 match_node(child, move |c, op| {
936 let mut open = false;
937 let mut open_long_press = false;
938 let mut close = false;
939 match op {
940 UiNodeOp::Init => {
941 WIDGET.sub_var(&SELECTION_TOOLBAR_FN_VAR);
942 }
943 UiNodeOp::Deinit => {
944 close = true;
945 }
946 UiNodeOp::Update { updates } => {
947 if SELECTION_TOOLBAR_FN_VAR.is_new() {
948 close = true;
949 }
950 if let Some(state) = popup_state.as_ref().and_then(|s| s.get_new()) {
951 let is_open = !matches!(state, PopupState::Closed);
952 let mut r_txt = TEXT.resolve();
953 if r_txt.selection_toolbar_is_open != is_open {
954 r_txt.selection_toolbar_is_open = is_open;
955 WIDGET.layout().render();
956
957 if !is_open {
958 popup_state = None;
959 }
960 }
961 }
962
963 c.update(updates);
964
965 let open_id = || {
966 if let Some(popup_state) = &popup_state
967 && let PopupState::Open(id) = popup_state.get()
968 {
969 return Some(id);
970 }
971 None
972 };
973
974 MOUSE_INPUT_EVENT.each_update(true, |args| {
975 if open_id().map(|id| !args.target.contains(id)).unwrap_or(false) {
976 close = true;
977 }
978 if args.state == ButtonState::Released {
979 open = true;
980 }
981 });
982
983 if TOUCH_LONG_PRESS_EVENT.has_update(true) {
984 open = true;
985 open_long_press = true;
986 }
987 if KEY_INPUT_EVENT.has_update(true) {
988 close = true;
989 }
990
991 FOCUS_CHANGED_EVENT.each_update(true, |args| {
992 if args.is_blur(WIDGET.id())
993 && open_id()
994 .map(|id| args.new_focus.as_ref().map(|p| !p.contains(id)).unwrap_or(true))
995 .unwrap_or(false)
996 {
997 close = true;
998 }
999 });
1000 TOUCH_INPUT_EVENT.each_update(true, |args| {
1001 if matches!(args.phase, TouchPhase::Start | TouchPhase::Move)
1002 && open_id().map(|id| !args.target.contains(id)).unwrap_or(false)
1003 {
1004 close = true;
1005 }
1006 });
1007
1008 if popup_state.is_some() {
1009 let r_txt = TEXT.resolved();
1010 if selection_range != r_txt.caret.selection_range() {
1011 close = true;
1012 }
1013 }
1014 }
1015 _ => {}
1016 }
1017 if close && let Some(state) = &popup_state.take() {
1018 selection_range = None;
1019 POPUP.close(state);
1020 TEXT.resolve().selection_toolbar_is_open = false;
1021 WIDGET.layout().render();
1022 }
1023 if open {
1024 let r_txt = TEXT.resolved();
1025
1026 let range = r_txt.caret.selection_range();
1027 if open_long_press || range.is_some() {
1028 selection_range = range;
1029
1030 let toolbar_fn = SELECTION_TOOLBAR_FN_VAR.get();
1031 if let Some(node) = toolbar_fn.call_checked(SelectionToolbarArgs {
1032 anchor_id: WIDGET.id(),
1033 is_touch: matches!(r_txt.selection_by, SelectionBy::Touch),
1034 }) {
1035 let (node, _) = node.init_widget();
1036
1037 let mut translate = PxVector::zero();
1038 let transform_key = FrameValueKey::new_unique();
1039 let node = match_widget(node, move |c, op| match op {
1040 UiNodeOp::Init => {
1041 c.init();
1042 if let Some(mut wgt) = c.node().as_widget() {
1043 wgt.with_context(WidgetUpdateMode::Bubble, || WIDGET.sub_var_layout(&SELECTION_TOOLBAR_ANCHOR_VAR));
1044 }
1045 }
1046 UiNodeOp::Layout { wl, final_size } => {
1047 let r_txt = TEXT.resolved();
1048
1049 let range = if open_long_press {
1050 Some(r_txt.caret.selection_range().unwrap_or_else(|| {
1051 let i = r_txt.caret.index.unwrap_or(CaretIndex::ZERO);
1052 i..i
1053 }))
1054 } else {
1055 r_txt.caret.selection_range()
1056 };
1057
1058 if let Some(range) = range {
1059 let l_txt = TEXT.laidout();
1060 let r_txt = r_txt.segmented_text.text();
1061
1062 let mut bounds = PxBox::new(PxPoint::splat(Px::MAX), PxPoint::splat(Px::MIN));
1063 for line_rect in l_txt.shaped_text.highlight_rects(range, r_txt) {
1064 let line_box = line_rect.to_box2d();
1065 bounds.min = bounds.min.min(line_box.min);
1066 bounds.max = bounds.max.max(line_box.max);
1067 }
1068 let selection_bounds = bounds.to_rect();
1069
1070 *final_size = c.layout(wl);
1071
1072 let offset = SELECTION_TOOLBAR_ANCHOR_VAR.get();
1073
1074 fn layout_offset(size: PxSize, point: Point) -> PxVector {
1075 LAYOUT
1076 .with_constraints(PxConstraints2d::new_exact_size(size), || point.layout())
1077 .to_vector()
1078 }
1079 let place = layout_offset(selection_bounds.size, offset.place);
1080 let origin = layout_offset(*final_size, offset.origin);
1081
1082 translate = selection_bounds.origin.to_vector() + place - origin;
1083 } else {
1084 wl.collapse();
1086 *final_size = PxSize::zero();
1087 };
1088 }
1089 UiNodeOp::Render { frame } => {
1090 let l_txt = TEXT.laidout();
1091 let transform = l_txt.render_info.transform.then_translate(translate.cast());
1092 let transform = adjust_viewport_bound(transform, c.node());
1093
1094 frame.push_reference_frame(transform_key.into(), FrameValue::Value(transform), true, false, |frame| {
1095 c.render(frame)
1096 });
1097 }
1098 UiNodeOp::RenderUpdate { update } => {
1099 let l_txt = TEXT.laidout();
1100 let transform = l_txt.render_info.transform.then_translate(translate.cast());
1101 let transform = adjust_viewport_bound(transform, c.node());
1102
1103 update.with_transform(transform_key.update(transform, true), false, |update| c.render_update(update));
1104 }
1105 _ => {}
1106 });
1107
1108 let capture = ContextCapture::CaptureBlend {
1110 filter: CaptureFilter::Exclude({
1111 let mut exclude = ContextValueSet::new();
1112 super::Text::context_vars_set(&mut exclude);
1113
1114 let mut allow = ContextValueSet::new();
1115 super::LangMix::<()>::context_vars_set(&mut allow);
1116 exclude.remove_all(&allow);
1117 exclude.remove(&SELECTION_TOOLBAR_ANCHOR_VAR);
1118
1119 exclude
1120 }),
1121 over: false,
1122 };
1123
1124 let mut anchor_mode = AnchorMode::tooltip();
1125 anchor_mode.transform = AnchorTransform::None;
1126 let state = POPUP.open_config(node, anchor_mode, capture);
1127 state.subscribe(UpdateOp::Update, WIDGET.id()).perm();
1128 popup_state = Some(state);
1129 drop(r_txt);
1130 TEXT.resolve().selection_toolbar_is_open = true;
1131 WIDGET.layout().render();
1132 }
1133 };
1134 }
1135 })
1136}
1137fn adjust_viewport_bound(transform: PxTransform, widget: &mut UiNode) -> PxTransform {
1138 let window_bounds = WINDOW.vars().actual_size_px().get();
1139 let wgt_bounds = PxBox::from(match widget.as_widget() {
1140 Some(mut w) => w.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().outer_size()),
1141 None => PxSize::zero(),
1142 });
1143 let wgt_bounds = transform.outer_transformed(wgt_bounds).unwrap_or_default();
1144
1145 let x_underflow = -wgt_bounds.min.x.min(Px(0));
1146 let x_overflow = (wgt_bounds.max.x - window_bounds.width).max(Px(0));
1147 let y_underflow = -wgt_bounds.min.y.min(Px(0));
1148 let y_overflow = (wgt_bounds.max.y - window_bounds.height).max(Px(0));
1149
1150 let x = x_underflow - x_overflow;
1151 let y = y_underflow - y_overflow;
1152
1153 let correction = PxVector::new(x, y);
1154
1155 transform.then_translate(correction.cast())
1156}