Add retained paints and shared text selection
This commit is contained in:
1 parent
1e6d3b1edd
commit
a33fbca966
42 files changed
+2424
-470
No files matched your search
@@ -0,0 +1,756 @@
|
||||
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<ParleySelection>,
|
||||
double_hit: Option<usize>,
|
||||
}
|
||||
|
||||
impl TextSelection {
|
||||
pub(super) fn selected_text(&self, text: &str) -> Option<String> {
|
||||
let selection = self.range?;
|
||||
if selection.is_collapsed() {
|
||||
return None;
|
||||
}
|
||||
Some(text[selection.text_range()].to_string())
|
||||
}
|
||||
|
||||
pub(super) fn range(&self) -> Option<std::ops::Range<usize>> {
|
||||
Some(self.range?.text_range())
|
||||
}
|
||||
|
||||
pub(super) fn caret(&self) -> Option<usize> {
|
||||
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<PaintId> {
|
||||
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<PaintId> {
|
||||
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<I: IdLike<Widget = Text>> TextSelectable for I {
|
||||
fn selection<'a>(&self, ui: &'a mut impl UiRsc) -> TextSelectionCtx<'a> {
|
||||
let ui = 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<WidgetId>,
|
||||
selected: Vec<WidgetId>,
|
||||
gesture: DragGesture,
|
||||
scroll: Option<WeakWidget<LazySpan>>,
|
||||
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<LazySpan>) -> Self {
|
||||
self.scroll = Some(scroll);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn separator(mut self, separator: impl Into<String>) -> Self {
|
||||
self.separator = separator.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn text_order(host: WidgetId, rsc: &impl UiRsc, render: &UiRenderState) -> Vec<WidgetId> {
|
||||
fn visit(id: WidgetId, rsc: &impl UiRsc, render: &UiRenderState, out: &mut Vec<WidgetId>) {
|
||||
if rsc
|
||||
.widgets()
|
||||
.get_dyn(id)
|
||||
.is_some_and(|widget| widget.as_any().is::<Text>())
|
||||
{
|
||||
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::<TextEdit>())
|
||||
{
|
||||
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<T>(
|
||||
rsc: &mut impl UiRsc,
|
||||
id: WidgetId,
|
||||
f: impl FnOnce(&mut TextSelectionCtx<'_>) -> T,
|
||||
) -> Option<T> {
|
||||
let ui = rsc.ui_mut();
|
||||
let text = ui
|
||||
.widgets
|
||||
.get_dyn_mut(id)?
|
||||
.as_any_mut()
|
||||
.downcast_mut::<Text>()?;
|
||||
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<Rsc: HasEvents>(
|
||||
&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::<Text>())
|
||||
.is_some_and(|text| text.selected_text().is_some())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn selected_text(&self, rsc: &impl UiRsc) -> Option<String> {
|
||||
let parts: Vec<String> = self
|
||||
.selected
|
||||
.iter()
|
||||
.filter_map(|&id| {
|
||||
rsc.widgets()
|
||||
.get_dyn(id)
|
||||
.and_then(|widget| widget.as_any().downcast_ref::<Text>())
|
||||
.and_then(Text::selected_text)
|
||||
})
|
||||
.collect();
|
||||
(!parts.is_empty()).then(|| parts.join(&self.separator))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Rsc: HasEvents> Controller<Rsc> 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: UiData,
|
||||
events: EventManager<TestRsc>,
|
||||
}
|
||||
|
||||
impl UiRsc for TestRsc {
|
||||
fn ui(&self) -> &UiData {
|
||||
&self.ui
|
||||
}
|
||||
|
||||
fn ui_mut(&mut self) -> &mut UiData {
|
||||
&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> {
|
||||
&self.events
|
||||
}
|
||||
|
||||
fn events_mut(&mut self) -> &mut EventManager<Self> {
|
||||
&mut self.events
|
||||
}
|
||||
}
|
||||
|
||||
fn two_texts(
|
||||
dir: Dir,
|
||||
) -> (
|
||||
TestRsc,
|
||||
UiRenderState,
|
||||
WeakWidget<Span>,
|
||||
WeakWidget<Text>,
|
||||
WeakWidget<Text>,
|
||||
StrongWidget,
|
||||
) {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::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::<SelectionController>(host.id())
|
||||
.unwrap();
|
||||
rsc.with_controller::<SelectionController, _>(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: UiData::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::<SelectionController>(host.id())
|
||||
.unwrap();
|
||||
rsc.with_controller::<SelectionController, _>(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: UiData::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::<SelectionController>(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::<SelectionController>(host.id())
|
||||
.unwrap();
|
||||
rsc.with_controller::<SelectionController, _>(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::<SelectionController>(host.id())
|
||||
.unwrap();
|
||||
rsc.with_controller::<SelectionController, _>(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::<SelectionController, _>(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::<SelectionController>(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::<SelectionController>(host.id())
|
||||
.unwrap();
|
||||
rsc.set_command_target(Some(id));
|
||||
|
||||
rsc.with_controller::<SelectionController, _>(id, |_selection, rsc| {
|
||||
rsc.events_mut().controllers.remove(host.id());
|
||||
});
|
||||
|
||||
assert!(
|
||||
rsc.events()
|
||||
.controllers
|
||||
.id::<SelectionController>(host.id())
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(rsc.events().controllers.command_target(), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user