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 resolve;
27pub use resolve::*;
28
29mod layout;
30pub use layout::*;
31
32mod render;
33pub use render::*;
34
35mod caret;
36pub use caret::*;
37
38#[derive(Clone)]
40pub struct CaretInfo {
41 pub opacity: ReadOnlyArcVar<Factor>,
48
49 pub index: Option<CaretIndex>,
53
54 pub selection_index: Option<CaretIndex>,
56
57 pub initial_selection: Option<(ops::Range<CaretIndex>, bool)>,
60
61 pub index_version: Wrapping<u8>,
68
69 pub used_retained_x: bool,
73
74 pub skip_next_scroll: bool,
78}
79impl fmt::Debug for CaretInfo {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 f.debug_struct("CaretInfo")
82 .field("opacity", &self.opacity.debug())
83 .field("index", &self.index)
84 .field("index_version", &self.index_version)
85 .field("used_retained_x", &self.used_retained_x)
86 .finish()
87 }
88}
89impl CaretInfo {
90 pub fn set_index(&mut self, index: CaretIndex) {
92 self.index = Some(index);
93 self.index_version += 1;
94 }
95
96 pub fn set_selection(&mut self, start: CaretIndex, end: CaretIndex) {
100 self.selection_index = Some(start);
101 self.set_index(end);
102 }
103
104 pub fn clear_selection(&mut self) {
106 self.selection_index = None;
107 self.initial_selection = None;
108 self.index_version += 1;
109 }
110
111 pub fn set_char_index(&mut self, index: usize) {
115 if let Some(i) = &mut self.index {
116 i.index = index;
117 } else {
118 self.index = Some(CaretIndex { index, line: 0 });
119 }
120 self.index_version += 1;
121 }
122
123 pub fn set_char_selection(&mut self, start: usize, end: usize) {
129 if let Some(s) = &mut self.selection_index {
130 s.index = start;
131 } else {
132 self.selection_index = Some(CaretIndex { index: start, line: 0 });
133 }
134 self.set_char_index(end);
135 }
136
137 pub fn selection_range(&self) -> Option<ops::Range<CaretIndex>> {
142 let a = self.index?;
143 let b = self.selection_index?;
144
145 use std::cmp::Ordering;
146 match a.index.cmp(&b.index) {
147 Ordering::Less => Some(a..b),
148 Ordering::Equal => None,
149 Ordering::Greater => Some(b..a),
150 }
151 }
152
153 pub fn selection_char_range(&self) -> Option<ops::Range<usize>> {
158 self.selection_range().map(|r| r.start.index..r.end.index)
159 }
160}
161
162#[derive(Clone)]
164pub struct ImePreview {
165 pub txt: Txt,
167
168 pub prev_caret: CaretIndex,
170 pub prev_selection: Option<CaretIndex>,
174}
175
176pub struct TEXT;
181
182impl TEXT {
183 pub fn try_resolved(&self) -> Option<RwLockReadGuardOwned<ResolvedText>> {
187 if RESOLVED_TEXT.is_default() {
188 None
189 } else {
190 Some(RESOLVED_TEXT.read_recursive())
191 }
192 }
193
194 pub fn resolved(&self) -> RwLockReadGuardOwned<ResolvedText> {
200 RESOLVED_TEXT.read_recursive()
201 }
202
203 pub fn try_laidout(&self) -> Option<RwLockReadGuardOwned<LaidoutText>> {
207 if LAIDOUT_TEXT.is_default() {
208 None
209 } else {
210 Some(LAIDOUT_TEXT.read_recursive())
211 }
212 }
213
214 pub fn laidout(&self) -> RwLockReadGuardOwned<LaidoutText> {
220 LAIDOUT_TEXT.read_recursive()
221 }
222
223 pub fn resolve_caret(&self) -> MappedRwLockWriteGuardOwned<ResolvedText, CaretInfo> {
229 RwLockWriteGuardOwned::map(self.resolve(), |ctx| &mut ctx.caret)
230 }
231
232 pub(crate) fn resolve(&self) -> RwLockWriteGuardOwned<ResolvedText> {
233 RESOLVED_TEXT.write()
234 }
235
236 fn layout(&self) -> RwLockWriteGuardOwned<LaidoutText> {
237 LAIDOUT_TEXT.write()
238 }
239}
240
241#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, bytemuck::NoUninit)]
245#[repr(u8)]
246pub enum SelectionBy {
247 Command = 0,
249 Keyboard = 1,
251 Mouse = 2,
253 Touch = 3,
255}
256impl SelectionBy {
257 pub fn matches_interactive_mode(self, mode: InteractiveCaretMode) -> bool {
259 match mode {
260 InteractiveCaretMode::TouchOnly => matches!(self, SelectionBy::Touch),
261 InteractiveCaretMode::Enabled => true,
262 InteractiveCaretMode::Disabled => false,
263 }
264 }
265}
266
267pub struct ResolvedText {
271 pub txt: BoxedVar<Txt>,
273 pub ime_preview: Option<ImePreview>,
276
277 pub segmented_text: SegmentedText,
279 pub faces: FontFaceList,
281 pub synthesis: FontSynthesis,
283
284 pub pending_layout: PendingLayout,
289
290 pub pending_edit: bool,
292
293 pub caret: CaretInfo,
295
296 pub selection_by: SelectionBy,
298
299 pub selection_toolbar_is_open: bool,
301}
302impl fmt::Debug for ResolvedText {
303 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304 f.debug_struct("ResolvedText")
305 .field("segmented_text", &self.segmented_text)
306 .field("faces", &self.faces)
307 .field("synthesis", &self.synthesis)
308 .field("pending_layout", &self.pending_layout)
309 .field("pending_edit", &self.pending_edit)
310 .field("caret", &self.caret)
311 .field("selection_by", &self.selection_by)
312 .field("selection_toolbar_is_open", &self.selection_toolbar_is_open)
313 .finish_non_exhaustive()
314 }
315}
316impl ResolvedText {
317 fn no_context() -> Self {
318 panic!("no `ResolvedText` in context, only available inside `resolve_text`")
319 }
320}
321
322#[derive(Debug, Clone)]
324pub struct RenderInfo {
325 pub transform: PxTransform,
327 pub scale_factor: Factor,
329}
330impl Default for RenderInfo {
331 fn default() -> Self {
333 Self {
334 transform: PxTransform::identity(),
335 scale_factor: 1.fct(),
336 }
337 }
338}
339
340#[derive(Debug)]
344pub struct LaidoutText {
345 pub fonts: FontList,
349
350 pub shaped_text: ShapedText,
352
353 pub overflow: Option<TextOverflowInfo>,
355
356 pub overflow_suffix: Option<ShapedText>,
358
359 pub shaped_text_version: u32,
361
362 pub overlines: Vec<(PxPoint, Px)>,
368
369 pub overline_thickness: Px,
371
372 pub strikethroughs: Vec<(PxPoint, Px)>,
378 pub strikethrough_thickness: Px,
380
381 pub underlines: Vec<(PxPoint, Px)>,
388 pub underline_thickness: Px,
390
391 pub ime_underlines: Vec<(PxPoint, Px)>,
397 pub ime_underline_thickness: Px,
399
400 pub caret_origin: Option<PxPoint>,
402
403 pub caret_selection_origin: Option<PxPoint>,
405
406 pub caret_retained_x: Px,
408
409 pub render_info: RenderInfo,
411
412 pub viewport: PxSize,
414}
415
416impl LaidoutText {
417 fn no_context() -> Self {
418 panic!("no `LaidoutText` in context, only available inside `layout_text`")
419 }
420}
421
422context_local! {
423 static RESOLVED_TEXT: RwLock<ResolvedText> = RwLock::new(ResolvedText::no_context());
425 static LAIDOUT_TEXT: RwLock<LaidoutText> = RwLock::new(LaidoutText::no_context());
427}
428
429bitflags! {
430 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
432 pub struct PendingLayout: u8 {
433 const UNDERLINE = 0b0000_0001;
435 const STRIKETHROUGH = 0b0000_0010;
437 const OVERLINE = 0b0000_0100;
439 const CARET = 0b0000_1000;
441 const OVERFLOW = 0b0001_0000;
443 const RESHAPE_LINES = 0b0111_1111;
445 const RESHAPE = 0b1111_1111;
447 }
448}
449
450pub fn line_placeholder(width: impl IntoVar<Length>) -> impl UiNode {
456 let child = layout_text(FillUiNode);
457 let child = resolve_text(child, " ");
458 zng_wgt_size_offset::width(child, width)
459}
460
461pub(super) fn get_caret_index(child: impl UiNode, index: impl IntoVar<Option<CaretIndex>>) -> impl UiNode {
462 let index = index.into_var();
463 match_node(child, move |c, op| {
464 let mut u = false;
465 match op {
466 UiNodeOp::Init => {
467 c.init();
468 let _ = index.set(TEXT.resolved().caret.index);
469 }
470 UiNodeOp::Deinit => {
471 let _ = index.set(None);
472 }
473 UiNodeOp::Event { update } => {
474 c.event(update);
475 u = true;
476 }
477 UiNodeOp::Update { updates } => {
478 c.update(updates);
479 u = true;
480 }
481 _ => {}
482 }
483 if u {
484 let t = TEXT.resolved();
485 let idx = t.caret.index;
486 if !t.pending_edit && index.get() != idx {
487 let _ = index.set(idx);
488 }
489 }
490 })
491}
492
493pub(super) fn get_caret_status(child: impl UiNode, status: impl IntoVar<CaretStatus>) -> impl UiNode {
494 let status = status.into_var();
495 match_node(child, move |c, op| {
496 let mut u = false;
497 match op {
498 UiNodeOp::Init => {
499 c.init();
500 let t = TEXT.resolved();
501 let _ = status.set(match t.caret.index {
502 None => CaretStatus::none(),
503 Some(i) => CaretStatus::new(i.index, &t.segmented_text),
504 });
505 }
506 UiNodeOp::Deinit => {
507 let _ = status.set(CaretStatus::none());
508 }
509 UiNodeOp::Event { update } => {
510 c.event(update);
511 u = true;
512 }
513 UiNodeOp::Update { updates } => {
514 c.update(updates);
515 u = true;
516 }
517 _ => {}
518 }
519 if u {
520 let t = TEXT.resolved();
521 let idx = t.caret.index;
522 if !t.pending_edit && status.get().index() != idx.map(|ci| ci.index) {
523 let _ = status.set(match idx {
524 None => CaretStatus::none(),
525 Some(i) => CaretStatus::new(i.index, &t.segmented_text),
526 });
527 }
528 }
529 })
530}
531
532pub(super) fn get_lines_len(child: impl UiNode, len: impl IntoVar<usize>) -> impl UiNode {
533 let len = len.into_var();
534 match_node(child, move |c, op| match op {
535 UiNodeOp::Deinit => {
536 let _ = len.set(0usize);
537 }
538 UiNodeOp::Layout { wl, final_size } => {
539 *final_size = c.layout(wl);
540 let t = TEXT.laidout();
541 let l = t.shaped_text.lines_len();
542 if l != len.get() {
543 let _ = len.set(t.shaped_text.lines_len());
544 }
545 }
546 _ => {}
547 })
548}
549
550pub(super) fn get_lines_wrap_count(child: impl UiNode, lines: impl IntoVar<super::LinesWrapCount>) -> impl UiNode {
551 let lines = lines.into_var();
552 let mut version = 0;
553 match_node(child, move |c, op| match op {
554 UiNodeOp::Deinit => {
555 let _ = lines.set(super::LinesWrapCount::NoWrap(0));
556 }
557 UiNodeOp::Layout { wl, final_size } => {
558 *final_size = c.layout(wl);
559 let t = TEXT.laidout();
560 if t.shaped_text_version != version {
561 version = t.shaped_text_version;
562 if let Some(update) = lines.with(|l| lines_wrap_count(l, &t.shaped_text)) {
563 let _ = lines.set(update);
564 }
565 }
566 }
567 _ => {}
568 })
569}
570fn lines_wrap_count(prev: &super::LinesWrapCount, txt: &ShapedText) -> Option<super::LinesWrapCount> {
572 match prev {
573 super::LinesWrapCount::NoWrap(len) => {
574 let mut counter = lines_wrap_counter(txt);
575 let mut l = 0;
576 for c in &mut counter {
577 if c != 1 {
578 let mut wrap = vec![1; l];
580 wrap.push(c);
581 wrap.extend(&mut counter);
582 return Some(super::LinesWrapCount::Wrap(wrap));
583 }
584 l += 1;
585 }
586 if l != *len {
587 Some(super::LinesWrapCount::NoWrap(l))
589 } else {
590 None
591 }
592 }
593 super::LinesWrapCount::Wrap(counts) => {
594 let mut prev_counts = counts.iter();
597 let mut new_counts = lines_wrap_counter(txt);
598 let mut eq_l = 0;
599 let mut eq_wrap = false;
600 for c in &mut new_counts {
601 if prev_counts.next() == Some(&c) {
602 eq_l += 1;
603 eq_wrap |= c != 1;
604 } else if eq_wrap || c != 1 {
605 let mut wrap = counts[..eq_l].to_vec();
607 wrap.push(c);
608 wrap.extend(&mut new_counts);
609 return Some(super::LinesWrapCount::Wrap(wrap));
610 } else {
611 let mut l = eq_l + 1; for c in &mut new_counts {
614 if c != 1 {
615 let mut wrap = vec![1; l];
617 wrap.push(c);
618 wrap.extend(&mut new_counts);
619 return Some(super::LinesWrapCount::Wrap(wrap));
620 }
621 l += 1;
622 }
623 return Some(super::LinesWrapCount::NoWrap(l));
625 }
626 }
627 if prev_counts.next().is_some() {
628 Some(super::LinesWrapCount::Wrap(counts[..eq_l].to_vec()))
629 } else {
630 None
631 }
632 }
633 }
634}
635fn lines_wrap_counter(txt: &ShapedText) -> impl Iterator<Item = u32> + '_ {
636 struct Counter<I> {
637 lines: I,
638 count: u32,
639 }
640 impl<'a, I: Iterator<Item = ShapedLine<'a>>> Iterator for Counter<I> {
641 type Item = u32;
642
643 fn next(&mut self) -> Option<u32> {
644 loop {
645 let line = self.lines.next()?;
646 if line.ended_by_wrap() {
647 self.count += 1;
648 continue;
649 }
650
651 let c = self.count;
652 self.count = 1;
653
654 return Some(c);
655 }
656 }
657 }
658 Counter {
659 lines: txt.lines(),
660 count: 1,
661 }
662}
663
664pub(super) fn parse_text<T>(child: impl UiNode, value: impl IntoVar<T>) -> impl UiNode
665where
666 T: super::TxtParseValue,
667{
668 let value = value.into_var();
669
670 let error = var(Txt::from_static(""));
671 let mut _error_note = DataNoteHandle::dummy();
672
673 #[derive(Clone, Copy, bytemuck::NoUninit)]
674 #[repr(u8)]
675 enum State {
676 Sync,
677 Requested,
678 Pending,
679 }
680 let state = Arc::new(Atomic::new(State::Sync));
681
682 match_node(child, move |_, op| match op {
683 UiNodeOp::Init => {
684 let ctx = TEXT.resolved();
685
686 let _ = ctx.txt.set_from_map(&value, |val| val.to_txt());
688
689 let live = TXT_PARSE_LIVE_VAR.actual_var();
697 let is_pending = TXT_PARSE_PENDING_VAR.actual_var();
698 let cmd_handle = Arc::new(super::cmd::PARSE_CMD.scoped(WIDGET.id()).subscribe(false));
699
700 let binding = ctx.txt.bind_filter_map_bidi(
701 &value,
702 clmv!(state, error, is_pending, cmd_handle, |txt| {
703 if live.get() || matches!(state.load(Ordering::Relaxed), State::Requested) {
704 if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
707 let _ = is_pending.set(false);
709 cmd_handle.set_enabled(false);
710 }
711
712 match T::from_txt(txt) {
714 Ok(val) => {
715 error.set(Txt::from_static(""));
716 Some(val)
717 }
718 Err(e) => {
719 error.set(e);
720 None
721 }
722 }
723 } else {
724 if !matches!(state.swap(State::Pending, Ordering::Relaxed), State::Pending) {
727 let _ = is_pending.set(true);
729 cmd_handle.set_enabled(true);
730 }
731
732 None
734 }
735 }),
736 clmv!(state, error, |val| {
737 error.set(Txt::from_static(""));
740
741 if !matches!(state.swap(State::Sync, Ordering::Relaxed), State::Sync) {
742 let _ = is_pending.set(false);
743 cmd_handle.set_enabled(false);
744 }
745
746 Some(val.to_txt())
747 }),
748 );
749
750 WIDGET.sub_var(&TXT_PARSE_LIVE_VAR).sub_var(&error).push_var_handles(binding);
753 }
754 UiNodeOp::Deinit => {
755 _error_note = DataNoteHandle::dummy();
756 }
757 UiNodeOp::Event { update } => {
758 if let Some(args) = super::cmd::PARSE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
759 if matches!(state.load(Ordering::Relaxed), State::Pending) {
760 state.store(State::Requested, Ordering::Relaxed);
763 let _ = TEXT.resolved().txt.update();
764 args.propagation().stop();
765 }
766 }
767 }
768 UiNodeOp::Update { .. } => {
769 if let Some(true) = TXT_PARSE_LIVE_VAR.get_new() {
770 if matches!(state.load(Ordering::Relaxed), State::Pending) {
771 let _ = TEXT.resolved().txt.update();
774 }
775 }
776
777 if let Some(error) = error.get_new() {
778 _error_note = if error.is_empty() {
781 DataNoteHandle::dummy()
782 } else {
783 DATA.invalidate(error)
784 };
785 }
786 }
787 _ => {}
788 })
789}
790
791pub(super) fn on_change_stop(child: impl UiNode, mut handler: impl WidgetHandler<ChangeStopArgs>) -> impl UiNode {
792 let mut pending = None;
793 match_node(child, move |c, op| match op {
794 UiNodeOp::Event { update } => {
795 if pending.is_none() {
796 return;
797 }
798
799 if let Some(args) = KEY_INPUT_EVENT.on_unhandled(update) {
800 if let (KeyState::Pressed, Key::Enter) = (args.state, &args.key) {
801 if !ACCEPTS_ENTER_VAR.get() {
802 pending = None;
803 handler.event(&ChangeStopArgs {
804 cause: ChangeStopCause::Enter,
805 });
806 }
807 }
808 } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
809 let target = WIDGET.id();
810 if args.is_blur(target) {
811 pending = None;
812 handler.event(&ChangeStopArgs {
813 cause: ChangeStopCause::Blur,
814 });
815 }
816 }
817 }
818 UiNodeOp::Update { updates } => {
819 if TEXT.resolved().txt.is_new() {
820 let deadline = TIMERS.deadline(CHANGE_STOP_DELAY_VAR.get());
821 deadline.subscribe(UpdateOp::Update, WIDGET.id()).perm();
822 pending = Some(deadline);
823 } else if let Some(p) = &pending {
824 if p.get().has_elapsed() {
825 pending = None;
826
827 handler.event(&ChangeStopArgs {
828 cause: ChangeStopCause::DelayElapsed,
829 });
830 }
831 }
832
833 c.update(updates);
834 handler.update();
835 }
836 _ => {}
837 })
838}
839
840pub fn selection_toolbar_node(child: impl UiNode) -> impl UiNode {
842 use super::node::*;
843
844 let mut selection_range = None;
845 let mut popup_state = None::<ReadOnlyArcVar<PopupState>>;
846 match_node(child, move |c, op| {
847 let mut open = false;
848 let mut open_long_press = false;
849 let mut close = false;
850 match op {
851 UiNodeOp::Init => {
852 WIDGET.sub_var(&SELECTION_TOOLBAR_FN_VAR);
853 }
854 UiNodeOp::Deinit => {
855 close = true;
856 }
857 UiNodeOp::Event { update } => {
858 c.event(update);
859
860 let open_id = || {
861 if let Some(popup_state) = &popup_state {
862 if let PopupState::Open(id) = popup_state.get() {
863 return Some(id);
864 }
865 }
866 None
867 };
868
869 if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
870 if open_id().map(|id| !args.target.contains(id)).unwrap_or(false) {
871 close = true;
872 }
873 if args.state == ButtonState::Released {
874 open = true;
875 }
876 } else if TOUCH_LONG_PRESS_EVENT.has(update) {
877 open = true;
878 open_long_press = true;
879 } else if KEY_INPUT_EVENT.has(update) {
880 close = true;
881 } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
882 if args.is_blur(WIDGET.id())
883 && open_id()
884 .map(|id| args.new_focus.as_ref().map(|p| !p.contains(id)).unwrap_or(true))
885 .unwrap_or(false)
886 {
887 close = true;
888 }
889 } else if let Some(args) = TOUCH_INPUT_EVENT.on(update) {
890 if matches!(args.phase, TouchPhase::Start | TouchPhase::Move)
891 && open_id().map(|id| !args.target.contains(id)).unwrap_or(false)
892 {
893 close = true;
894 }
895 }
896
897 if popup_state.is_some() {
898 let r_txt = TEXT.resolved();
899 if selection_range != r_txt.caret.selection_range() {
900 close = true;
901 }
902 }
903 }
904 UiNodeOp::Update { .. } => {
905 if SELECTION_TOOLBAR_FN_VAR.is_new() {
906 close = true;
907 }
908 if let Some(state) = popup_state.as_ref().and_then(|s| s.get_new()) {
909 let is_open = !matches!(state, PopupState::Closed);
910 let mut r_txt = TEXT.resolve();
911 if r_txt.selection_toolbar_is_open != is_open {
912 r_txt.selection_toolbar_is_open = is_open;
913 WIDGET.layout().render();
914
915 if !is_open {
916 popup_state = None;
917 }
918 }
919 }
920 }
921 _ => {}
922 }
923 if close {
924 if let Some(state) = &popup_state.take() {
925 selection_range = None;
926 POPUP.close(state);
927 TEXT.resolve().selection_toolbar_is_open = false;
928 WIDGET.layout().render();
929 }
930 }
931 if open {
932 let r_txt = TEXT.resolved();
933
934 let range = r_txt.caret.selection_range();
935 if open_long_press || range.is_some() {
936 selection_range = range;
937
938 let toolbar_fn = SELECTION_TOOLBAR_FN_VAR.get();
939 if let Some(node) = toolbar_fn.call_checked(SelectionToolbarArgs {
940 anchor_id: WIDGET.id(),
941 is_touch: matches!(r_txt.selection_by, SelectionBy::Touch),
942 }) {
943 let (node, _) = node.init_widget();
944
945 let mut translate = PxVector::zero();
946 let transform_key = FrameValueKey::new_unique();
947 let node = match_widget(node, move |c, op| match op {
948 UiNodeOp::Init => {
949 c.init();
950 c.with_context(WidgetUpdateMode::Bubble, || WIDGET.sub_var_layout(&SELECTION_TOOLBAR_ANCHOR_VAR));
951 }
952 UiNodeOp::Layout { wl, final_size } => {
953 let r_txt = TEXT.resolved();
954
955 let range = if open_long_press {
956 Some(r_txt.caret.selection_range().unwrap_or_else(|| {
957 let i = r_txt.caret.index.unwrap_or(CaretIndex::ZERO);
958 i..i
959 }))
960 } else {
961 r_txt.caret.selection_range()
962 };
963
964 if let Some(range) = range {
965 let l_txt = TEXT.laidout();
966 let r_txt = r_txt.segmented_text.text();
967
968 let mut bounds = PxBox::new(PxPoint::splat(Px::MAX), PxPoint::splat(Px::MIN));
969 for line_rect in l_txt.shaped_text.highlight_rects(range, r_txt) {
970 let line_box = line_rect.to_box2d();
971 bounds.min = bounds.min.min(line_box.min);
972 bounds.max = bounds.max.max(line_box.max);
973 }
974 let selection_bounds = bounds.to_rect();
975
976 *final_size = c.layout(wl);
977
978 let offset = SELECTION_TOOLBAR_ANCHOR_VAR.get();
979
980 fn layout_offset(size: PxSize, point: Point) -> PxVector {
981 LAYOUT
982 .with_constraints(PxConstraints2d::new_exact_size(size), || point.layout())
983 .to_vector()
984 }
985 let place = layout_offset(selection_bounds.size, offset.place);
986 let origin = layout_offset(*final_size, offset.origin);
987
988 translate = selection_bounds.origin.to_vector() + place - origin;
989 } else {
990 wl.collapse();
992 *final_size = PxSize::zero();
993 };
994 }
995 UiNodeOp::Render { frame } => {
996 let l_txt = TEXT.laidout();
997 let transform = l_txt.render_info.transform.then_translate(translate.cast());
998 let transform = adjust_viewport_bound(transform, c);
999
1000 frame.push_reference_frame(transform_key.into(), FrameValue::Value(transform), true, false, |frame| {
1001 c.render(frame)
1002 });
1003 }
1004 UiNodeOp::RenderUpdate { update } => {
1005 let l_txt = TEXT.laidout();
1006 let transform = l_txt.render_info.transform.then_translate(translate.cast());
1007 let transform = adjust_viewport_bound(transform, c);
1008
1009 update.with_transform(transform_key.update(transform, true), false, |update| c.render_update(update));
1010 }
1011 _ => {}
1012 });
1013
1014 let capture = ContextCapture::CaptureBlend {
1016 filter: CaptureFilter::Exclude({
1017 let mut exclude = ContextValueSet::new();
1018 super::Text::context_vars_set(&mut exclude);
1019
1020 let mut allow = ContextValueSet::new();
1021 super::LangMix::<()>::context_vars_set(&mut allow);
1022 exclude.remove_all(&allow);
1023 exclude.remove(&SELECTION_TOOLBAR_ANCHOR_VAR);
1024
1025 exclude
1026 }),
1027 over: false,
1028 };
1029
1030 let mut anchor_mode = AnchorMode::tooltip();
1031 anchor_mode.transform = AnchorTransform::None;
1032 let state = POPUP.open_config(node, anchor_mode, capture);
1033 state.subscribe(UpdateOp::Update, WIDGET.id()).perm();
1034 popup_state = Some(state);
1035 drop(r_txt);
1036 TEXT.resolve().selection_toolbar_is_open = true;
1037 WIDGET.layout().render();
1038 }
1039 };
1040 }
1041 })
1042}
1043fn adjust_viewport_bound(transform: PxTransform, widget: &mut impl UiNode) -> PxTransform {
1044 let window_bounds = WINDOW.vars().actual_size_px().get();
1045 let wgt_bounds = PxBox::from(
1046 widget
1047 .with_context(WidgetUpdateMode::Ignore, || WIDGET.bounds().outer_size())
1048 .unwrap_or_else(PxSize::zero),
1049 );
1050 let wgt_bounds = transform.outer_transformed(wgt_bounds).unwrap_or_default();
1051
1052 let x_underflow = -wgt_bounds.min.x.min(Px(0));
1053 let x_overflow = (wgt_bounds.max.x - window_bounds.width).max(Px(0));
1054 let y_underflow = -wgt_bounds.min.y.min(Px(0));
1055 let y_overflow = (wgt_bounds.max.y - window_bounds.height).max(Px(0));
1056
1057 let x = x_underflow - x_overflow;
1058 let y = y_underflow - y_overflow;
1059
1060 let correction = PxVector::new(x, y);
1061
1062 transform.then_translate(correction.cast())
1063}