cranpose_ui/widgets/
selection_container.rs1use std::{cell::RefCell, rc::Rc};
5
6use cranpose_core::{
7 CompositionLocal, CompositionLocalProvider, DisposableEffect, remember, rememberMutableStateOf,
8 with_current_composer,
9};
10use cranpose_foundation::{PointerButton, modifier_element};
11
12use crate::{
13 PointerEventKind, PointerInputScope, composable,
14 modifier::{Brush, Modifier},
15 modifier_nodes::SelectableTextElement,
16 selection_container::{
17 SelectableGeometry, SelectionGesture, SelectionRegistrar, TextSelection,
18 },
19 text::{AnnotatedString, TextLayoutOptions, TextStyle},
20 widgets::{Box, BoxSpec, LiquidTextMenu, MenuAnchor, TextMenuItem},
21};
22
23pub fn local_selection_registrar() -> CompositionLocal<Option<SelectionRegistrar>> {
26 crate::environment_locals::cached_local(
27 |locals| &locals.selection_registrar,
28 || cranpose_core::compositionLocalOf(|| None),
29 )
30}
31
32#[composable]
56pub fn SelectionContainer(modifier: Modifier, content: impl FnMut() + 'static) {
57 let selection = rememberMutableStateOf(|| None::<TextSelection>);
58 let menu_open = rememberMutableStateOf(|| false);
59 let registrar = remember(move || SelectionRegistrar::new(selection)).with(Clone::clone);
60 let frame_clock = with_current_composer(|composer| composer.runtime_handle()).frame_clock();
61 let gesture = {
62 let registrar = registrar.clone();
63 remember(move || Rc::new(SelectionGesture::new(registrar, frame_clock, menu_open)))
64 .with(Rc::clone)
65 };
66 {
67 let gesture = Rc::clone(&gesture);
68 crate::UnhandledKeyEvents(move |event| gesture.on_key(event));
69 }
70 let content = Rc::new(RefCell::new(content));
71 let pointer = Rc::clone(&gesture);
72 CompositionLocalProvider(
73 [local_selection_registrar().provides(Some(registrar.clone()))],
74 move || {
75 let content = Rc::clone(&content);
76 Box(
77 modifier.pointer_input((), move |scope| {
78 select_with_pointer(scope, Rc::clone(&pointer))
79 }),
80 BoxSpec::default(),
81 move || (content.borrow_mut())(),
82 );
83 },
84 );
85 if menu_open.get() {
86 selection_menu(®istrar, &gesture);
87 }
88}
89
90async fn select_with_pointer(scope: PointerInputScope, gesture: Rc<SelectionGesture>) {
92 scope
93 .await_pointer_event_scope(|events| async move {
94 loop {
95 let event = events.await_pointer_event().await;
96 if event.is_consumed() {
97 continue;
98 }
99 match event.kind {
100 PointerEventKind::Down => {
101 let secondary_only = event.buttons.contains(PointerButton::Secondary)
102 && !event.buttons.contains(PointerButton::Primary);
103 if !secondary_only {
104 gesture.on_down(&event);
105 }
106 }
107 PointerEventKind::Move => {
108 if gesture.on_move(&event) {
109 event.consume();
110 }
111 }
112 PointerEventKind::Up => gesture.on_up(),
113 PointerEventKind::Cancel => gesture.on_cancel(),
114 _ => {}
115 }
116 }
117 })
118 .await;
119}
120
121fn selection_menu(registrar: &SelectionRegistrar, gesture: &Rc<SelectionGesture>) {
123 let Some(line) = registrar.first_highlight_in_window() else {
124 return;
125 };
126 let copy = Rc::clone(gesture);
127 let everything = registrar.clone();
128 let items = vec![
129 TextMenuItem::new(crate::UiString::Copy.resolve(), move || {
130 copy.copy();
131 copy.dismiss();
132 }),
133 TextMenuItem::new(crate::UiString::SelectAll.resolve(), move || {
134 everything.select_all();
135 }),
136 ];
137 let anchor = MenuAnchor {
138 center_x: line.x + line.width * 0.5,
139 line_top: line.y,
140 line_bottom: line.y + line.height,
141 };
142 LiquidTextMenu(anchor, true, None, items);
143}
144
145#[composable]
148pub fn DisableSelection(content: impl FnOnce()) {
149 CompositionLocalProvider([local_selection_registrar().provides(None)], content);
150}
151
152#[composable]
156pub(crate) fn selectable_text(
157 registrar: SelectionRegistrar,
158 text: Rc<AnnotatedString>,
159 style: TextStyle,
160 options: TextLayoutOptions,
161) -> Modifier {
162 let key = {
163 let registrar = registrar.clone();
164 remember(move || registrar.subscribe()).with(|key| *key)
165 };
166 let geometry = remember(|| Rc::new(SelectableGeometry::default())).with(Rc::clone);
167 registrar.update(key, text, style, options, Rc::clone(&geometry));
168 {
169 let registrar = registrar.clone();
170 DisposableEffect(key, move |scope| {
171 scope.on_dispose(move || registrar.unsubscribe(key))
172 });
173 }
174 let reporter = Modifier::from_parts(&[modifier_element(SelectableTextElement::new(
175 Rc::clone(&geometry),
176 ))]);
177 reporter.draw_behind(move |scope| {
178 geometry.set_content_size(scope.size());
179 for line in registrar.highlight(key) {
180 scope.draw_rect_at(
181 line,
182 Brush::solid(crate::text_field_modifier_node::DEFAULT_SELECTION_COLOR),
183 );
184 }
185 })
186}
187
188#[cfg(test)]
189#[path = "tests/selection_container_tests.rs"]
190mod tests;