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 fn no_dispatch_context() -> Vec<EventUpdate> {
512 panic!("`RichText::notify_leaf` must be called inside `UiNode::event` only")
513 }
514}
515
516context_local! {
517 static RICH_TEXT: RwLock<RichText> = RwLock::new(RichText::no_context());
519 static RESOLVED_TEXT: RwLock<ResolvedText> = RwLock::new(ResolvedText::no_context());
521 static LAIDOUT_TEXT: RwLock<LaidoutText> = RwLock::new(LaidoutText::no_context());
523 static RICH_TEXT_NOTIFY: RwLock<Vec<EventUpdate>> = RwLock::new(RichText::no_dispatch_context());
525}
526
527impl RichText {
528 pub fn notify_leaf(&self, update: EventUpdate) {
541 RICH_TEXT_NOTIFY.write().push(update);
542 }
543}
544
545bitflags! {
546 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
548 pub struct PendingLayout: u8 {
549 const UNDERLINE = 0b0000_0001;
551 const STRIKETHROUGH = 0b0000_0010;
553 const OVERLINE = 0b0000_0100;
555 const CARET = 0b0000_1000;
557 const OVERFLOW = 0b0001_0000;
559 const RESHAPE_LINES = 0b0111_1111;
561 const RESHAPE = 0b1111_1111;
563 }
564}
565
566pub fn line_placeholder(width: impl IntoVar<Length>) -> UiNode {
572 let child = layout_text(FillUiNode);
573 let child = resolve_text(child, " ");
574 zng_wgt_size_offset::width(child, width)
575}
576
577pub(super) fn get_caret_index(child: impl IntoUiNode, index: impl IntoVar<Option<CaretIndex>>) -> UiNode {
578 let index = index.into_var();
579 match_node(child, move |c, op| {
580 let mut u = false;
581 match op {
582 UiNodeOp::Init => {
583 c.init();
584 index.set(TEXT.resolved().caret.index);
585 }
586 UiNodeOp::Deinit => {
587 index.set(None);
588 }
589 UiNodeOp::Event { update } => {
590 c.event(update);
591 u = true;
592 }
593 UiNodeOp::Update { updates } => {
594 c.update(updates);
595 u = true;
596 }
597 _ => {}
598 }
599 if u {
600 let t = TEXT.resolved();
601 let idx = t.caret.index;
602 if !t.pending_edit && index.get() != idx {
603 index.set(idx);
604 }
605 }
606 })
607}
608
609pub(super) fn get_caret_status(child: impl IntoUiNode, status: impl IntoVar<CaretStatus>) -> UiNode {
610 let status = status.into_var();
611 match_node(child, move |c, op| {
612 let mut u = false;
613 match op {
614 UiNodeOp::Init => {
615 c.init();
616 let t = TEXT.resolved();
617 status.set(match t.caret.index {
618 None => CaretStatus::none(),
619 Some(i) => CaretStatus::new(i.index, &t.segmented_text),
620 });
621 }
622 UiNodeOp::Deinit => {
623 status.set(CaretStatus::none());
624 }
625 UiNodeOp::Event { update } => {
626 c.event(update);
627 u = true;
628 }
629 UiNodeOp::Update { updates } => {
630 c.update(updates);
631 u = true;
632 }
633 _ => {}
634 }
635 if u {
636 let t = TEXT.resolved();
637 let idx = t.caret.index;
638 if !t.pending_edit && status.get().index() != idx.map(|ci| ci.index) {
639 status.set(match idx {
640 None => CaretStatus::none(),
641 Some(i) => CaretStatus::new(i.index, &t.segmented_text),
642 });
643 }
644 }
645 })
646}
647
648pub(super) fn get_lines_len(child: impl IntoUiNode, len: impl IntoVar<usize>) -> UiNode {
649 let len = len.into_var();
650 match_node(child, move |c, op| match op {
651 UiNodeOp::Deinit => {
652 len.set(0usize);
653 }
654 UiNodeOp::Layout { wl, final_size } => {
655 *final_size = c.layout(wl);
656 let t = TEXT.laidout();
657 let l = t.shaped_text.lines_len();
658 if l != len.get() {
659 len.set(t.shaped_text.lines_len());
660 }
661 }
662 _ => {}
663 })
664}
665
666pub(super) fn get_lines_wrap_count(child: impl IntoUiNode, lines: impl IntoVar<super::LinesWrapCount>) -> UiNode {
667 let lines = lines.into_var();
668 let mut version = 0;
669 match_node(child, move |c, op| match op {
670 UiNodeOp::Deinit => {
671 lines.set(super::LinesWrapCount::NoWrap(0));
672 }
673 UiNodeOp::Layout { wl, final_size } => {
674 *final_size = c.layout(wl);
675 let t = TEXT.laidout();
676 if t.shaped_text_version != version {
677 version = t.shaped_text_version;
678 if let Some(update) = lines.with(|l| lines_wrap_count(l, &t.shaped_text)) {
679 lines.set(update);
680 }
681 }
682 }
683 _ => {}
684 })
685}
686fn lines_wrap_count(prev: &super::LinesWrapCount, txt: &ShapedText) -> Option<super::LinesWrapCount> {
688 match prev {
689 super::LinesWrapCount::NoWrap(len) => {
690 let mut counter = lines_wrap_counter(txt);
691 let mut l = 0;
692 for c in &mut counter {
693 if c != 1 {
694 let mut wrap = vec![1; l];
696 wrap.push(c);
697 wrap.extend(&mut counter);
698 return Some(super::LinesWrapCount::Wrap(wrap));
699 }
700 l += 1;
701 }
702 if l != *len {
703 Some(super::LinesWrapCount::NoWrap(l))
705 } else {
706 None
707 }
708 }
709 super::LinesWrapCount::Wrap(counts) => {
710 let mut prev_counts = counts.iter();
713 let mut new_counts = lines_wrap_counter(txt);
714 let mut eq_l = 0;
715 let mut eq_wrap = false;
716 for c in &mut new_counts {
717 if prev_counts.next() == Some(&c) {
718 eq_l += 1;
719 eq_wrap |= c != 1;
720 } else if eq_wrap || c != 1 {
721 let mut wrap = counts[..eq_l].to_vec();
723 wrap.push(c);
724 wrap.extend(&mut new_counts);
725 return Some(super::LinesWrapCount::Wrap(wrap));
726 } else {
727 let mut l = eq_l + 1; for c in &mut new_counts {
730 if c != 1 {
731 let mut wrap = vec![1; l];
733 wrap.push(c);
734 wrap.extend(&mut new_counts);
735 return Some(super::LinesWrapCount::Wrap(wrap));
736 }
737 l += 1;
738 }
739 return Some(super::LinesWrapCount::NoWrap(l));
741 }
742 }
743 if prev_counts.next().is_some() {
744 Some(super::LinesWrapCount::Wrap(counts[..eq_l].to_vec()))
745 } else {
746 None
747 }
748 }
749 }
750}
751fn lines_wrap_counter(txt: &ShapedText) -> impl Iterator<Item = u32> + '_ {
752 struct Counter<I> {
753 lines: I,
754 count: u32,
755 }
756 impl<'a, I: Iterator<Item = ShapedLine<'a>>> Iterator for Counter<I> {
757 type Item = u32;
758
759 fn next(&mut self) -> Option<u32> {
760 loop {
761 let line = self.lines.next()?;
762 if line.ended_by_wrap() {
763 self.count += 1;
764 continue;
765 }
766
767 let c = self.count;
768 self.count = 1;
769
770 return Some(c);
771 }
772 }
773 }
774 Counter {
775 lines: txt.lines(),
776 count: 1,
777 }
778}
779
780pub(super) fn parse_text<T>(child: impl IntoUiNode, value: impl IntoVar<T>) -> UiNode
781where
782 T: super::TxtParseValue,
783{
784 let value = value.into_var();
785
786 let error = var(Txt::from_static(""));
787 let mut _error_note = DataNoteHandle::dummy();
788
789 #[derive(Clone, Copy, bytemuck::NoUninit)]
790 #[repr(u8)]
791 enum State {
792 Sync,
793 Requested,
794 Pending,
795 }
796 let state = Arc::new(Atomic::new(State::Sync));
797
798 match_node(child, move |_, op| match op {
799 UiNodeOp::Init => {
800 let ctx = TEXT.resolved();
801
802 ctx.txt.set_from_map(&value, |val| val.to_txt());
804
805 let live = TXT_PARSE_LIVE_VAR.current_context();
813 let is_pending = TXT_PARSE_PENDING_VAR.current_context();
814 let cmd_handle = Arc::new(super::cmd::PARSE_CMD.scoped(WIDGET.id()).subscribe(false));
815
816 let binding = ctx.txt.bind_filter_map_bidi(
817 &value,
818 clmv!(state, error, is_pending, cmd_handle, |txt| {
819 if live.get() || matches!(state.load(Ordering::Relaxed), State::Requested) {
820 if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
823 is_pending.set(false);
825 cmd_handle.set_enabled(false);
826 }
827
828 match T::from_txt(txt) {
830 Ok(val) => {
831 error.set(Txt::from_static(""));
832 Some(val)
833 }
834 Err(e) => {
835 error.set(e);
836 None
837 }
838 }
839 } else {
840 if !matches!(state.swap(State::Pending, Ordering::Relaxed), State::Pending) {
843 is_pending.set(true);
845 cmd_handle.set_enabled(true);
846 }
847
848 None
850 }
851 }),
852 clmv!(state, error, |val| {
853 error.set(Txt::from_static(""));
856
857 if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
858 is_pending.set(false);
859 cmd_handle.set_enabled(false);
860 }
861
862 Some(val.to_txt())
863 }),
864 );
865
866 WIDGET.sub_var(&TXT_PARSE_LIVE_VAR).sub_var(&error).push_var_handles(binding);
869 }
870 UiNodeOp::Deinit => {
871 _error_note = DataNoteHandle::dummy();
872 }
873 UiNodeOp::Event { update } => {
874 if let Some(args) = super::cmd::PARSE_CMD.scoped(WIDGET.id()).on_unhandled(update)
875 && matches!(state.load(Ordering::Relaxed), State::Pending)
876 {
877 state.store(State::Requested, Ordering::Relaxed);
880 TEXT.resolved().txt.update();
881 args.propagation().stop();
882 }
883 }
884 UiNodeOp::Update { .. } => {
885 if let Some(true) = TXT_PARSE_LIVE_VAR.get_new()
886 && matches!(state.load(Ordering::Relaxed), State::Pending)
887 {
888 TEXT.resolved().txt.update();
891 }
892
893 if let Some(error) = error.get_new() {
894 _error_note = if error.is_empty() {
897 DataNoteHandle::dummy()
898 } else {
899 DATA.invalidate(error)
900 };
901 }
902 }
903 _ => {}
904 })
905}
906
907pub(super) fn on_change_stop(child: impl IntoUiNode, handler: Handler<ChangeStopArgs>) -> UiNode {
908 let mut handler = handler.into_wgt_runner();
909 let mut pending = None;
910 match_node(child, move |c, op| match op {
911 UiNodeOp::Deinit => {
912 handler.deinit();
913 }
914 UiNodeOp::Event { update } => {
915 if pending.is_none() {
916 return;
917 }
918
919 if let Some(args) = KEY_INPUT_EVENT.on_unhandled(update) {
920 if let KeyState::Pressed = args.state
921 && let Key::Enter = &args.key
922 && !ACCEPTS_ENTER_VAR.get()
923 {
924 pending = None;
925 handler.event(&ChangeStopArgs {
926 cause: ChangeStopCause::Enter,
927 });
928 }
929 } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
930 let target = WIDGET.id();
931 if args.is_blur(target) {
932 pending = None;
933 handler.event(&ChangeStopArgs {
934 cause: ChangeStopCause::Blur,
935 });
936 }
937 }
938 }
939 UiNodeOp::Update { updates } => {
940 if TEXT.resolved().txt.is_new() {
941 let deadline = TIMERS.deadline(CHANGE_STOP_DELAY_VAR.get());
942 deadline.subscribe(UpdateOp::Update, WIDGET.id()).perm();
943 pending = Some(deadline);
944 } else if let Some(p) = &pending
945 && p.get().has_elapsed()
946 {
947 pending = None;
948
949 handler.event(&ChangeStopArgs {
950 cause: ChangeStopCause::DelayElapsed,
951 });
952 }
953
954 c.update(updates);
955 handler.update();
956 }
957 _ => {}
958 })
959}
960
961pub fn selection_toolbar_node(child: impl IntoUiNode) -> UiNode {
963 use super::node::*;
964
965 let mut selection_range = None;
966 let mut popup_state = None::<Var<PopupState>>;
967 match_node(child, move |c, op| {
968 let mut open = false;
969 let mut open_long_press = false;
970 let mut close = false;
971 match op {
972 UiNodeOp::Init => {
973 WIDGET.sub_var(&SELECTION_TOOLBAR_FN_VAR);
974 }
975 UiNodeOp::Deinit => {
976 close = true;
977 }
978 UiNodeOp::Event { update } => {
979 c.event(update);
980
981 let open_id = || {
982 if let Some(popup_state) = &popup_state
983 && let PopupState::Open(id) = popup_state.get()
984 {
985 return Some(id);
986 }
987 None
988 };
989
990 if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
991 if open_id().map(|id| !args.target.contains(id)).unwrap_or(false) {
992 close = true;
993 }
994 if args.state == ButtonState::Released {
995 open = true;
996 }
997 } else if TOUCH_LONG_PRESS_EVENT.has(update) {
998 open = true;
999 open_long_press = true;
1000 } else if KEY_INPUT_EVENT.has(update) {
1001 close = true;
1002 } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
1003 if args.is_blur(WIDGET.id())
1004 && open_id()
1005 .map(|id| args.new_focus.as_ref().map(|p| !p.contains(id)).unwrap_or(true))
1006 .unwrap_or(false)
1007 {
1008 close = true;
1009 }
1010 } else if let Some(args) = TOUCH_INPUT_EVENT.on(update)
1011 && matches!(args.phase, TouchPhase::Start | TouchPhase::Move)
1012 && open_id().map(|id| !args.target.contains(id)).unwrap_or(false)
1013 {
1014 close = true;
1015 }
1016
1017 if popup_state.is_some() {
1018 let r_txt = TEXT.resolved();
1019 if selection_range != r_txt.caret.selection_range() {
1020 close = true;
1021 }
1022 }
1023 }
1024 UiNodeOp::Update { .. } => {
1025 if SELECTION_TOOLBAR_FN_VAR.is_new() {
1026 close = true;
1027 }
1028 if let Some(state) = popup_state.as_ref().and_then(|s| s.get_new()) {
1029 let is_open = !matches!(state, PopupState::Closed);
1030 let mut r_txt = TEXT.resolve();
1031 if r_txt.selection_toolbar_is_open != is_open {
1032 r_txt.selection_toolbar_is_open = is_open;
1033 WIDGET.layout().render();
1034
1035 if !is_open {
1036 popup_state = None;
1037 }
1038 }
1039 }
1040 }
1041 _ => {}
1042 }
1043 if close && let Some(state) = &popup_state.take() {
1044 selection_range = None;
1045 POPUP.close(state);
1046 TEXT.resolve().selection_toolbar_is_open = false;
1047 WIDGET.layout().render();
1048 }
1049 if open {
1050 let r_txt = TEXT.resolved();
1051
1052 let range = r_txt.caret.selection_range();
1053 if open_long_press || range.is_some() {
1054 selection_range = range;
1055
1056 let toolbar_fn = SELECTION_TOOLBAR_FN_VAR.get();
1057 if let Some(node) = toolbar_fn.call_checked(SelectionToolbarArgs {
1058 anchor_id: WIDGET.id(),
1059 is_touch: matches!(r_txt.selection_by, SelectionBy::Touch),
1060 }) {
1061 let (node, _) = node.init_widget();
1062
1063 let mut translate = PxVector::zero();
1064 let transform_key = FrameValueKey::new_unique();
1065 let node = match_widget(node, move |c, op| match op {
1066 UiNodeOp::Init => {
1067 c.init();
1068 if let Some(mut wgt) = c.node().as_widget() {
1069 wgt.with_context(WidgetUpdateMode::Bubble, || WIDGET.sub_var_layout(&SELECTION_TOOLBAR_ANCHOR_VAR));
1070 }
1071 }
1072 UiNodeOp::Layout { wl, final_size } => {
1073 let r_txt = TEXT.resolved();
1074
1075 let range = if open_long_press {
1076 Some(r_txt.caret.selection_range().unwrap_or_else(|| {
1077 let i = r_txt.caret.index.unwrap_or(CaretIndex::ZERO);
1078 i..i
1079 }))
1080 } else {
1081 r_txt.caret.selection_range()
1082 };
1083
1084 if let Some(range) = range {
1085 let l_txt = TEXT.laidout();
1086 let r_txt = r_txt.segmented_text.text();
1087
1088 let mut bounds = PxBox::new(PxPoint::splat(Px::MAX), PxPoint::splat(Px::MIN));
1089 for line_rect in l_txt.shaped_text.highlight_rects(range, r_txt) {
1090 let line_box = line_rect.to_box2d();
1091 bounds.min = bounds.min.min(line_box.min);
1092 bounds.max = bounds.max.max(line_box.max);
1093 }
1094 let selection_bounds = bounds.to_rect();
1095
1096 *final_size = c.layout(wl);
1097
1098 let offset = SELECTION_TOOLBAR_ANCHOR_VAR.get();
1099
1100 fn layout_offset(size: PxSize, point: Point) -> PxVector {
1101 LAYOUT
1102 .with_constraints(PxConstraints2d::new_exact_size(size), || point.layout())
1103 .to_vector()
1104 }
1105 let place = layout_offset(selection_bounds.size, offset.place);
1106 let origin = layout_offset(*final_size, offset.origin);
1107
1108 translate = selection_bounds.origin.to_vector() + place - origin;
1109 } else {
1110 wl.collapse();
1112 *final_size = PxSize::zero();
1113 };
1114 }
1115 UiNodeOp::Render { frame } => {
1116 let l_txt = TEXT.laidout();
1117 let transform = l_txt.render_info.transform.then_translate(translate.cast());
1118 let transform = adjust_viewport_bound(transform, c.node());
1119
1120 frame.push_reference_frame(transform_key.into(), FrameValue::Value(transform), true, false, |frame| {
1121 c.render(frame)
1122 });
1123 }
1124 UiNodeOp::RenderUpdate { update } => {
1125 let l_txt = TEXT.laidout();
1126 let transform = l_txt.render_info.transform.then_translate(translate.cast());
1127 let transform = adjust_viewport_bound(transform, c.node());
1128
1129 update.with_transform(transform_key.update(transform, true), false, |update| c.render_update(update));
1130 }
1131 _ => {}
1132 });
1133
1134 let capture = ContextCapture::CaptureBlend {
1136 filter: CaptureFilter::Exclude({
1137 let mut exclude = ContextValueSet::new();
1138 super::Text::context_vars_set(&mut exclude);
1139
1140 let mut allow = ContextValueSet::new();
1141 super::LangMix::<()>::context_vars_set(&mut allow);
1142 exclude.remove_all(&allow);
1143 exclude.remove(&SELECTION_TOOLBAR_ANCHOR_VAR);
1144
1145 exclude
1146 }),
1147 over: false,
1148 };
1149
1150 let mut anchor_mode = AnchorMode::tooltip();
1151 anchor_mode.transform = AnchorTransform::None;
1152 let state = POPUP.open_config(node, anchor_mode, capture);
1153 state.subscribe(UpdateOp::Update, WIDGET.id()).perm();
1154 popup_state = Some(state);
1155 drop(r_txt);
1156 TEXT.resolve().selection_toolbar_is_open = true;
1157 WIDGET.layout().render();
1158 }
1159 };
1160 }
1161 })
1162}
1163fn adjust_viewport_bound(transform: PxTransform, widget: &mut UiNode) -> PxTransform {
1164 let window_bounds = WINDOW.vars().actual_size_px().get();
1165 let wgt_bounds = PxBox::from(match widget.as_widget() {
1166 Some(mut w) => w.with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().outer_size()),
1167 None => PxSize::zero(),
1168 });
1169 let wgt_bounds = transform.outer_transformed(wgt_bounds).unwrap_or_default();
1170
1171 let x_underflow = -wgt_bounds.min.x.min(Px(0));
1172 let x_overflow = (wgt_bounds.max.x - window_bounds.width).max(Px(0));
1173 let y_underflow = -wgt_bounds.min.y.min(Px(0));
1174 let y_overflow = (wgt_bounds.max.y - window_bounds.height).max(Px(0));
1175
1176 let x = x_underflow - x_overflow;
1177 let y = y_underflow - y_overflow;
1178
1179 let correction = PxVector::new(x, y);
1180
1181 transform.then_translate(correction.cast())
1182}