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