1use std::rc::Rc;
39
40use teksilo_canvas::{Canvas, Point, Rect, SizeProposal};
41
42use crate::accessibility::AccessNodeBuilder;
43use crate::binding::BindingLevel;
44use crate::build_context::BuildContext;
45use crate::event::{EventResponse, WidgetEvent};
46use crate::overlay::SelectionHandleKind;
47use crate::styles::{TextMagnifierRecipe, TextSelectionHandleRecipe};
48use crate::widget::{
49 EventContext, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
50};
51use crate::widget_builder::WidgetBuilder;
52use crate::widget_id::WidgetId;
53
54use super::{HandleDragPhase, SelectionHandleGeometry, TextAffordances};
55
56pub trait TextAffordanceDelegate {
64 fn handle_drag(
66 &self,
67 kind: SelectionHandleKind,
68 phase: HandleDragPhase,
69 point: Point,
70 ctx: &mut EventContext<'_>,
71 );
72
73 fn set_handle_offset(
76 &self,
77 kind: SelectionHandleKind,
78 offset: usize,
79 ctx: &mut EventContext<'_>,
80 );
81}
82
83pub struct SelectionHandle {
85 kind: SelectionHandleKind,
86 affordances: TextAffordances,
87 recipe: TextSelectionHandleRecipe,
88 delegate: Rc<dyn TextAffordanceDelegate>,
89}
90
91impl std::fmt::Debug for SelectionHandle {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("SelectionHandle")
94 .field("kind", &self.kind)
95 .finish()
96 }
97}
98
99impl SelectionHandle {
100 pub fn new(
101 kind: SelectionHandleKind,
102 affordances: TextAffordances,
103 recipe: TextSelectionHandleRecipe,
104 delegate: Rc<dyn TextAffordanceDelegate>,
105 ) -> Self {
106 Self {
107 kind,
108 affordances,
109 recipe,
110 delegate,
111 }
112 }
113
114 fn geometry(&self) -> Option<SelectionHandleGeometry> {
115 self.affordances.handle(self.kind)
116 }
117
118 fn default_label(&self) -> &'static str {
122 match self.kind {
123 SelectionHandleKind::Caret => "Text cursor",
124 SelectionHandleKind::Start => "Selection start",
125 SelectionHandleKind::End => "Selection end",
126 }
127 }
128}
129
130impl Widget for SelectionHandle {
131 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
132 let kind = self.kind;
133 let drag_delegate = Rc::clone(&self.delegate);
134 let action_delegate = Rc::clone(&self.delegate);
135 let handlers = crate::widget_builder::HandlerSet::new()
136 .on_pointer_event(move |event, ctx| {
137 if !ctx.pointer_kind().is_direct() {
151 return EventResponse::Ignored;
152 }
153 match event {
154 WidgetEvent::PointerDown { position, .. } => {
155 ctx.capture_pointer();
156 drag_delegate.handle_drag(kind, HandleDragPhase::Begin, *position, ctx);
157 EventResponse::Handled
158 }
159 WidgetEvent::PointerMove { position, .. } => {
160 drag_delegate.handle_drag(kind, HandleDragPhase::Move, *position, ctx);
161 EventResponse::Handled
162 }
163 WidgetEvent::PointerUp { position, .. } => {
164 drag_delegate.handle_drag(kind, HandleDragPhase::End, *position, ctx);
165 EventResponse::Handled
166 }
167 WidgetEvent::PointerCancel { .. } => {
168 drag_delegate.handle_drag(
179 kind,
180 HandleDragPhase::Cancel,
181 Point::new(0.0, 0.0),
182 ctx,
183 );
184 EventResponse::Handled
185 }
186 _ => EventResponse::Ignored,
187 }
188 })
189 .on_access_action_request(move |action, _node, data, ctx| {
190 if action != accesskit::Action::SetValue {
191 return EventResponse::Ignored;
192 }
193 let offset = match data {
194 Some(accesskit::ActionData::NumericValue(v)) => v.max(0.0) as usize,
195 Some(accesskit::ActionData::Value(v)) => match v.parse::<usize>() {
196 Ok(parsed) => parsed,
197 Err(_) => return EventResponse::Ignored,
198 },
199 _ => return EventResponse::Ignored,
200 };
201 action_delegate.set_handle_offset(kind, offset, ctx);
202 EventResponse::Handled
203 });
204 ctx.apply_self_handlers(handlers);
205 vec![]
206 }
207
208 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
209 let extent = self.recipe.hit_size;
210 proposal.resolve(extent, extent).into()
211 }
212
213 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
214 let Some(geometry) = self.geometry() else {
215 return;
216 };
217 let fill = self.recipe.fill.resolve(ctx.theme);
218 let radius = self.recipe.diameter / 2.0;
219 let centre = Point::new(
223 geometry
224 .anchor
225 .x
226 .clamp(bounds.x + radius, bounds.right() - radius),
227 geometry
228 .anchor
229 .y
230 .clamp(bounds.y + radius, bounds.bottom() - radius),
231 );
232 if self.recipe.stem_width > 0.0 {
233 let stem_x = centre.x - self.recipe.stem_width / 2.0;
234 let caret = geometry.caret;
235 let (top, bottom) = if centre.y < caret.y {
236 (centre.y, caret.y)
237 } else {
238 (caret.bottom(), centre.y)
239 };
240 if bottom > top {
241 canvas.fill_rect(
242 Rect::new(stem_x, top, self.recipe.stem_width, bottom - top),
243 fill,
244 );
245 }
246 }
247 if self.recipe.outline_width > 0.0 {
248 canvas.stroke_circle(
249 centre,
250 radius,
251 self.recipe.outline.resolve(ctx.theme),
252 self.recipe.outline_width,
253 );
254 }
255 canvas.fill_circle(centre, radius, fill);
256 }
257
258 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
259 builder.set_role(accesskit::Role::Slider);
260 builder.set_name(self.default_label());
261 if let Some(geometry) = self.geometry() {
262 builder.set_numeric_value(geometry.offset as f64);
263 builder.set_min_numeric_value(0.0);
264 builder.set_max_numeric_value(geometry.document_len as f64);
265 builder.set_numeric_value_step(1.0);
266 }
267 builder.add_action(accesskit::Action::SetValue);
272 }
273}
274
275pub struct TextMagnifier {
278 affordances: TextAffordances,
279 recipe: TextMagnifierRecipe,
280 painter: Rc<dyn Fn(&mut Canvas, &PaintContext<'_>)>,
281}
282
283impl std::fmt::Debug for TextMagnifier {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 f.debug_struct("TextMagnifier").finish()
286 }
287}
288
289impl TextMagnifier {
290 pub fn new(
295 affordances: TextAffordances,
296 recipe: TextMagnifierRecipe,
297 painter: Rc<dyn Fn(&mut Canvas, &PaintContext<'_>)>,
298 ) -> Self {
299 Self {
300 affordances,
301 recipe,
302 painter,
303 }
304 }
305}
306
307impl Widget for TextMagnifier {
308 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
309 proposal
310 .resolve(self.recipe.radius * 2.0, self.recipe.half_height * 2.0)
311 .into()
312 }
313
314 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
315 let Some(request) = self.affordances.magnifier() else {
316 return;
317 };
318 let corner = self.recipe.corner_radius;
319 canvas.fill_rounded_rect(
320 bounds,
321 teksilo_tokens::CornerRadius::uniform(corner),
322 self.recipe.background.resolve(ctx.theme),
323 );
324 ctx.replay(canvas, &*self.painter, request.transform(), bounds);
332 if self.recipe.border_width > 0.0 {
333 canvas.stroke_rounded_rect(
334 bounds,
335 teksilo_tokens::CornerRadius::uniform(corner),
336 self.recipe.border.resolve(ctx.theme),
337 self.recipe.border_width,
338 );
339 }
340 }
341
342 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
343 builder.set_hidden();
346 }
347}
348
349pub struct TextAffordanceLayer {
356 affordances: TextAffordances,
357 handle_recipe: TextSelectionHandleRecipe,
358 magnifier_recipe: TextMagnifierRecipe,
359 delegate: Rc<dyn TextAffordanceDelegate>,
360 painter: Option<Rc<dyn Fn(&mut Canvas, &PaintContext<'_>)>>,
361 handles: Vec<(SelectionHandleKind, WidgetId)>,
362 magnifier: Option<WidgetId>,
363}
364
365impl std::fmt::Debug for TextAffordanceLayer {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 f.debug_struct("TextAffordanceLayer")
368 .field("handles", &self.handles.len())
369 .field("magnifier", &self.magnifier.is_some())
370 .finish()
371 }
372}
373
374impl TextAffordanceLayer {
375 pub fn new(
376 affordances: TextAffordances,
377 handle_recipe: TextSelectionHandleRecipe,
378 magnifier_recipe: TextMagnifierRecipe,
379 delegate: Rc<dyn TextAffordanceDelegate>,
380 ) -> Self {
381 Self {
382 affordances,
383 handle_recipe,
384 magnifier_recipe,
385 delegate,
386 painter: None,
387 handles: Vec::new(),
388 magnifier: None,
389 }
390 }
391
392 pub fn magnifier_painter(
396 mut self,
397 painter: Rc<dyn Fn(&mut Canvas, &PaintContext<'_>)>,
398 ) -> Self {
399 self.painter = Some(painter);
400 self
401 }
402}
403
404impl Widget for TextAffordanceLayer {
405 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
406 self.affordances.version_signal().bind_to(
410 ctx.self_id(),
411 ctx.binding_registry(),
412 BindingLevel::Relayout,
413 );
414
415 self.handles.clear();
416 for kind in [
417 SelectionHandleKind::Caret,
418 SelectionHandleKind::Start,
419 SelectionHandleKind::End,
420 ] {
421 let id = ctx.add(
422 SelectionHandle::new(
423 kind,
424 self.affordances.clone(),
425 self.handle_recipe,
426 Rc::clone(&self.delegate),
427 )
428 .visible_when(self.affordances.handle_visible_signal(kind)),
429 );
430 self.handles.push((kind, id));
431 }
432 self.magnifier = self.painter.as_ref().map(|painter| {
433 ctx.add(
434 TextMagnifier::new(
435 self.affordances.clone(),
436 self.magnifier_recipe,
437 Rc::clone(painter),
438 )
439 .visible_when(self.affordances.magnifier_visible_signal()),
440 )
441 });
442
443 let handlers = crate::widget_builder::HandlerSet::new().event_pass_through(true);
444 ctx.apply_self_handlers(handlers);
445
446 self.children()
447 }
448
449 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
450 proposal.resolve(0.0, 0.0).into()
453 }
454
455 fn place_children(
456 &self,
457 _bounds: Rect,
458 _proposal: SizeProposal,
459 children: &mut [WidgetPlacement],
460 _ctx: &LayoutContext,
461 ) {
462 for placement in children.iter_mut() {
463 if let Some((kind, _)) = self.handles.iter().find(|(_, id)| *id == placement.id) {
464 if let Some(geometry) = self.affordances.handle(*kind) {
465 placement.origin = geometry.hit.origin();
466 placement.size = geometry.hit.size();
467 }
468 } else if Some(placement.id) == self.magnifier
469 && let Some(request) = self.affordances.magnifier()
470 {
471 placement.origin = request.lens.origin();
472 placement.size = request.lens.size();
473 }
474 }
475 }
476
477 fn children(&self) -> Vec<WidgetId> {
478 self.handles
479 .iter()
480 .map(|(_, id)| *id)
481 .chain(self.magnifier)
482 .collect()
483 }
484
485 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
486 builder.set_role(accesskit::Role::GenericContainer);
489 }
490}