231 lines
7.7 KiB
Rust
231 lines
7.7 KiB
Rust
use super::*;
|
|
use iced::advanced::renderer::Headless;
|
|
use iced::{Element, Event, Point, Size, keyboard, mouse};
|
|
use iced_runtime::user_interface::{Cache, UserInterface};
|
|
use std::cell::Cell;
|
|
|
|
const VALUE: &str = "Control portée alpha";
|
|
const SUBMIT: &str = "submit query";
|
|
|
|
#[derive(Default)]
|
|
struct Clipboard(Option<String>);
|
|
|
|
impl iced::advanced::Clipboard for Clipboard {
|
|
fn read(&self, _: iced::advanced::clipboard::Kind) -> Option<String> {
|
|
self.0.clone()
|
|
}
|
|
|
|
fn write(&mut self, _: iced::advanced::clipboard::Kind, text: String) {
|
|
self.0 = Some(text);
|
|
}
|
|
}
|
|
|
|
fn input(readonly: bool, observed: &Cell<Option<text_input::Status>>) -> Element<'_, String> {
|
|
// This is the same conditional-callback and style path as Hosted fields.
|
|
// No synthetic cursor/selection state is injected into the widget tree.
|
|
iced::widget::text_input("Search model names", VALUE)
|
|
.id("readonly-query")
|
|
.padding(10)
|
|
.size(18)
|
|
.on_input_maybe((!readonly).then_some(std::convert::identity::<String>))
|
|
.on_submit(SUBMIT.to_owned())
|
|
.style(move |theme, status| {
|
|
observed.set(Some(status));
|
|
selectable_input(theme, status)
|
|
})
|
|
.into()
|
|
}
|
|
|
|
fn key(
|
|
key: keyboard::Key,
|
|
code: keyboard::key::Code,
|
|
modifiers: keyboard::Modifiers,
|
|
text: Option<&str>,
|
|
) -> Event {
|
|
Event::Keyboard(keyboard::Event::KeyPressed {
|
|
modified_key: key.clone(),
|
|
key,
|
|
physical_key: keyboard::key::Physical::Code(code),
|
|
location: keyboard::Location::Standard,
|
|
modifiers,
|
|
text: text.map(Into::into),
|
|
repeat: false,
|
|
})
|
|
}
|
|
|
|
fn modifiers(modifiers: keyboard::Modifiers) -> Event {
|
|
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers))
|
|
}
|
|
|
|
fn near(pixel: &[u8], color: Color, tolerance: u8) -> bool {
|
|
pixel[..3]
|
|
.iter()
|
|
.zip([color.r, color.g, color.b])
|
|
.all(|(byte, channel)| byte.abs_diff((channel * 255.0).round() as u8) <= tolerance)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn mounted_readonly_input_keeps_selection_copy_and_readable_paint() {
|
|
let mut renderer = <iced::Renderer as Headless>::new(iced::Font::DEFAULT, 18.into(), None)
|
|
.await
|
|
.expect("mounted input test requires the supported WGPU headless renderer");
|
|
let theme = theme();
|
|
let size = Size::new(480.0, 64.0);
|
|
let cursor = mouse::Cursor::Available(Point::new(20.0, 18.0));
|
|
let command = keyboard::Modifiers::COMMAND;
|
|
let no_modifiers = keyboard::Modifiers::empty();
|
|
|
|
for submit_then_readonly in [false, true] {
|
|
let observed = Cell::new(None);
|
|
let mut clipboard = Clipboard::default();
|
|
let mut messages = Vec::new();
|
|
let mut ui = UserInterface::build(
|
|
input(!submit_then_readonly, &observed),
|
|
size,
|
|
Cache::new(),
|
|
&mut renderer,
|
|
);
|
|
let _ = ui.update(
|
|
&[
|
|
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
|
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
|
|
modifiers(command),
|
|
key(
|
|
keyboard::Key::Character("a".into()),
|
|
keyboard::key::Code::KeyA,
|
|
command,
|
|
None,
|
|
),
|
|
modifiers(no_modifiers),
|
|
],
|
|
cursor,
|
|
&mut renderer,
|
|
&mut clipboard,
|
|
&mut messages,
|
|
);
|
|
assert!(messages.is_empty(), "selection must not mutate the field");
|
|
|
|
if submit_then_readonly {
|
|
let _ = ui.update(
|
|
&[key(
|
|
keyboard::Key::Named(keyboard::key::Named::Enter),
|
|
keyboard::key::Code::Enter,
|
|
no_modifiers,
|
|
None,
|
|
)],
|
|
cursor,
|
|
&mut renderer,
|
|
&mut clipboard,
|
|
&mut messages,
|
|
);
|
|
assert_eq!(messages, [SUBMIT.to_owned()]);
|
|
messages.clear();
|
|
// Model the Hosted submit -> busy rebuild while retaining Iced's
|
|
// actual focus/selection cache, value and widget identity.
|
|
let cache = ui.into_cache();
|
|
ui = UserInterface::build(input(true, &observed), size, cache, &mut renderer);
|
|
}
|
|
|
|
let copy = [
|
|
modifiers(command),
|
|
key(
|
|
keyboard::Key::Character("c".into()),
|
|
keyboard::key::Code::KeyC,
|
|
command,
|
|
None,
|
|
),
|
|
modifiers(no_modifiers),
|
|
];
|
|
let _ = ui.update(©, cursor, &mut renderer, &mut clipboard, &mut messages);
|
|
assert_eq!(clipboard.0.as_deref(), Some(VALUE));
|
|
assert!(messages.is_empty(), "copy must not publish an edit");
|
|
|
|
let _ = ui.update(
|
|
&[
|
|
key(
|
|
keyboard::Key::Character("z".into()),
|
|
keyboard::key::Code::KeyZ,
|
|
no_modifiers,
|
|
Some("z"),
|
|
),
|
|
key(
|
|
keyboard::Key::Named(keyboard::key::Named::Backspace),
|
|
keyboard::key::Code::Backspace,
|
|
no_modifiers,
|
|
None,
|
|
),
|
|
],
|
|
cursor,
|
|
&mut renderer,
|
|
&mut clipboard,
|
|
&mut messages,
|
|
);
|
|
assert!(
|
|
messages.is_empty(),
|
|
"no callback means no edit, even while selected"
|
|
);
|
|
clipboard.0 = None;
|
|
let _ = ui.update(©, cursor, &mut renderer, &mut clipboard, &mut messages);
|
|
assert_eq!(
|
|
clipboard.0.as_deref(),
|
|
Some(VALUE),
|
|
"readonly input must retain its selected value"
|
|
);
|
|
|
|
ui.draw(
|
|
&mut renderer,
|
|
&theme,
|
|
&iced::advanced::renderer::Style { text_color: INK },
|
|
cursor,
|
|
);
|
|
assert_eq!(observed.get(), Some(text_input::Status::Disabled));
|
|
let pixels = Headless::screenshot(&mut renderer, Size::new(480, 64), 1.0, BG);
|
|
let style = selectable_input(&theme, text_input::Status::Disabled);
|
|
let selection_pixels: Vec<_> = pixels
|
|
.as_chunks::<4>()
|
|
.0
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, pixel)| near(pixel, style.selection, 3))
|
|
.map(|(index, _)| (index % 480, index / 480))
|
|
.collect();
|
|
assert!(
|
|
selection_pixels.len() > 100,
|
|
"actual readonly range must be painted"
|
|
);
|
|
let left = selection_pixels.iter().map(|p| p.0).min().unwrap();
|
|
let right = selection_pixels.iter().map(|p| p.0).max().unwrap();
|
|
let top = selection_pixels.iter().map(|p| p.1).min().unwrap();
|
|
let bottom = selection_pixels.iter().map(|p| p.1).max().unwrap();
|
|
let glyph_pixels = pixels
|
|
.as_chunks::<4>()
|
|
.0
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(index, pixel)| {
|
|
let (x, y) = (index % 480, index / 480);
|
|
(left..=right).contains(&x)
|
|
&& (top..=bottom).contains(&y)
|
|
&& near(pixel, style.value, 12)
|
|
})
|
|
.count();
|
|
assert!(
|
|
glyph_pixels > 10,
|
|
"selected glyphs must render with the dark value ink"
|
|
);
|
|
// Check specified colors separately from antialiased glyph-edge pixels.
|
|
require_contrast(
|
|
"painted readonly selected value",
|
|
style.value,
|
|
style.selection,
|
|
4.5,
|
|
);
|
|
require_contrast(
|
|
"painted readonly selection",
|
|
style.selection,
|
|
solid(style.background),
|
|
3.0,
|
|
);
|
|
}
|
|
}
|