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
+3
-3
@@ -6,7 +6,7 @@ fn a_named_widget_reaches_the_tree_with_its_role_and_bounds() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let leaf: WeakWidget<Rect> = rect(UiColor::WHITE).label("Add task").add(&mut rsc);
|
||||
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("Add task").add(&mut rsc);
|
||||
let root = leaf.upgrade(&mut rsc).any();
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((800.0, 600.0));
|
||||
@@ -40,7 +40,7 @@ fn a_widget_with_no_label_never_reaches_the_tree() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let root = rsc.ui.widgets.add_strong(rect(UiColor::WHITE));
|
||||
let root = rsc.ui.widgets.add_strong(rect(PaintId::WHITE));
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((800.0, 600.0));
|
||||
render.update(&root.any(), &mut rsc);
|
||||
@@ -58,7 +58,7 @@ fn bounds_follow_a_moved_widget_and_updates_stay_incremental() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let leaf: WeakWidget<Rect> = rect(UiColor::WHITE).label("thing").add(&mut rsc);
|
||||
let leaf: WeakWidget<Rect> = rect(PaintId::WHITE).label("thing").add(&mut rsc);
|
||||
let leaf_strong = leaf.upgrade(&mut rsc).any();
|
||||
let offset = rsc.ui.widgets.add_strong(Offset {
|
||||
inner: leaf_strong,
|
||||
|
||||
+29
-17
@@ -4,7 +4,7 @@ use android_view::{
|
||||
jni::{JavaVM, objects::GlobalRef},
|
||||
ndk::native_window::NativeWindow,
|
||||
};
|
||||
use iris_core::{FrameParts, UiData, UiRenderNode, UiRenderState};
|
||||
use iris_core::{FrameParts, LinearRgba, UiData, UiRenderNode, UiRenderState};
|
||||
use pollster::FutureExt;
|
||||
use std::time::Instant;
|
||||
use wgpu::{
|
||||
@@ -12,7 +12,7 @@ use wgpu::{
|
||||
*,
|
||||
};
|
||||
|
||||
pub const CLEAR_COLOR: Color = Color::BLACK;
|
||||
pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK;
|
||||
|
||||
/// `NativeWindow` (from the surface android-view hands over in
|
||||
/// `surfaceChanged`) has a window handle but not a display one -- there is
|
||||
@@ -46,6 +46,7 @@ pub struct AndroidRenderer {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
config: SurfaceConfiguration,
|
||||
view_format: TextureFormat,
|
||||
encoder: CommandEncoder,
|
||||
pub ui: UiRenderNode,
|
||||
pub adapter_name: String,
|
||||
@@ -73,6 +74,7 @@ pub struct AndroidRenderer {
|
||||
pub struct FrameDiagnostics {
|
||||
pub masks_resized: bool,
|
||||
pub moves_resized: bool,
|
||||
pub paints_resized: bool,
|
||||
pub atlas_pages_grown_prev: u64,
|
||||
pub image_bind_group_creates_prev: u64,
|
||||
}
|
||||
@@ -189,30 +191,33 @@ impl AndroidRenderer {
|
||||
);
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let surface_format = surface_caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(surface_caps.formats[0]);
|
||||
let formats = iris_core::srgb_surface_format(&surface_caps)?;
|
||||
log::info!(
|
||||
"iris renderer: surface={:?}, view={:?}, color_space=Srgb",
|
||||
formats.surface,
|
||||
formats.view,
|
||||
);
|
||||
|
||||
let config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
format: formats.surface,
|
||||
// wgpu 30's new field; `Auto` is what every earlier version did.
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
color_space: SurfaceColorSpace::Srgb,
|
||||
width,
|
||||
height,
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: vec![],
|
||||
view_formats: (formats.view != formats.surface)
|
||||
.then_some(formats.view)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
surface.configure(&device, &config);
|
||||
|
||||
let encoder = Self::create_encoder(&device);
|
||||
let window_size = iris_core::util::Vec2::new(width as f32, height as f32);
|
||||
let ui = match UiRenderNode::new(&device, &queue, &config, window_size) {
|
||||
let ui = match UiRenderNode::new(&device, &queue, formats.view, window_size) {
|
||||
Ok(ui) => ui,
|
||||
Err(wgpu_error) => return Err(Self::diagnostic(&adapter, &wgpu_error)),
|
||||
};
|
||||
@@ -222,6 +227,7 @@ impl AndroidRenderer {
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
view_format: formats.view,
|
||||
encoder,
|
||||
ui,
|
||||
adapter_name,
|
||||
@@ -289,8 +295,10 @@ impl AndroidRenderer {
|
||||
format!(
|
||||
"iris diagnostics. Copy this text and send it to Iris.\n\n\
|
||||
adapter: {name} ({backend:?}), driver: {driver}\n\
|
||||
surface: {surface:?}, view: {view:?}, color_space: Srgb\n\
|
||||
content_scale: {content_scale}\n\
|
||||
atlas format: Rgba8Unorm, views live: {views}\n\
|
||||
paint format: linear vec4<f32>\n\
|
||||
atlas/image format: Rgba8UnormSrgb, views live: {views}\n\
|
||||
fonts: {families_found} families found, default={default_family:?} \
|
||||
mono={default_mono_family:?}\n\
|
||||
fonts resolved: regular={regular:?} bold={bold:?} italic={italic:?} \
|
||||
@@ -301,6 +309,8 @@ impl AndroidRenderer {
|
||||
name = self.adapter_name,
|
||||
backend = self.adapter_backend,
|
||||
driver = self.adapter_driver,
|
||||
surface = self.config.format,
|
||||
view = self.view_format,
|
||||
content_scale = self.content_scale,
|
||||
views = self.ui.view_count(),
|
||||
families_found = font.families_found,
|
||||
@@ -328,6 +338,7 @@ impl AndroidRenderer {
|
||||
FrameDiagnostics {
|
||||
masks_resized: stats.masks_resized,
|
||||
moves_resized: stats.moves_resized,
|
||||
paints_resized: stats.paints_resized,
|
||||
atlas_pages_grown_prev,
|
||||
image_bind_group_creates_prev,
|
||||
}
|
||||
@@ -348,9 +359,10 @@ impl AndroidRenderer {
|
||||
other => panic!("no surface texture to draw into: {other:?}"),
|
||||
};
|
||||
let acquire = acquire_start.elapsed();
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
let view = output.texture.create_view(&TextureViewDescriptor {
|
||||
format: Some(self.view_format),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||
{
|
||||
@@ -359,7 +371,7 @@ impl AndroidRenderer {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(CLEAR_COLOR),
|
||||
load: LoadOp::Clear(CLEAR_COLOR.to_wgpu()),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
|
||||
@@ -383,10 +383,12 @@ impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||
if renderer.frame_count() <= DIAGNOSTIC_FRAMES {
|
||||
log::info!(
|
||||
"iris frame diagnostics: frame={} masks_resized={} moves_resized={} \
|
||||
paints_resized={} \
|
||||
atlas_pages_grown_prev={} image_bind_group_creates_prev={} wgpu_errors={}",
|
||||
renderer.frame_count(),
|
||||
frame_diagnostics.masks_resized,
|
||||
frame_diagnostics.moves_resized,
|
||||
frame_diagnostics.paints_resized,
|
||||
frame_diagnostics.atlas_pages_grown_prev,
|
||||
frame_diagnostics.image_bind_group_creates_prev,
|
||||
renderer.wgpu_errors.snapshot().len(),
|
||||
@@ -698,6 +700,7 @@ impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||
self.rsc.ui.text.atlas.page_count(),
|
||||
);
|
||||
self.rsc.ui.textures.reupload();
|
||||
self.rsc.ui.paints.reupload();
|
||||
self.state.android_state_mut().renderer = Some(renderer);
|
||||
self.render(ctx, Instant::now());
|
||||
}
|
||||
|
||||
+7
-1
@@ -99,7 +99,7 @@ where
|
||||
/// whatever is behind the field (a list to pan) still sees every frame of
|
||||
/// it, the same as a drag that never touched a selectable field at all.
|
||||
fn on_press(
|
||||
rsc: &mut impl UiRsc,
|
||||
rsc: &mut impl HasEvents,
|
||||
render: &UiRenderState,
|
||||
state: &mut impl FocusHost,
|
||||
id: WeakWidget<TextEdit>,
|
||||
@@ -107,6 +107,12 @@ fn on_press(
|
||||
size: Vec2,
|
||||
sense: CursorSense,
|
||||
) {
|
||||
if sense == CursorSense::PressStart(CursorButton::Left) {
|
||||
// An editable field becomes the command destination of the new
|
||||
// interaction. Dismiss a retained display-text selection first so
|
||||
// Copy cannot keep going to text the user has visibly left behind.
|
||||
rsc.run_command(Command::Escape);
|
||||
}
|
||||
if state.is_focused(id) {
|
||||
// Already focused, so there is no keyboard to withhold -- but a
|
||||
// vertical drag still is not a selection. Android's own `EditText`
|
||||
|
||||
+28
-1
@@ -344,7 +344,34 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
||||
ui_state.window.request_redraw();
|
||||
}
|
||||
WindowEvent::KeyboardInput { event, .. } => {
|
||||
if let Some(sel) = ui_state.focus
|
||||
let requested = event.state.is_pressed().then(|| match &event.logical_key {
|
||||
winit::keyboard::Key::Character(c) if ui_state.input.modifiers.control => {
|
||||
match c.as_str().to_ascii_lowercase().as_str() {
|
||||
"c" => Some(Command::Copy),
|
||||
"a" => Some(Command::SelectAll),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
winit::keyboard::Key::Named(winit::keyboard::NamedKey::Escape) => {
|
||||
Some(Command::Escape)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
let command = requested
|
||||
.flatten()
|
||||
.map_or(CommandResult::Unused, |command| rsc.run_command(command));
|
||||
let command_used = match command {
|
||||
CommandResult::Copy(text) => {
|
||||
if let Err(err) = ui_state.clipboard.set_text(text) {
|
||||
eprintln!("failed to copy text to clipboard: {err}")
|
||||
}
|
||||
true
|
||||
}
|
||||
CommandResult::Used => true,
|
||||
CommandResult::Unused => false,
|
||||
};
|
||||
if !command_used
|
||||
&& let Some(sel) = ui_state.focus
|
||||
&& event.state.is_pressed()
|
||||
{
|
||||
let mut text = sel.edit(rsc);
|
||||
|
||||
+23
-16
@@ -1,12 +1,12 @@
|
||||
use crate::task::RequestRedraw;
|
||||
use iris_core::{FrameParts, UiData, UiRenderNode, UiRenderState, util::Vec2};
|
||||
use iris_core::{FrameParts, LinearRgba, UiData, UiRenderNode, UiRenderState, util::Vec2};
|
||||
use pollster::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use wgpu::*;
|
||||
use winit::{dpi::PhysicalSize, window::Window};
|
||||
|
||||
pub const CLEAR_COLOR: Color = Color::BLACK;
|
||||
pub const CLEAR_COLOR: LinearRgba = LinearRgba::BLACK;
|
||||
|
||||
impl RequestRedraw for Window {
|
||||
fn request_redraw(&self) {
|
||||
@@ -20,6 +20,7 @@ pub struct UiRenderer {
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
config: SurfaceConfiguration,
|
||||
view_format: TextureFormat,
|
||||
encoder: CommandEncoder,
|
||||
pub ui: UiRenderNode,
|
||||
}
|
||||
@@ -45,9 +46,10 @@ impl UiRenderer {
|
||||
other => panic!("no surface texture to draw into: {other:?}"),
|
||||
};
|
||||
let acquire = acquire_start.elapsed();
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
let view = output.texture.create_view(&TextureViewDescriptor {
|
||||
format: Some(self.view_format),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||
{
|
||||
@@ -56,7 +58,7 @@ impl UiRenderer {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: Operations {
|
||||
load: LoadOp::Clear(CLEAR_COLOR),
|
||||
load: LoadOp::Clear(CLEAR_COLOR.to_wgpu()),
|
||||
store: StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
@@ -170,18 +172,19 @@ impl UiRenderer {
|
||||
.expect("Could not get device!");
|
||||
|
||||
let surface_caps = surface.get_capabilities(&adapter);
|
||||
let surface_format = surface_caps
|
||||
.formats
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|f| f.is_srgb())
|
||||
.unwrap_or(surface_caps.formats[0]);
|
||||
let formats = iris_core::srgb_surface_format(&surface_caps)
|
||||
.expect("Could not select an sRGB iris surface format");
|
||||
log::info!(
|
||||
"iris renderer: surface={:?}, view={:?}, color_space=Srgb",
|
||||
formats.surface,
|
||||
formats.view,
|
||||
);
|
||||
|
||||
let config = SurfaceConfiguration {
|
||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||
format: surface_format,
|
||||
format: formats.surface,
|
||||
// wgpu 30's new field; `Auto` is what every earlier version did.
|
||||
color_space: SurfaceColorSpace::Auto,
|
||||
color_space: SurfaceColorSpace::Srgb,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
// Vsync, because a toolkit aiming at battery life must not present
|
||||
@@ -192,7 +195,10 @@ impl UiRenderer {
|
||||
present_mode: PresentMode::AutoVsync,
|
||||
alpha_mode: surface_caps.alpha_modes[0],
|
||||
desired_maximum_frame_latency: 2,
|
||||
view_formats: vec![],
|
||||
view_formats: (formats.view != formats.surface)
|
||||
.then_some(formats.view)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
};
|
||||
|
||||
surface.configure(&device, &config);
|
||||
@@ -210,7 +216,7 @@ impl UiRenderer {
|
||||
// `default::content_scale` for why this backend stopped dividing
|
||||
// into a separate logical space, and what disagreed while it did.
|
||||
let physical_size = Vec2::new(size.width as f32, size.height as f32);
|
||||
let ui = UiRenderNode::new(&device, &queue, &config, physical_size)
|
||||
let ui = UiRenderNode::new(&device, &queue, formats.view, physical_size)
|
||||
.expect("Could not create iris render node!");
|
||||
|
||||
Self {
|
||||
@@ -218,6 +224,7 @@ impl UiRenderer {
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
view_format: formats.view,
|
||||
encoder,
|
||||
ui,
|
||||
window,
|
||||
|
||||
@@ -36,6 +36,17 @@ pub trait Eventable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> {
|
||||
}
|
||||
impl<WL: WidgetLike<Rsc, Tag>, Rsc: HasEvents, Tag> Eventable<Rsc, Tag> for WL {}
|
||||
|
||||
pub trait Controllable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> {
|
||||
fn controller<C: Controller<Rsc>>(self, controller: C) -> impl WidgetIdFn<Rsc, Self::Widget> {
|
||||
move |rsc| {
|
||||
let id = self.add(rsc);
|
||||
rsc.register_controller(id, controller);
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<WL: WidgetLike<Rsc, Tag>, Rsc: HasEvents, Tag> Controllable<Rsc, Tag> for WL {}
|
||||
|
||||
widget_trait! {
|
||||
pub trait TaskEventable<Rsc: HasEvents + HasTasks>;
|
||||
fn task_on<E: EventLike, F: AsyncWidgetEventFn<Rsc, WL::Widget>>(
|
||||
|
||||
+28
-27
@@ -19,8 +19,9 @@ struct FixedRect(f32);
|
||||
impl Widget for FixedRect {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let size = Size::from_axis(Axis::Y, Len::abs(self.0), Len::REST);
|
||||
let paint = painter.paint(&PaintId::WHITE);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(UiColor::WHITE),
|
||||
RectPrimitive::color(paint),
|
||||
size.to_uivec2(painter.density())
|
||||
.align(RegionAlign::TOP_LEFT),
|
||||
);
|
||||
@@ -89,8 +90,8 @@ fn a_widget_retains_its_entry_layer_not_its_child_cursor() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let back = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let front = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED));
|
||||
let back = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let front = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED));
|
||||
let stack = rsc.ui.widgets.add_strong(Stack {
|
||||
children: vec![back.any(), front.any()],
|
||||
size: StackSize::Default,
|
||||
@@ -277,7 +278,7 @@ fn a_hinted_rest_draws_once_and_only_moves_the_fixed_child_after_it() {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let first = rsc.ui.widgets.add_strong(FixedRect(40.0));
|
||||
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let fill = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: fill.any(),
|
||||
x: None,
|
||||
@@ -309,7 +310,7 @@ fn scrolled_rects(
|
||||
let mut span = Span::empty(Dir::DOWN);
|
||||
let mut rects = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
rects.push(rect.weak());
|
||||
let row = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: rect.any(),
|
||||
@@ -459,13 +460,13 @@ fn composer_like_tree(rsc: &mut TestRsc) -> (WeakWidget<TextEdit>, StrongWidget)
|
||||
.text_align(Align::LEFT)
|
||||
.wrap(true)
|
||||
.size(18)
|
||||
.color(UiColor::WHITE)
|
||||
.color(PaintId::WHITE)
|
||||
.add(rsc);
|
||||
let bar = (field.pad(dp(12)).width(rest(1)),)
|
||||
.span(Dir::RIGHT)
|
||||
.background(rect(UiColor::new(40, 40, 46, 255)))
|
||||
.background(rect(Srgba8::new(40, 40, 46, 255)))
|
||||
.add(rsc);
|
||||
let list_stand_in = rect(UiColor::BLACK).height(rest(1)).add(rsc);
|
||||
let list_stand_in = rect(PaintId::BLACK).height(rest(1)).add(rsc);
|
||||
let tree = (list_stand_in, bar).span(Dir::DOWN).add_strong(rsc).any();
|
||||
(field, tree)
|
||||
}
|
||||
@@ -517,7 +518,7 @@ fn a_scroll_measures_the_box_it_was_offered_not_the_window() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: rect.any(),
|
||||
x: None,
|
||||
@@ -567,7 +568,7 @@ fn a_panned_widgets_own_hit_box_moves_exactly_once() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: rect.any(),
|
||||
x: None,
|
||||
@@ -609,7 +610,7 @@ fn a_masked_widget_keeps_one_mask_slot_that_is_always_its_own_region() {
|
||||
inner: inner_root,
|
||||
});
|
||||
let masked_id = masked.id();
|
||||
let filler = rsc.ui.widgets.add_strong(Rect::new(UiColor::BLACK));
|
||||
let filler = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLACK));
|
||||
let filler = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: filler.any(),
|
||||
x: None,
|
||||
@@ -650,7 +651,7 @@ fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: rect.any(),
|
||||
x: None,
|
||||
@@ -662,7 +663,7 @@ fn a_dp_cap_is_reported_in_pixels_so_a_span_can_place_it() {
|
||||
y: Some(Len::dp(100.0)),
|
||||
});
|
||||
let capped_w = capped.weak();
|
||||
let filler = rsc.ui.widgets.add_strong(Rect::new(UiColor::BLACK));
|
||||
let filler = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLACK));
|
||||
let filler = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: filler.any(),
|
||||
x: None,
|
||||
@@ -692,14 +693,14 @@ fn a_size_independent_widget_moved_by_its_parent_has_the_hit_box_it_is_drawn_at(
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let top = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let top = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let spacer = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: top.any(),
|
||||
x: None,
|
||||
y: Some(Len::abs(100.0)),
|
||||
});
|
||||
let spacer_w = spacer.weak();
|
||||
let below = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let below = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let below_w = below.weak();
|
||||
let mut span = Span::empty(Dir::DOWN);
|
||||
span.push(spacer.any());
|
||||
@@ -759,7 +760,7 @@ fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let child = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: rect.any(),
|
||||
x: None,
|
||||
@@ -800,12 +801,12 @@ fn a_widget_moved_by_its_parent_and_then_placed_inside_it_lands_at_the_placement
|
||||
const RADIUS: f32 = 20.0;
|
||||
|
||||
fn rounded_container(rsc: &mut TestRsc) -> (UiRenderState, MaskIdx, WidgetId, u32) {
|
||||
let child = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let child = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let child_id = child.id();
|
||||
let shape = rsc
|
||||
.ui
|
||||
.widgets
|
||||
.add_strong(Rect::new(UiColor::BLACK).radius(Len::abs(RADIUS)));
|
||||
.add_strong(Rect::new(PaintId::BLACK).radius(Len::abs(RADIUS)));
|
||||
let shape_id = shape.id();
|
||||
let root = rsc
|
||||
.ui
|
||||
@@ -909,13 +910,13 @@ fn nested_masks_multiply_their_coverage() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let child = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let child = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let child_id = child.id();
|
||||
|
||||
let inner_shape = rsc
|
||||
.ui
|
||||
.widgets
|
||||
.add_strong(Rect::new(UiColor::BLACK).radius(Len::abs(RADIUS)));
|
||||
.add_strong(Rect::new(PaintId::BLACK).radius(Len::abs(RADIUS)));
|
||||
let inner_shape_id = inner_shape.id();
|
||||
let inner = rsc.ui.widgets.add_strong(Masked {
|
||||
shape: Some(inner_shape.any()),
|
||||
@@ -924,7 +925,7 @@ fn nested_masks_multiply_their_coverage() {
|
||||
let outer_shape = rsc
|
||||
.ui
|
||||
.widgets
|
||||
.add_strong(Rect::new(UiColor::BLACK).radius(Len::abs(RADIUS)));
|
||||
.add_strong(Rect::new(PaintId::BLACK).radius(Len::abs(RADIUS)));
|
||||
let outer_shape_id = outer_shape.id();
|
||||
let root = rsc
|
||||
.ui
|
||||
@@ -1004,7 +1005,7 @@ fn a_scroll_area_opens_at_the_start_of_content_it_has_not_measured_yet() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)).any();
|
||||
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
|
||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: fill,
|
||||
x: None,
|
||||
@@ -1039,7 +1040,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let header_fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED)).any();
|
||||
let header_fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED)).any();
|
||||
let header_id = header_fill.id();
|
||||
let header = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: header_fill,
|
||||
@@ -1049,7 +1050,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
|
||||
let mut inner = Span::empty(Dir::DOWN);
|
||||
let mut rects = Vec::new();
|
||||
for _ in 0..3 {
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
rects.push(rect.weak());
|
||||
let sized = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: rect.any(),
|
||||
@@ -1061,7 +1062,7 @@ fn a_span_of_padded_children_inside_a_span_draws_each_where_its_box_is() {
|
||||
inner: sized.any(),
|
||||
exact_region: false,
|
||||
});
|
||||
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::BLUE)).any();
|
||||
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::BLUE)).any();
|
||||
let card = rsc.ui.widgets.add_strong(Stack {
|
||||
children: vec![fill, padded.any()],
|
||||
size: StackSize::Child(1),
|
||||
@@ -1122,7 +1123,7 @@ fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let first = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let first = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let first_id = first.id();
|
||||
let first = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: first.any(),
|
||||
@@ -1146,7 +1147,7 @@ fn a_new_child_in_a_growing_lazy_row_uses_its_final_box_immediately() {
|
||||
render.resize((200.0, 200.0));
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
let second = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let second = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let second_id = second.id();
|
||||
let second = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: second.any(),
|
||||
|
||||
+1
-1
@@ -1986,7 +1986,7 @@ mod drag_gesture_tests {
|
||||
}
|
||||
|
||||
fn some_id(ui: &mut UiData) -> WidgetId {
|
||||
ui.widgets.add_strong(Rect::new(UiColor::WHITE)).id()
|
||||
ui.widgets.add_strong(Rect::new(PaintId::WHITE)).id()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+10
-10
@@ -55,9 +55,9 @@ fn a_button_over_a_list_scrolls_the_list_and_still_clicks() {
|
||||
};
|
||||
|
||||
// the case in IRIS_TODO.md's report.
|
||||
let list = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let list = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let list_weak = list.weak();
|
||||
let button = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED));
|
||||
let button = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED));
|
||||
let button_weak = button.weak();
|
||||
|
||||
let scrolled = Rc::new(Cell::new(false));
|
||||
@@ -125,7 +125,7 @@ fn a_release_outside_every_hit_region_still_reaches_the_captured_widget() {
|
||||
events: EventManager::default(),
|
||||
};
|
||||
|
||||
let draggable = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)).any();
|
||||
let draggable = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
|
||||
let draggable_weak = draggable.weak();
|
||||
|
||||
let dropped = Rc::new(Cell::new(false));
|
||||
@@ -184,9 +184,9 @@ fn capturing_one_widget_starves_every_other_widget_of_events() {
|
||||
events: EventManager::default(),
|
||||
};
|
||||
|
||||
let a = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let a = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let a_weak = a.weak();
|
||||
let b = rsc.ui.widgets.add_strong(Rect::new(UiColor::RED));
|
||||
let b = rsc.ui.widgets.add_strong(Rect::new(PaintId::RED));
|
||||
let b_weak = b.weak();
|
||||
|
||||
let b_hovered = Rc::new(Cell::new(false));
|
||||
@@ -229,7 +229,7 @@ fn a_finger_drag_over_a_scroll_area_pans_it() {
|
||||
events: EventManager::default(),
|
||||
};
|
||||
|
||||
let scroll_strong = rect(UiColor::WHITE)
|
||||
let scroll_strong = rect(PaintId::WHITE)
|
||||
.height(Len::abs(1000.0))
|
||||
.scrollable(Axis::Y, Pin::Start)
|
||||
.add_strong(&mut rsc);
|
||||
@@ -319,7 +319,7 @@ fn a_scroll_area_that_captured_the_pointer_learns_its_gesture_ended() {
|
||||
ui: UiData::default(),
|
||||
events: EventManager::default(),
|
||||
};
|
||||
let scroll_strong = rect(UiColor::WHITE)
|
||||
let scroll_strong = rect(PaintId::WHITE)
|
||||
.height(Len::abs(1000.0))
|
||||
.scrollable(Axis::Y, Pin::Start)
|
||||
.add_strong(&mut rsc);
|
||||
@@ -374,7 +374,7 @@ fn taking_the_pointer_cancels_everyone_else_tracking_the_press() {
|
||||
events: EventManager::default(),
|
||||
};
|
||||
|
||||
let capturer = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let capturer = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let capturer_weak = capturer.weak();
|
||||
let bystander = rsc.ui.widgets.add_strong(Stack {
|
||||
children: vec![capturer.any()],
|
||||
@@ -474,7 +474,7 @@ fn a_drag_pans_whichever_nested_scroll_area_owns_its_axis() {
|
||||
};
|
||||
let seen = Rc::new(Cell::new(None));
|
||||
let record = seen.clone();
|
||||
let outer_strong = rect(UiColor::WHITE)
|
||||
let outer_strong = rect(PaintId::WHITE)
|
||||
.width(Len::abs(1000.0))
|
||||
.height(Len::abs(1000.0))
|
||||
.scrollable(Axis::X, Pin::Start)
|
||||
@@ -533,7 +533,7 @@ fn a_press_does_not_reach_a_widget_the_pointer_has_just_left() {
|
||||
let seen: [Rc<Cell<Option<WeakWidget<ScrollArea>>>>; 2] = Default::default();
|
||||
let half = |slot: &Rc<Cell<Option<WeakWidget<ScrollArea>>>>| {
|
||||
let record = slot.clone();
|
||||
rect(UiColor::WHITE)
|
||||
rect(PaintId::WHITE)
|
||||
.height(Len::abs(1000.0))
|
||||
.scrollable(Axis::Y, Pin::Start)
|
||||
.with_id(move |_rsc, id| {
|
||||
|
||||
@@ -741,6 +741,10 @@ impl Scrollable for LazySpan {
|
||||
}
|
||||
|
||||
impl Widget for LazySpan {
|
||||
fn child_order(&self) -> ChildOrder {
|
||||
ChildOrder::Axis(self.dir.axis)
|
||||
}
|
||||
|
||||
/// A lazy span animates exactly one thing, its fling -- and it drives
|
||||
/// its own rather than being handed deltas by a `ScrollArea` around
|
||||
/// it, since which rows exist at all is a function of where it is
|
||||
@@ -842,7 +846,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn fixed_row(rsc: &mut TestRsc, height: f32) -> (WeakWidget<Sized>, StrongWidget) {
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let sized = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: rect.any(),
|
||||
x: None,
|
||||
@@ -1054,9 +1058,9 @@ mod tests {
|
||||
rsc: &mut TestRsc,
|
||||
height: f32,
|
||||
) -> (WidgetId, WeakWidget<Sized>, StrongWidget) {
|
||||
let bg = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let bg = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let bg_id = bg.id();
|
||||
let fg_rect = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE));
|
||||
let fg_rect = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE));
|
||||
let fg = rsc.ui.widgets.add_strong(Sized {
|
||||
inner: fg_rect.any(),
|
||||
x: None,
|
||||
|
||||
@@ -98,7 +98,7 @@ mod tests {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let fill = rsc.ui.widgets.add_strong(Rect::new(UiColor::WHITE)).any();
|
||||
let fill = rsc.ui.widgets.add_strong(Rect::new(PaintId::WHITE)).any();
|
||||
let id = fill.id();
|
||||
let long = Some(Len::abs(1000.0));
|
||||
let tall = rsc.ui.widgets.add_strong(Sized {
|
||||
|
||||
@@ -8,6 +8,10 @@ pub struct Span {
|
||||
}
|
||||
|
||||
impl Widget for Span {
|
||||
fn child_order(&self) -> ChildOrder {
|
||||
ChildOrder::Axis(self.dir.axis)
|
||||
}
|
||||
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let axis = self.dir.axis;
|
||||
let gap = self.gap.apply_rest(painter.density()).abs;
|
||||
|
||||
+20
-9
@@ -1,26 +1,36 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone)]
|
||||
pub struct Rect {
|
||||
pub color: UiColor,
|
||||
paint: PaintValue,
|
||||
pub radius: Len,
|
||||
pub thickness: f32,
|
||||
pub inner_radius: f32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
pub fn new(color: UiColor) -> Self {
|
||||
pub fn new(paint: impl Paint) -> Self {
|
||||
Self {
|
||||
color,
|
||||
paint: paint.into_value(),
|
||||
radius: Len::ZERO,
|
||||
inner_radius: 0.0,
|
||||
thickness: 0.0,
|
||||
}
|
||||
}
|
||||
pub fn color(mut self, color: UiColor) -> Self {
|
||||
self.color = color;
|
||||
pub fn paint(mut self, paint: impl Paint) -> Self {
|
||||
self.paint = paint.into_value();
|
||||
self
|
||||
}
|
||||
pub fn color(self, paint: impl Paint) -> Self {
|
||||
self.paint(paint)
|
||||
}
|
||||
|
||||
pub fn set_paint(&mut self, paint: impl Paint) {
|
||||
self.paint = paint.into_value();
|
||||
}
|
||||
pub fn is_paint(&self, paint: &PaintId) -> bool {
|
||||
self.paint.is(paint)
|
||||
}
|
||||
pub fn radius(mut self, radius: impl Into<Len>) -> Self {
|
||||
self.radius = radius.into();
|
||||
self
|
||||
@@ -29,8 +39,9 @@ impl Rect {
|
||||
|
||||
impl Widget for Rect {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let paint = painter.paint_value(&mut self.paint);
|
||||
painter.primitive(RectPrimitive {
|
||||
color: self.color,
|
||||
paint,
|
||||
radius: self.radius.fold_dp(painter.density()).abs,
|
||||
thickness: self.thickness,
|
||||
inner_radius: self.inner_radius,
|
||||
@@ -43,6 +54,6 @@ impl Widget for Rect {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rect(color: UiColor) -> Rect {
|
||||
Rect::new(color)
|
||||
pub fn rect(paint: impl Paint) -> Rect {
|
||||
Rect::new(paint)
|
||||
}
|
||||
@@ -16,7 +16,7 @@ impl<State, O, H: WidgetOption<State>> TextBuilder<State, O, H> {
|
||||
self.attrs.line_height = self.attrs.font_size * LINE_HEIGHT_MULT;
|
||||
self
|
||||
}
|
||||
pub fn color(mut self, color: UiColor) -> Self {
|
||||
pub fn color(mut self, color: PaintId) -> Self {
|
||||
self.attrs.color = color;
|
||||
self
|
||||
}
|
||||
|
||||
+48
-123
@@ -1,7 +1,9 @@
|
||||
use crate::prelude::*;
|
||||
use iris_core::{TextData, UiColor};
|
||||
use iris_core::{PaintId, TextData};
|
||||
use parley::{Affinity, Layout, Selection};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use super::selection_layout;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use winit::{
|
||||
event::KeyEvent,
|
||||
@@ -22,10 +24,8 @@ pub enum Motion {
|
||||
|
||||
pub struct TextEdit {
|
||||
view: TextView,
|
||||
selection: Option<Selection>,
|
||||
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||
history: Vec<(String, Option<Selection>)>,
|
||||
double_hit: Option<usize>,
|
||||
pub(crate) press_origin: Option<Vec2>,
|
||||
pub mode: EditMode,
|
||||
}
|
||||
@@ -40,20 +40,14 @@ impl TextEdit {
|
||||
pub fn new(view: TextView, mode: EditMode) -> Self {
|
||||
Self {
|
||||
view,
|
||||
selection: None,
|
||||
history: Default::default(),
|
||||
double_hit: None,
|
||||
press_origin: None,
|
||||
mode,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_text(&self) -> Option<String> {
|
||||
let sel = self.selection?;
|
||||
if sel.is_collapsed() {
|
||||
return None;
|
||||
}
|
||||
Some(self.buf.text()[sel.text_range()].to_string())
|
||||
self.view.selection.selected_text(self.view.buf.text())
|
||||
}
|
||||
|
||||
/// The field's content. Byte-indexed, like everything else here since
|
||||
@@ -67,46 +61,19 @@ impl TextEdit {
|
||||
/// The selection as a byte range, collapsed to `caret..caret` when
|
||||
/// there is no span. `None` when the field is not focused.
|
||||
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
|
||||
Some(self.selection?.text_range())
|
||||
self.view.selection.range()
|
||||
}
|
||||
|
||||
/// The caret's byte offset -- the focus end of the selection, which is
|
||||
/// where typing lands regardless of which end of a span it is.
|
||||
pub fn caret(&self) -> Option<usize> {
|
||||
Some(self.selection?.focus().index())
|
||||
self.view.selection.caret()
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for TextEdit {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
let base = painter.layer;
|
||||
painter.child_layer();
|
||||
let used = self.view.draw(painter);
|
||||
painter.layer = base;
|
||||
let region = self.region();
|
||||
|
||||
let Some(selection) = self.selection else {
|
||||
painter.set_size(used);
|
||||
return;
|
||||
};
|
||||
let layout = self.view.buf.layout();
|
||||
|
||||
for (rect, _) in selection.geometry(layout) {
|
||||
let size = vec2(rect.width() as f32, rect.height() as f32);
|
||||
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::SKY),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
}
|
||||
|
||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
||||
let size = vec2(caret.width() as f32, caret.height() as f32);
|
||||
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(Color::WHITE),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
let used = self.view.draw_selectable(painter, true);
|
||||
painter.set_size(used);
|
||||
}
|
||||
|
||||
@@ -122,28 +89,26 @@ impl Widget for TextEdit {
|
||||
}
|
||||
}
|
||||
|
||||
const CARET_WIDTH: f32 = 1.0;
|
||||
|
||||
pub struct TextEditCtx<'a> {
|
||||
pub text: &'a mut TextEdit,
|
||||
pub data: &'a mut TextData,
|
||||
}
|
||||
|
||||
impl<'a> TextEditCtx<'a> {
|
||||
fn layout(&mut self) -> &Layout<UiColor> {
|
||||
let attrs = self.text.view.attrs.clone();
|
||||
let width = self.text.view.wrap_width();
|
||||
let density = self.data.density;
|
||||
self.text.view.buf.shape(self.data, &attrs, width, density);
|
||||
self.text.view.buf.layout()
|
||||
fn selection_ctx(&mut self) -> TextSelectionCtx<'_> {
|
||||
TextSelectionCtx {
|
||||
view: &mut self.text.view,
|
||||
data: self.data,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(&mut self) -> &Layout<iris_core::PaintId> {
|
||||
selection_layout(&mut self.text.view, self.data)
|
||||
}
|
||||
|
||||
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||
fn refresh(&mut self) {
|
||||
if let Some(sel) = self.text.selection {
|
||||
let layout = self.layout();
|
||||
self.text.selection = Some(sel.refresh(layout));
|
||||
}
|
||||
self.selection_ctx().refresh();
|
||||
}
|
||||
|
||||
pub fn take(&mut self) -> String {
|
||||
@@ -156,18 +121,18 @@ impl<'a> TextEditCtx<'a> {
|
||||
let text = self.string(text);
|
||||
self.text.view.buf.set_text(text);
|
||||
self.text.view.buf.changed = true;
|
||||
self.text.selection = None;
|
||||
self.text.view.selection.deselect();
|
||||
}
|
||||
|
||||
pub fn set_with_spans(&mut self, text: &str, spans: Vec<SpanStyle>) {
|
||||
let text = self.string(text);
|
||||
self.text.view.buf.set_text(text);
|
||||
self.text.view.buf.set_spans(spans);
|
||||
self.text.selection = None;
|
||||
self.text.view.selection.deselect();
|
||||
}
|
||||
|
||||
pub fn motion(&mut self, motion: Motion, select: bool) {
|
||||
let Some(sel) = self.text.selection else {
|
||||
let Some(sel) = self.text.view.selection.range else {
|
||||
return;
|
||||
};
|
||||
let layout = self.layout();
|
||||
@@ -184,7 +149,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
} else {
|
||||
apply_motion(sel, layout, motion, select)
|
||||
};
|
||||
self.text.selection = Some(sel);
|
||||
self.text.view.selection.range = Some(sel);
|
||||
}
|
||||
|
||||
pub fn replace(&mut self, len: usize, text: &str) {
|
||||
@@ -213,7 +178,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
return;
|
||||
}
|
||||
self.clear_span();
|
||||
let at = match self.text.selection {
|
||||
let at = match self.text.view.selection.range {
|
||||
Some(sel) => sel.focus().index(),
|
||||
// No caret means nowhere to put the text, so this drops the
|
||||
// keystroke -- which is invisible, and was the whole of the
|
||||
@@ -238,7 +203,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
|
||||
pub fn clear_span(&mut self) -> bool {
|
||||
let Some(sel) = self.text.selection else {
|
||||
let Some(sel) = self.text.view.selection.range else {
|
||||
return false;
|
||||
};
|
||||
if sel.is_collapsed() {
|
||||
@@ -252,13 +217,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
}
|
||||
|
||||
fn set_caret(&mut self, index: usize) {
|
||||
let index = index.min(self.text.view.buf.text().len());
|
||||
let layout = self.layout();
|
||||
self.text.selection = Some(Selection::from_byte_index(
|
||||
layout,
|
||||
index,
|
||||
Affinity::default(),
|
||||
));
|
||||
self.selection_ctx().set_caret(index);
|
||||
}
|
||||
|
||||
pub fn newline(&mut self) {
|
||||
@@ -271,7 +230,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
if self.clear_span() {
|
||||
return;
|
||||
}
|
||||
let Some(sel) = self.text.selection else {
|
||||
let Some(sel) = self.text.view.selection.range else {
|
||||
return;
|
||||
};
|
||||
let end = sel.focus().index();
|
||||
@@ -291,7 +250,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
if self.clear_span() {
|
||||
return;
|
||||
}
|
||||
let Some(sel) = self.text.selection else {
|
||||
let Some(sel) = self.text.view.selection.range else {
|
||||
return;
|
||||
};
|
||||
let start = sel.focus().index();
|
||||
@@ -350,67 +309,33 @@ impl<'a> TextEditCtx<'a> {
|
||||
/// actually *on* something" checks its own ranges, which is what
|
||||
/// makes a tap in the padding hit no link.
|
||||
pub fn byte_at(&mut self, pos: Vec2, size: Vec2) -> usize {
|
||||
let pos = pos - self.text.region().top_left().to_abs(size);
|
||||
let layout = self.layout();
|
||||
Selection::from_point(layout, pos.x, pos.y).focus().index()
|
||||
self.selection_ctx().byte_at(pos, size)
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self) {
|
||||
let len = self.text.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.text.selection = Some(Selection::new(anchor, focus));
|
||||
self.selection_ctx().select_all();
|
||||
}
|
||||
|
||||
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool, recent: bool) {
|
||||
let pos = pos - self.text.region().top_left().to_abs(size);
|
||||
let prev_sel = self.text.selection;
|
||||
let prev_hit = self.text.double_hit;
|
||||
|
||||
let outcome = {
|
||||
let layout = self.layout();
|
||||
if drag {
|
||||
prev_sel.map(|sel| (Some(sel.extend_to_point(layout, pos.x, pos.y)), prev_hit))
|
||||
} else {
|
||||
let hit = Selection::from_point(layout, pos.x, pos.y);
|
||||
let index = hit.focus().index();
|
||||
Some(if recent && prev_hit == Some(index) {
|
||||
(Some(Selection::line_from_point(layout, pos.x, pos.y)), None)
|
||||
} else if recent && prev_sel.map(|s| s.focus().index()) == Some(index) {
|
||||
(
|
||||
Some(Selection::word_from_point(layout, pos.x, pos.y)),
|
||||
Some(index),
|
||||
)
|
||||
} else {
|
||||
(Some(hit), None)
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((selection, double_hit)) = outcome {
|
||||
self.text.selection = selection;
|
||||
self.text.double_hit = double_hit;
|
||||
}
|
||||
self.selection_ctx().select(pos, size, drag, recent);
|
||||
}
|
||||
|
||||
pub fn deselect(&mut self) {
|
||||
self.text.selection = None;
|
||||
self.text.double_hit = None;
|
||||
self.selection_ctx().deselect();
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
||||
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
||||
let old = (
|
||||
self.text.view.buf.text().to_string(),
|
||||
self.text.view.selection.range,
|
||||
);
|
||||
let mut undo = false;
|
||||
let res = self.apply_event_inner(event, modifiers, &mut undo);
|
||||
if undo {
|
||||
if let Some((old, selection)) = self.text.history.pop() {
|
||||
self.set(&old);
|
||||
self.text.selection = selection;
|
||||
self.text.view.selection.range = selection;
|
||||
self.refresh();
|
||||
}
|
||||
} else if self.text.view.buf.text() != old.0 {
|
||||
@@ -495,7 +420,7 @@ impl<'a> TextEditCtx<'a> {
|
||||
|
||||
fn apply_motion(
|
||||
sel: Selection,
|
||||
layout: &Layout<UiColor>,
|
||||
layout: &Layout<PaintId>,
|
||||
motion: Motion,
|
||||
extend: bool,
|
||||
) -> Selection {
|
||||
@@ -512,15 +437,15 @@ fn apply_motion(
|
||||
}
|
||||
|
||||
trait RangeCursors {
|
||||
fn start_cursor(&self, layout: &Layout<UiColor>) -> parley::Cursor;
|
||||
fn end_cursor(&self, layout: &Layout<UiColor>) -> parley::Cursor;
|
||||
fn start_cursor(&self, layout: &Layout<PaintId>) -> parley::Cursor;
|
||||
fn end_cursor(&self, layout: &Layout<PaintId>) -> parley::Cursor;
|
||||
}
|
||||
|
||||
impl RangeCursors for std::ops::Range<usize> {
|
||||
fn start_cursor(&self, layout: &Layout<UiColor>) -> parley::Cursor {
|
||||
fn start_cursor(&self, layout: &Layout<PaintId>) -> parley::Cursor {
|
||||
parley::Cursor::from_byte_index(layout, self.start, Affinity::default())
|
||||
}
|
||||
fn end_cursor(&self, layout: &Layout<UiColor>) -> parley::Cursor {
|
||||
fn end_cursor(&self, layout: &Layout<PaintId>) -> parley::Cursor {
|
||||
parley::Cursor::from_byte_index(layout, self.end, Affinity::default())
|
||||
}
|
||||
}
|
||||
@@ -605,7 +530,7 @@ mod tests {
|
||||
ctx(&mut t, &mut d).set_caret(1);
|
||||
ctx(&mut t, &mut d).insert("b");
|
||||
assert_eq!(content(&t), "abc");
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 2);
|
||||
assert_eq!(t.caret(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -655,14 +580,14 @@ mod tests {
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
assert!(ctx(&mut t, &mut d).clear_span());
|
||||
assert_eq!(content(&t), "");
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 0);
|
||||
assert_eq!(t.caret(), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tapping_an_empty_field_places_a_caret_so_typing_lands() {
|
||||
let (mut t, mut d) = edit("", EditMode::MultiLine);
|
||||
ctx(&mut t, &mut d).select(vec2(40.0, 20.0), vec2(1080.0, 2400.0), false, false);
|
||||
assert!(t.selection.is_some(), "a tap must leave a caret behind");
|
||||
assert!(t.caret().is_some(), "a tap must leave a caret behind");
|
||||
ctx(&mut t, &mut d).insert("hi");
|
||||
assert_eq!(content(&t), "hi");
|
||||
}
|
||||
@@ -671,14 +596,14 @@ mod tests {
|
||||
fn tapping_past_the_end_of_the_text_clamps_to_the_end() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
||||
ctx(&mut t, &mut d).select(vec2(9000.0, 9000.0), vec2(1080.0, 2400.0), false, false);
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 3);
|
||||
assert_eq!(t.caret(), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_without_a_previous_selection_selects_nothing() {
|
||||
let (mut t, mut d) = edit("abc", EditMode::MultiLine);
|
||||
ctx(&mut t, &mut d).select(vec2(10.0, 10.0), vec2(1080.0, 2400.0), true, false);
|
||||
assert!(t.selection.is_none());
|
||||
assert!(t.selection_range().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -765,7 +690,7 @@ mod tests {
|
||||
let (mut t, mut d) = edit("abc", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).set_caret(0);
|
||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 1);
|
||||
assert_eq!(t.caret(), Some(1));
|
||||
ctx(&mut t, &mut d).motion(Motion::Right, true);
|
||||
assert_eq!(t.selected_text().as_deref(), Some("b"));
|
||||
}
|
||||
@@ -775,11 +700,11 @@ mod tests {
|
||||
let (mut t, mut d) = edit("abcdef", EditMode::SingleLine);
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
ctx(&mut t, &mut d).motion(Motion::Left, false);
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 0);
|
||||
assert_eq!(t.caret(), Some(0));
|
||||
|
||||
ctx(&mut t, &mut d).select_all();
|
||||
ctx(&mut t, &mut d).motion(Motion::Right, false);
|
||||
assert_eq!(t.selection.unwrap().focus().index(), 6);
|
||||
assert_eq!(t.caret(), Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+146
-2
@@ -1,9 +1,11 @@
|
||||
mod build;
|
||||
mod edit;
|
||||
mod selection;
|
||||
|
||||
pub use build::*;
|
||||
pub use edit::*;
|
||||
use iris_core::util::MutDetect;
|
||||
pub use selection::*;
|
||||
|
||||
use crate::prelude::*;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
@@ -19,6 +21,7 @@ pub struct TextView {
|
||||
tex: Option<RenderedText>,
|
||||
width: Option<f32>,
|
||||
pub hint: Option<StrongWidget>,
|
||||
selection: TextSelection,
|
||||
}
|
||||
|
||||
impl TextView {
|
||||
@@ -37,6 +40,7 @@ impl TextView {
|
||||
tex: None,
|
||||
width: None,
|
||||
hint,
|
||||
selection: TextSelection::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +98,39 @@ impl TextView {
|
||||
Size::abs(tex.size)
|
||||
}
|
||||
|
||||
pub(super) fn draw_selectable(&mut self, painter: &mut Painter, caret: bool) -> Size {
|
||||
let base = painter.layer;
|
||||
painter.child_layer();
|
||||
let used = self.draw(painter);
|
||||
painter.layer = base;
|
||||
let region = self.region();
|
||||
|
||||
let Some(selection) = self.selection.range else {
|
||||
return used;
|
||||
};
|
||||
let layout = self.buf.layout();
|
||||
for (rect, _) in selection.geometry(layout) {
|
||||
let size = vec2(rect.width() as f32, rect.height() as f32);
|
||||
let top_left = vec2(rect.x0 as f32, rect.y0 as f32);
|
||||
let paint = painter.paint(&PaintId::SKY);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(paint),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
}
|
||||
if caret {
|
||||
let caret = selection.focus().geometry(layout, CARET_WIDTH);
|
||||
let size = vec2(caret.width() as f32, caret.height() as f32);
|
||||
let top_left = vec2(caret.x0 as f32, caret.y0 as f32);
|
||||
let paint = painter.paint(&PaintId::WHITE);
|
||||
painter.primitive_within(
|
||||
RectPrimitive::color(paint),
|
||||
size.align(Align::TOP_LEFT).offset(top_left).within(®ion),
|
||||
);
|
||||
}
|
||||
used
|
||||
}
|
||||
|
||||
pub fn content(&self) -> String {
|
||||
self.buf.text().to_string()
|
||||
}
|
||||
@@ -111,14 +148,36 @@ impl Text {
|
||||
if self.content.changed {
|
||||
self.content.changed = false;
|
||||
self.view.buf.set_text(self.content.as_str());
|
||||
self.view.selection.deselect();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_text(&self) -> Option<String> {
|
||||
self.view.selection.selected_text(self.view.buf.text())
|
||||
}
|
||||
|
||||
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
|
||||
self.view.selection.range()
|
||||
}
|
||||
|
||||
pub fn set_with_spans(&mut self, content: impl Into<String>, spans: Vec<SpanStyle>) {
|
||||
let content = content.into();
|
||||
*self.content = content.clone();
|
||||
self.content.changed = false;
|
||||
self.view.buf.set_text(content);
|
||||
self.view.buf.set_spans(spans);
|
||||
self.view.selection.deselect();
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Text {
|
||||
fn draw(&mut self, painter: &mut Painter) {
|
||||
self.update_buf();
|
||||
let size = self.view.draw(painter);
|
||||
let size = if self.view.selection.range.is_some() {
|
||||
self.view.draw_selectable(painter, false)
|
||||
} else {
|
||||
self.view.draw(painter)
|
||||
};
|
||||
painter.set_size(size);
|
||||
}
|
||||
|
||||
@@ -127,6 +186,8 @@ impl Widget for Text {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const CARET_WIDTH: f32 = 1.0;
|
||||
|
||||
impl Deref for Text {
|
||||
type Target = TextAttrs;
|
||||
|
||||
@@ -160,6 +221,89 @@ mod tests {
|
||||
use crate::layout_tests::TestRsc;
|
||||
use crate::prelude::*;
|
||||
|
||||
fn rendered_text(content: &str) -> (TestRsc, UiRenderState, WeakWidget<Text>, StrongWidget) {
|
||||
let mut rsc = TestRsc {
|
||||
ui: UiData::default(),
|
||||
};
|
||||
let text = wtext(content).add_strong(&mut rsc);
|
||||
let id = text.weak();
|
||||
let root = text.any();
|
||||
let mut render = UiRenderState::new();
|
||||
render.resize((800.0, 600.0));
|
||||
render.update(&root, &mut rsc);
|
||||
(rsc, render, id, root)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_text_and_edit_text_use_the_same_selection_engine() {
|
||||
let (mut rsc, _render, text, _root) = rendered_text("hello there");
|
||||
text.selection(&mut rsc).select_all();
|
||||
|
||||
let view = TextView::new(TextBuffer::new("hello there"), TextAttrs::default(), None);
|
||||
let mut edit = TextEdit::new(view, EditMode::MultiLine);
|
||||
let mut data = TextData::default();
|
||||
TextEditCtx {
|
||||
text: &mut edit,
|
||||
data: &mut data,
|
||||
}
|
||||
.select_all();
|
||||
|
||||
assert_eq!(
|
||||
rsc.ui.widgets[text].selection_range(),
|
||||
edit.selection_range()
|
||||
);
|
||||
assert_eq!(rsc.ui.widgets[text].selected_text(), edit.selected_text());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_display_text_clears_its_now_stale_selection() {
|
||||
let (mut rsc, mut render, text, root) = rendered_text("before");
|
||||
text.selection(&mut rsc).select_all();
|
||||
assert_eq!(
|
||||
rsc.ui.widgets[text].selected_text().as_deref(),
|
||||
Some("before")
|
||||
);
|
||||
|
||||
*rsc.ui.widgets[text].content = "after".to_string();
|
||||
render.update(&root, &mut rsc);
|
||||
|
||||
assert_eq!(rsc.ui.widgets[text].selected_text(), None);
|
||||
assert_eq!(rsc.ui.widgets[text].selection_range(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_text_draws_the_shared_highlight_without_an_editing_caret() {
|
||||
let (mut rsc, mut render, text, root) = rendered_text("selected");
|
||||
let plain = render.active[&text.id()].primitives.len();
|
||||
text.selection(&mut rsc).select_all();
|
||||
render.update(&root, &mut rsc);
|
||||
let selected = render.active[&text.id()].primitives.len();
|
||||
|
||||
let view = TextView::new(TextBuffer::new("selected"), TextAttrs::default(), None);
|
||||
let edit = rsc
|
||||
.ui
|
||||
.widgets
|
||||
.add_strong(TextEdit::new(view, EditMode::MultiLine));
|
||||
let edit_id = edit.weak();
|
||||
let edit_root = edit.any();
|
||||
let mut edit_render = UiRenderState::new();
|
||||
edit_render.resize((800.0, 600.0));
|
||||
edit_render.update(&edit_root, &mut rsc);
|
||||
edit_id.edit(&mut rsc).select_all();
|
||||
edit_render.update(&edit_root, &mut rsc);
|
||||
let editable = edit_render.active[&edit_id.id()].primitives.len();
|
||||
|
||||
assert!(
|
||||
selected > plain,
|
||||
"the selection added no highlight primitive"
|
||||
);
|
||||
assert_eq!(
|
||||
editable,
|
||||
selected + 1,
|
||||
"editable text should add exactly its caret to the shared highlight"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clearing_the_atlas_re_renders_cached_text_instead_of_reusing_it() {
|
||||
let mut rsc = TestRsc {
|
||||
@@ -167,7 +311,7 @@ mod tests {
|
||||
};
|
||||
let root = wtext("hello there")
|
||||
.size(18)
|
||||
.color(UiColor::WHITE)
|
||||
.color(PaintId::WHITE)
|
||||
.add_strong(&mut rsc)
|
||||
.any();
|
||||
let mut render = UiRenderState::new();
|
||||
|
||||
@@ -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