Files
ARCADIA-Studio/crates/oa-desktop/src/style/tests/readonly.rs
T

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(&copy, 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(&copy, 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,
);
}
}