use crate::prelude::*; use iris_core::{PaintId, TextData}; use parley::{Affinity, Layout, Selection as ParleySelection}; use std::time::Instant; /// The selection state shared by display text and editable text. Editing, /// focus and IME state deliberately live in `TextEdit`; this owns only the /// state whose meaning comes from a shaped text layout. #[derive(Default)] pub(super) struct TextSelection { pub(super) range: Option, double_hit: Option, } impl TextSelection { pub(super) fn selected_text(&self, text: &str) -> Option { let selection = self.range?; if selection.is_collapsed() { return None; } Some(text[selection.text_range()].to_string()) } pub(super) fn range(&self) -> Option> { Some(self.range?.text_range()) } pub(super) fn caret(&self) -> Option { Some(self.range?.focus().index()) } pub(super) fn deselect(&mut self) { self.range = None; self.double_hit = None; } } /// Selection operations that need both a text widget's shaped buffer and /// iris's text resources. `TextEditCtx` delegates to this same context rather /// than maintaining an editable-only copy of the geometry and hit testing. pub struct TextSelectionCtx<'a> { pub(super) view: &'a mut TextView, pub(super) data: &'a mut TextData, } impl TextSelectionCtx<'_> { pub(super) fn layout(&mut self) -> &Layout { selection_layout(self.view, self.data) } pub(crate) fn refresh(&mut self) { if let Some(selection) = self.view.selection.range { let layout = self.layout(); self.view.selection.range = Some(selection.refresh(layout)); } } /// The byte offset in the text nearest `pos`. Positions and `size` use /// the same widget-local coordinates as a `CursorSense` event. pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize { let pos = pos - self.view.region().top_left().to_abs(size); let layout = self.layout(); ParleySelection::from_point(layout, pos.x, pos.y) .focus() .index() } pub fn select_all(&mut self) { let len = self.view.buf.text().len(); if len == 0 { return; } let layout = self.layout(); let anchor = parley::Cursor::from_byte_index(layout, 0, Affinity::default()); let focus = parley::Cursor::from_byte_index(layout, len, Affinity::default()); self.view.selection.range = Some(ParleySelection::new(anchor, focus)); } pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) { let pos = pos - self.view.region().top_left().to_abs(size); let previous = self.view.selection.range; let previous_hit = self.view.selection.double_hit; let outcome = { let layout = self.layout(); if drag { previous.map(|selection| { ( Some(selection.extend_to_point(layout, pos.x, pos.y)), previous_hit, ) }) } else { let hit = ParleySelection::from_point(layout, pos.x, pos.y); let index = hit.focus().index(); Some(if recent && previous_hit == Some(index) { ( Some(ParleySelection::line_from_point(layout, pos.x, pos.y)), None, ) } else if recent && previous.map(|selection| selection.focus().index()) == Some(index) { ( Some(ParleySelection::word_from_point(layout, pos.x, pos.y)), Some(index), ) } else { (Some(hit), None) }) } }; if let Some((range, double_hit)) = outcome { self.view.selection.range = range; self.view.selection.double_hit = double_hit; } } pub fn deselect(&mut self) { self.view.selection.deselect(); } pub(crate) fn set_caret(&mut self, index: usize) { let index = index.min(self.view.buf.text().len()); let layout = self.layout(); self.view.selection.range = Some(ParleySelection::from_byte_index( layout, index, Affinity::default(), )); } fn select_between(&mut self, anchor: usize, focus: usize) { let len = self.view.buf.text().len(); let layout = self.layout(); let anchor = parley::Cursor::from_byte_index(layout, anchor.min(len), Affinity::default()); let focus = parley::Cursor::from_byte_index(layout, focus.min(len), Affinity::default()); self.view.selection.range = Some(ParleySelection::new(anchor, focus)); } } pub(super) fn selection_layout<'a>( view: &'a mut TextView, data: &mut TextData, ) -> &'a Layout { let attrs = view.attrs.clone(); let width = view.wrap_width(); let density = data.density; view.buf.shape(data, &attrs, width, density); view.buf.layout() } /// Gives an ordinary `Text` handle access to the same selection operations as /// `TextEditCtx`. Gesture policy is intentionally not part of this trait; a /// selection controller and an editor's focus handler do different /// things with the same mechanics. pub trait TextSelectable { fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a>; } impl> TextSelectable for I { fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a> { let ui: &mut UiData = ui.ui_mut(); TextSelectionCtx { view: &mut ui.widgets.get_mut(self).unwrap().view, data: &mut ui.text, } } } /// Selection across the ordinary `Text` descendants of the widget this /// controller is attached to. The controller owns the cross-widget gesture /// and command state; each text leaf owns only its local Parley selection. pub struct SelectionController { anchor: Option<(WidgetId, usize)>, order: Vec, selected: Vec, gesture: DragGesture, scroll: Option>, separator: String, last_input: Option<(Instant, CursorSense, SelectionInput)>, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SelectionInput { Tapped, Handled, } impl Default for SelectionController { fn default() -> Self { Self::new() } } impl SelectionController { pub fn new() -> Self { Self { anchor: None, order: Vec::new(), selected: Vec::new(), gesture: DragGesture::new(), scroll: None, separator: String::new(), last_input: None, } } pub fn with_scroll(mut self, scroll: WeakWidget) -> Self { self.scroll = Some(scroll); self } pub fn separator(mut self, separator: impl Into) -> Self { self.separator = separator.into(); self } fn text_order(host: WidgetId, rsc: &impl UiRsc, render: &UiRenderState) -> Vec { fn visit(id: WidgetId, rsc: &impl UiRsc, render: &UiRenderState, out: &mut Vec) { if rsc .widgets() .get_dyn(id) .is_some_and(|widget| widget.as_any().is::()) { out.push(id); return; } // An editor owns its own focus, commands and selection gesture. if rsc .widgets() .get_dyn(id) .is_some_and(|widget| widget.as_any().is::()) { return; } for child in render.ordered_children(id, rsc) { visit(child, rsc, render, out); } } let mut out = Vec::new(); visit(host, rsc, render, &mut out); out } fn with_text( rsc: &mut impl UiRsc, id: WidgetId, f: impl FnOnce(&mut TextSelectionCtx<'_>) -> T, ) -> Option { let ui: &mut UiData = rsc.ui_mut(); let text = ui .widgets .get_dyn_mut(id)? .as_any_mut() .downcast_mut::()?; text.update_buf(); let mut ctx = TextSelectionCtx { view: &mut text.view, data: &mut ui.text, }; Some(f(&mut ctx)) } fn locate( &self, rsc: &impl UiRsc, render: &UiRenderState, pos: Vec2, ) -> Option<(WidgetId, Vec2, Vec2)> { self.order.iter().find_map(|&id| { let active = render.active.get(&id)?; let region = render.window_region(&id, rsc)?; (region.contains(pos) && render.mask_admits(active.mask, pos, rsc)).then(|| { ( id, pos - region.top_left, region.bot_right - region.top_left, ) }) }) } fn deselect(&mut self, rsc: &mut impl UiRsc) { let mut ids = std::mem::take(&mut self.selected); if let Some((anchor, _)) = self.anchor.take() && !ids.contains(&anchor) { ids.push(anchor); } for id in ids { Self::with_text(rsc, id, |text| text.deselect()); } } fn begin(&mut self, rsc: &mut impl UiRsc, id: WidgetId, pos: Vec2, size: Vec2) { self.deselect(rsc); let byte = Self::with_text(rsc, id, |text| { text.select(pos, size, false, false); text.byte_at(pos, size) }); self.anchor = byte.map(|byte| (id, byte)); } fn extend(&mut self, rsc: &mut impl UiRsc, id: WidgetId, pos: Vec2, size: Vec2) { let Some((anchor, anchor_byte)) = self.anchor else { return; }; let Some(anchor_at) = self.order.iter().position(|&candidate| candidate == anchor) else { self.deselect(rsc); return; }; let Some(focus_at) = self.order.iter().position(|&candidate| candidate == id) else { return; }; let Some(focus_byte) = Self::with_text(rsc, id, |text| text.byte_at(pos, size)) else { return; }; let (lo, hi) = if anchor_at <= focus_at { (anchor_at, focus_at) } else { (focus_at, anchor_at) }; let old = std::mem::take(&mut self.selected); for old_id in old { if !self.order[lo..=hi].contains(&old_id) { Self::with_text(rsc, old_id, |text| text.deselect()); } } for &text_id in &self.order[lo..=hi] { let forward = anchor_at <= focus_at; Self::with_text(rsc, text_id, |text| { let len = text.view.buf.text().len(); let (start, end) = if text_id == anchor && text_id == id { (anchor_byte, focus_byte) } else if text_id == anchor { (anchor_byte, if forward { len } else { 0 }) } else if text_id == id { (if forward { 0 } else { len }, focus_byte) } else { (0, len) }; text.select_between(start, end); }); } self.selected = self.order[lo..=hi].to_vec(); } pub fn drag( &mut self, id: ControllerId, rsc: &mut Rsc, input: &CursorData<'_>, ) -> SelectionInput { // A leaf listener and the controller host may both cover one point on // the same layer. They are two routes for one physical sample, not two // gestures; the second route must observe the first route's decision. if let Some((last, sense, outcome)) = self.last_input && last == input.cursor.time && sense == input.sense { return outcome; } self.order = Self::text_order(id.host(), rsc, input.render); let hit = self.locate(rsc, input.render, input.cursor.pos); let mut press = PressState::default(); if self.gesture.starts_press(input.sense) { press.scrolling = self.scroll.is_some_and(|scroll| scroll(rsc).is_scrolling()); if let Some(scroll) = self.scroll { scroll(rsc).cancel_fling(); } } press.already_selected = self.has_selection(rsc); let outcome = self.gesture.handle( input.pointer, id.host(), input.sense, input.cursor.pos, input.cursor.time, press, ); let input_result = match outcome { GestureOutcome::Pan(delta) => { if let Some(scroll) = self.scroll { scroll(rsc).scroll(delta); } SelectionInput::Handled } GestureOutcome::SelectStart => { if let Some((text, pos, size)) = hit { self.begin(rsc, text, pos, size); rsc.set_command_target(Some(id)); } SelectionInput::Handled } GestureOutcome::SelectExtend => { if let Some((text, pos, size)) = hit { self.extend(rsc, text, pos, size); } SelectionInput::Handled } GestureOutcome::Released(Some(velocity)) => { if let Some(scroll) = self.scroll && scroll(rsc).fling(velocity) { rsc.ui_mut().animate(scroll.id()); } SelectionInput::Handled } GestureOutcome::Tapped => { if self.anchor.is_some() || !self.selected.is_empty() { self.deselect(rsc); rsc.set_command_target(None); SelectionInput::Handled } else { SelectionInput::Tapped } } GestureOutcome::Cancelled | GestureOutcome::Undecided | GestureOutcome::Released(None) => SelectionInput::Handled, }; self.last_input = Some((input.cursor.time, input.sense, input_result)); input_result } pub fn has_selection(&self, rsc: &impl UiRsc) -> bool { self.selected.iter().any(|&id| { rsc.widgets() .get_dyn(id) .and_then(|widget| widget.as_any().downcast_ref::()) .is_some_and(|text| text.selected_text().is_some()) }) } pub fn selected_text(&self, rsc: &impl UiRsc) -> Option { let parts: Vec = self .selected .iter() .filter_map(|&id| { rsc.widgets() .get_dyn(id) .and_then(|widget| widget.as_any().downcast_ref::()) .and_then(Text::selected_text) }) .collect(); (!parts.is_empty()).then(|| parts.join(&self.separator)) } } impl Controller for SelectionController { fn command(&mut self, command: Command, rsc: &mut Rsc) -> CommandResult { match command { Command::Copy => self .selected_text(rsc) .map(CommandResult::Copy) .unwrap_or(CommandResult::Unused), Command::SelectAll => { let order = self.order.clone(); self.deselect(rsc); for &id in &order { Self::with_text(rsc, id, |text| text.select_all()); } self.selected = order; CommandResult::Used } Command::Escape => { self.deselect(rsc); CommandResult::Used } } } } #[cfg(test)] mod controller_tests { use super::*; struct TestRsc { ui: Ui, events: EventManager, } impl UiRsc for TestRsc { fn ui(&self) -> &Ui { &self.ui } fn ui_mut(&mut self) -> &mut Ui { &mut self.ui } fn on_draw(&mut self, active: &ActiveData) { self.events.draw(active); } fn on_undraw(&mut self, active: &ActiveData) { self.events.undraw(active); } fn on_remove(&mut self, id: WidgetId) { self.events.remove(id); } } impl HasState for TestRsc { type State = (); } impl HasEvents for TestRsc { fn events(&self) -> &EventManager { &self.events } fn events_mut(&mut self) -> &mut EventManager { &mut self.events } } fn two_texts( dir: Dir, ) -> ( TestRsc, UiRenderState, WeakWidget, WeakWidget, WeakWidget, StrongWidget, ) { let mut rsc = TestRsc { ui: Ui::default(), events: EventManager::default(), }; let first = wtext("first").add(&mut rsc); let second = wtext("second").add(&mut rsc); let host = (first, second) .span(dir) .controller(SelectionController::new().separator("|")) .add(&mut rsc); let root = host.upgrade(&mut rsc).any(); let mut render = UiRenderState::new(); render.resize((400.0, 200.0)); render.update(&root, &mut rsc); (rsc, render, host, first, second, root) } #[test] fn a_span_orders_selection_on_its_visual_axis() { let (mut rsc, render, host, _first, _second, _root) = two_texts(Dir::LEFT); let id = rsc .events() .controllers .id::(host.id()) .unwrap(); rsc.with_controller::(id, |selection, rsc| { selection.order = SelectionController::text_order(host.id(), rsc, &render); }); rsc.set_command_target(Some(id)); assert_eq!(rsc.run_command(Command::SelectAll), CommandResult::Used); assert_eq!( rsc.run_command(Command::Copy), CommandResult::Copy("second|first".to_string()) ); } #[test] fn a_widget_without_an_order_override_keeps_draw_order() { let mut rsc = TestRsc { ui: Ui::default(), events: EventManager::default(), }; let first = wtext("back").add(&mut rsc); let second = wtext("front").add(&mut rsc); let host = (first, second) .stack() .controller(SelectionController::new().separator("|")) .add(&mut rsc); let root = host.upgrade(&mut rsc).any(); let mut render = UiRenderState::new(); render.resize((400.0, 200.0)); render.update(&root, &mut rsc); let id = rsc .events() .controllers .id::(host.id()) .unwrap(); rsc.with_controller::(id, |selection, rsc| { selection.order = SelectionController::text_order(host.id(), rsc, &render); }); rsc.set_command_target(Some(id)); assert_eq!(rsc.run_command(Command::SelectAll), CommandResult::Used); assert_eq!( rsc.run_command(Command::Copy), CommandResult::Copy("back|front".to_string()) ); } #[test] fn nearest_controller_prefers_the_inner_scope() { let mut rsc = TestRsc { ui: Ui::default(), events: EventManager::default(), }; let leaf = wtext("leaf").add(&mut rsc); let inner = (leaf,) .span(Dir::DOWN) .controller(SelectionController::new()) .add(&mut rsc); let outer = (inner,) .span(Dir::DOWN) .controller(SelectionController::new()) .add(&mut rsc); let root = outer.upgrade(&mut rsc).any(); let mut render = UiRenderState::new(); render.resize((400.0, 200.0)); render.update(&root, &mut rsc); let found = rsc .events() .controllers .nearest_id::(leaf.id(), &render) .unwrap(); assert_eq!(found.host(), inner.id()); } #[test] fn command_target_outlives_pointer_release_and_copies_the_controller_selection() { let (mut rsc, render, host, first, second, _root) = two_texts(Dir::DOWN); let id = rsc .events() .controllers .id::(host.id()) .unwrap(); rsc.with_controller::(id, |selection, rsc| { selection.order = SelectionController::text_order(host.id(), rsc, &render); let first_size = render.window_region(&first, rsc).unwrap().size(); let second_size = render.window_region(&second, rsc).unwrap().size(); selection.begin(rsc, first.id(), Vec2::ZERO, first_size); selection.extend(rsc, second.id(), second_size, second_size); }); rsc.set_command_target(Some(id)); assert_eq!( rsc.run_command(Command::Copy), CommandResult::Copy("first|second".to_string()) ); } #[test] fn tapping_after_selection_deselects_and_releases_the_command_target() { let (mut rsc, render, host, _first, _second, _root) = two_texts(Dir::DOWN); let id = rsc .events() .controllers .id::(host.id()) .unwrap(); rsc.with_controller::(id, |selection, rsc| { selection.order = SelectionController::text_order(host.id(), rsc, &render); let order = selection.order.clone(); for &text in &order { SelectionController::with_text(rsc, text, |text| text.select_all()); } selection.selected = order; }); rsc.set_command_target(Some(id)); let pointer = PointerRequests::default(); let now = Instant::now(); rsc.with_controller::(id, |selection, rsc| { let press = CursorData { pos: Vec2::ZERO, size: Vec2::ZERO, scroll_delta: Vec2::ZERO, hover: Default::default(), cursor: CursorState { pos: Vec2::ZERO, time: now, ..Default::default() }, drag_axis: None, captured: false, sense: CursorSense::PressStart(CursorButton::Left), render: &render, pointer: &pointer, }; selection.drag(id, rsc, &press); let release = CursorData { pos: Vec2::ZERO, size: Vec2::ZERO, scroll_delta: Vec2::ZERO, hover: Default::default(), cursor: CursorState { pos: Vec2::ZERO, time: now + std::time::Duration::from_millis(20), ..Default::default() }, drag_axis: None, captured: false, sense: CursorSense::PressEnd(CursorButton::Left), render: &render, pointer: &pointer, }; assert_eq!(selection.drag(id, rsc, &release), SelectionInput::Handled); }); assert_eq!(rsc.events().controllers.command_target(), None); assert_eq!(rsc.run_command(Command::Copy), CommandResult::Unused); } #[test] fn removing_a_controller_host_clears_its_command_target() { let (mut rsc, mut render, host, _first, _second, root) = two_texts(Dir::DOWN); let id = rsc .events() .controllers .id::(host.id()) .unwrap(); rsc.set_command_target(Some(id)); drop(root); render.update(None, &mut rsc); rsc.free(); assert_eq!(rsc.events().controllers.command_target(), None); assert_eq!(rsc.run_command(Command::Copy), CommandResult::Unused); } #[test] fn removing_a_host_during_a_callback_does_not_restore_its_controller() { let (mut rsc, _render, host, _first, _second, _root) = two_texts(Dir::DOWN); let id = rsc .events() .controllers .id::(host.id()) .unwrap(); rsc.set_command_target(Some(id)); rsc.with_controller::(id, |_selection, rsc| { rsc.events_mut().controllers.remove(host.id()); }); assert!( rsc.events() .controllers .id::(host.id()) .is_none() ); assert_eq!(rsc.events().controllers.command_target(), None); } }