Skip to main content

iced_cube/data/
property_grid.rs

1//! Named properties in a compact two-column inspector, such as the
2//! properties panel of a design tool: names on the left, editors on the
3//! right, grouped into categories that collapse.
4//!
5//! [`State`] holds the properties by id with their values, which categories
6//! are collapsed, the highlight, the filter and the [`Sort`]. Its
7//! [`update`](State::update) returns [`Output::Changed`] whenever a value
8//! changes and [`Output::Reset`] when one goes back to its default.
9//!
10//! Text, numbers and colours are edited as text. A valid edit changes the
11//! value as it is typed; an invalid one keeps its draft and shows the error
12//! until it is fixed or cancelled, without changing the value. Cancelling
13//! puts back the value from before the edit.
14//!
15//! The grid resolves its keys through a [`Keymap`] of [`Action`]s while it
16//! has focus, which it takes when pressed; see [`default_keymap`].
17
18use std::collections::{HashMap, HashSet};
19use std::fmt;
20use std::hash::Hash;
21use std::ops::Range;
22use std::rc::Rc;
23
24use iced::advanced::layout::{self, Layout};
25use iced::advanced::widget::operation;
26use iced::advanced::widget::{Operation, Tree, Widget, tree};
27use iced::advanced::{Clipboard, Shell, overlay, renderer};
28use iced::keyboard::key::Named;
29use iced::widget::{self, Space, column, container, mouse_area, responsive, row};
30use iced::{
31    Alignment, Background, Border, Color, Element, Length, Padding, Rectangle, Renderer, Size,
32    Task, Theme, Vector, mouse,
33};
34
35use crate::data::ellipsis::ellipsis;
36use crate::data::interaction::{
37    Look, RowMenu, pressable, ringed, row_menu, scope, search_count, search_keys, shown_label,
38};
39use crate::data::rows::rows;
40use crate::forms::select::select;
41use crate::icon::{opacity, themed};
42use crate::keys::{self, Chord, Keymap};
43use crate::overlay::context_menu;
44use crate::overlay::dropdown_menu::item;
45use crate::overlay::tooltip::tooltip;
46use crate::primitives::button;
47use crate::primitives::checkbox::checkbox;
48use crate::primitives::icon_button::icon_button;
49use crate::primitives::input::{self, input};
50use crate::primitives::switch::switch;
51use crate::theme::{self, Tokens, fade, mix, radius, space, text_size};
52
53/// Height of every row, which fits a select.
54pub const ROW_HEIGHT: f32 = 36.0;
55/// The widest the name column starts out, before the user drags it.
56pub const NAME_WIDTH: f32 = 160.0;
57/// The narrowest the name column gets.
58pub const MIN_NAME_WIDTH: f32 = 64.0;
59/// The room the value column keeps when the name column is dragged wide.
60pub const MIN_VALUE_WIDTH: f32 = 88.0;
61/// The room the value column keeps before the name column, at its default
62/// width, starts to shrink: enough for a colour swatch and its hex.
63pub const VALUE_WIDTH: f32 = 112.0;
64/// Width of the draggable line between the columns.
65pub const HANDLE_WIDTH: f32 = 9.0;
66/// Width of the slot at the end of a row for the reset button.
67pub const RESET_WIDTH: f32 = 24.0;
68/// The least height of the description area under the rows, which grows
69/// to fit a longer description.
70pub const DESCRIPTION_HEIGHT: f32 = 64.0;
71/// The widget id of the filter box, which [`focus_search`] focuses.
72pub const SEARCH: widget::Id = widget::Id::new("iced-cube-property-grid-filter");
73/// The widget id of the field being edited, to find it in tests.
74pub const EDITOR_ID: widget::Id = widget::Id::new("iced-cube-property-grid-editor");
75
76/// Shown in the description area while nothing is highlighted.
77pub const NOTHING_SELECTED: &str = "Select a property to see its description.";
78
79/// Width of the slot before a name that marks a modified property.
80const MARK_WIDTH: f32 = 14.0;
81const MARK_SIZE: f32 = 6.0;
82const SWATCH_SIZE: f32 = 16.0;
83const ICON_SIZE: f32 = 16.0;
84const RESET_ICON_SIZE: f32 = 14.0;
85
86/// A property's value.
87#[derive(Debug, Clone, PartialEq)]
88pub enum Value {
89    /// Text, a read-only value or the chosen option of a choice.
90    Text(String),
91    Number(f64),
92    Bool(bool),
93    Colour(Color),
94}
95
96impl Value {
97    pub fn as_text(&self) -> Option<&str> {
98        match self {
99            Value::Text(value) => Some(value),
100            _ => None,
101        }
102    }
103
104    pub fn as_number(&self) -> Option<f64> {
105        match self {
106            Value::Number(value) => Some(*value),
107            _ => None,
108        }
109    }
110
111    pub fn as_bool(&self) -> Option<bool> {
112        match self {
113            Value::Bool(value) => Some(*value),
114            _ => None,
115        }
116    }
117
118    pub fn as_colour(&self) -> Option<Color> {
119        match self {
120            Value::Colour(value) => Some(*value),
121            _ => None,
122        }
123    }
124}
125
126impl From<&str> for Value {
127    fn from(value: &str) -> Self {
128        Value::Text(value.to_owned())
129    }
130}
131
132impl From<String> for Value {
133    fn from(value: String) -> Self {
134        Value::Text(value)
135    }
136}
137
138impl From<f64> for Value {
139    fn from(value: f64) -> Self {
140        Value::Number(value)
141    }
142}
143
144impl From<f32> for Value {
145    fn from(value: f32) -> Self {
146        Value::Number(f64::from(value))
147    }
148}
149
150impl From<i32> for Value {
151    fn from(value: i32) -> Self {
152        Value::Number(f64::from(value))
153    }
154}
155
156impl From<bool> for Value {
157    fn from(value: bool) -> Self {
158        Value::Bool(value)
159    }
160}
161
162impl From<Color> for Value {
163    fn from(value: Color) -> Self {
164        Value::Colour(value)
165    }
166}
167
168/// The limits and look of a number property.
169#[derive(Debug, Clone, PartialEq, Default)]
170pub struct Number {
171    pub min: Option<f64>,
172    pub max: Option<f64>,
173    /// How far Up and Down move the value while it is edited. A whole
174    /// step also makes values between steps invalid.
175    pub step: Option<f64>,
176    /// Decimal places shown. Defaults to those of the step, or as many as
177    /// the value needs.
178    pub decimals: Option<usize>,
179    /// Shown after the value, such as `px` or `%`, and accepted when typed.
180    pub unit: Option<String>,
181}
182
183impl Number {
184    /// The decimal places values are shown with, if fixed.
185    pub fn precision(&self) -> Option<usize> {
186        self.decimals.or_else(|| self.step.map(decimals_of))
187    }
188
189    /// The value as text, without the unit.
190    pub fn format(&self, value: f64) -> String {
191        let value = if value == 0.0 { 0.0 } else { value };
192        match self.precision() {
193            Some(decimals) => format!("{value:.decimals$}"),
194            None => format!("{value}"),
195        }
196    }
197
198    /// The value as text, with the unit.
199    pub fn display(&self, value: f64) -> String {
200        match &self.unit {
201            Some(unit) => format!("{}{unit}", self.format(value)),
202            None => self.format(value),
203        }
204    }
205
206    /// Reads typed text, with or without the unit, and checks it.
207    pub fn parse(&self, input: &str) -> Result<f64, String> {
208        let mut input = input.trim();
209        if let Some(unit) = self.unit.as_deref().filter(|unit| !unit.is_empty()) {
210            input = input.strip_suffix(unit).unwrap_or(input).trim_end();
211        }
212        let value: f64 = input.parse().map_err(|_| "Enter a number".to_owned())?;
213        if !value.is_finite() {
214            return Err("Enter a number".to_owned());
215        }
216        self.check(value)
217    }
218
219    /// Checks a value against the limits and a whole step.
220    pub fn check(&self, value: f64) -> Result<f64, String> {
221        if let Some(min) = self.min
222            && value < min
223        {
224            return Err(format!("Must be at least {}", self.display(min)));
225        }
226        if let Some(max) = self.max
227            && value > max
228        {
229            return Err(format!("Must be at most {}", self.display(max)));
230        }
231        let Some(step) = self.step.filter(|step| *step > 0.0 && step.fract() == 0.0) else {
232            return Ok(value);
233        };
234        let steps = (value - self.min.unwrap_or(0.0)) / step;
235        if (steps - steps.round()).abs() > 1e-9 {
236            if step == 1.0 {
237                return Err("Must be a whole number".to_owned());
238            }
239            return Err(format!("Must be a multiple of {}", self.format(step)));
240        }
241        Ok(value)
242    }
243
244    /// The value one step up or down, kept within the limits.
245    pub fn stepped(&self, value: f64, up: bool) -> f64 {
246        let step = self.step.filter(|step| *step > 0.0).unwrap_or(1.0);
247        let next = if up { value + step } else { value - step };
248        let next = match self.precision() {
249            Some(decimals) => {
250                let scale = 10f64.powi(i32::try_from(decimals).unwrap_or(i32::MAX).min(15));
251                (next * scale).round() / scale
252            }
253            None => next,
254        };
255        let next = self.min.map_or(next, |min| next.max(min));
256        self.max.map_or(next, |max| next.min(max))
257    }
258}
259
260/// Decimal places in a step such as 0.25.
261fn decimals_of(step: f64) -> usize {
262    let text = format!("{step}");
263    text.split_once('.')
264        .map_or(0, |(_, fraction)| fraction.len())
265}
266
267/// A colour as `#RRGGBB`, or `#RRGGBBAA` when it is not opaque.
268pub fn hex(colour: Color) -> String {
269    let [r, g, b, a] = colour.into_rgba8();
270    if a == u8::MAX {
271        format!("#{r:02X}{g:02X}{b:02X}")
272    } else {
273        format!("#{r:02X}{g:02X}{b:02X}{a:02X}")
274    }
275}
276
277/// Reads a hex colour: `#RGB`, `#RGBA`, `#RRGGBB` or `#RRGGBBAA`, with
278/// or without the `#`.
279pub fn parse_hex(input: &str) -> Result<Color, String> {
280    let error = || "Enter a hex colour, such as #3B82F6".to_owned();
281    let input = input.trim();
282    let digits = input.strip_prefix('#').unwrap_or(input);
283    if !digits.chars().all(|digit| digit.is_ascii_hexdigit()) {
284        return Err(error());
285    }
286    let digits: String = match digits.len() {
287        3 | 4 => digits.chars().flat_map(|digit| [digit, digit]).collect(),
288        6 | 8 => digits.to_owned(),
289        _ => return Err(error()),
290    };
291    let bytes = (0..digits.len())
292        .step_by(2)
293        .map(|start| {
294            digits
295                .get(start..start + 2)
296                .and_then(|pair| u8::from_str_radix(pair, 16).ok())
297        })
298        .collect::<Option<Vec<u8>>>()
299        .ok_or_else(error)?;
300    match bytes.as_slice() {
301        [r, g, b] => Ok(Color::from_rgb8(*r, *g, *b)),
302        [r, g, b, a] => Ok(Color::from_rgba8(*r, *g, *b, f32::from(*a) / 255.0)),
303        _ => Err(error()),
304    }
305}
306
307/// Checks text typed into a text property, returning the error to show.
308pub type Validate = fn(&str) -> Result<(), String>;
309
310/// How a true or false property is shown.
311#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
312pub enum Toggle {
313    #[default]
314    Checkbox,
315    Switch,
316}
317
318impl Toggle {
319    pub const ALL: [Toggle; 2] = [Toggle::Checkbox, Toggle::Switch];
320}
321
322/// What a property holds and how it is edited.
323#[derive(Debug, Clone)]
324pub enum Kind {
325    /// Free text, with an optional check.
326    Text(Option<Validate>),
327    Number(Number),
328    Toggle(Toggle),
329    /// One of a list of options, chosen with a select.
330    Choice(Vec<String>),
331    /// A colour, shown as a swatch and edited as hex.
332    Colour,
333    /// Text that cannot be edited.
334    ReadOnly,
335    /// Drawn by the app with [`PropertyGrid::custom`].
336    Custom,
337}
338
339/// One named property.
340#[derive(Debug, Clone)]
341pub struct Property<Id> {
342    pub id: Id,
343    pub name: String,
344    /// The group it is listed under while categorised.
345    pub category: Option<String>,
346    /// Shown under the rows while the property is highlighted.
347    pub description: Option<String>,
348    pub kind: Kind,
349    pub value: Value,
350    /// The value a reset goes back to. A property without one has no reset.
351    pub default: Option<Value>,
352    pub disabled: bool,
353}
354
355fn property<Id>(id: Id, name: impl Into<String>, kind: Kind, value: Value) -> Property<Id> {
356    Property {
357        id,
358        name: name.into(),
359        category: None,
360        description: None,
361        kind,
362        value,
363        default: None,
364        disabled: false,
365    }
366}
367
368/// A text property.
369pub fn text<Id>(id: Id, name: impl Into<String>, value: impl Into<String>) -> Property<Id> {
370    property(id, name, Kind::Text(None), Value::Text(value.into()))
371}
372
373/// A number property. Set its limits with [`min`](Property::min),
374/// [`max`](Property::max), [`step`](Property::step) and its look with
375/// [`decimals`](Property::decimals) and [`unit`](Property::unit).
376pub fn number<Id>(id: Id, name: impl Into<String>, value: f64) -> Property<Id> {
377    property(
378        id,
379        name,
380        Kind::Number(Number::default()),
381        Value::Number(value),
382    )
383}
384
385/// A true or false property, shown as a checkbox, or as a switch with
386/// [`switch`](Property::switch).
387pub fn toggle<Id>(id: Id, name: impl Into<String>, value: bool) -> Property<Id> {
388    property(id, name, Kind::Toggle(Toggle::Checkbox), Value::Bool(value))
389}
390
391/// A property chosen from `options` with a select.
392pub fn choice<Id>(
393    id: Id,
394    name: impl Into<String>,
395    options: impl IntoIterator<Item = impl Into<String>>,
396    selected: impl Into<String>,
397) -> Property<Id> {
398    property(
399        id,
400        name,
401        Kind::Choice(options.into_iter().map(Into::into).collect()),
402        Value::Text(selected.into()),
403    )
404}
405
406/// A colour property, shown as a swatch and edited as hex.
407pub fn colour<Id>(id: Id, name: impl Into<String>, value: Color) -> Property<Id> {
408    property(id, name, Kind::Colour, Value::Colour(value))
409}
410
411/// Text that cannot be edited, such as an id or a computed size.
412pub fn read_only<Id>(id: Id, name: impl Into<String>, value: impl Into<String>) -> Property<Id> {
413    property(id, name, Kind::ReadOnly, Value::Text(value.into()))
414}
415
416/// A property the app draws itself with [`PropertyGrid::custom`]. Without
417/// a renderer its value is shown as text.
418pub fn custom<Id>(id: Id, name: impl Into<String>, value: impl Into<Value>) -> Property<Id> {
419    property(id, name, Kind::Custom, value.into())
420}
421
422impl<Id> Property<Id> {
423    pub fn category(mut self, category: impl Into<String>) -> Self {
424        self.category = Some(category.into());
425        self
426    }
427
428    pub fn description(mut self, description: impl Into<String>) -> Self {
429        self.description = Some(description.into());
430        self
431    }
432
433    /// The value a reset goes back to. The name shows as modified while
434    /// the value differs from it.
435    pub fn default(mut self, value: impl Into<Value>) -> Self {
436        self.default = Some(value.into());
437        self
438    }
439
440    /// A disabled property shows its value but cannot be edited or reset.
441    pub fn disabled(mut self, disabled: bool) -> Self {
442        self.disabled = disabled;
443        self
444    }
445
446    /// The smallest value of a number property.
447    pub fn min(mut self, min: f64) -> Self {
448        if let Kind::Number(number) = &mut self.kind {
449            number.min = Some(min);
450        }
451        self
452    }
453
454    /// The largest value of a number property.
455    pub fn max(mut self, max: f64) -> Self {
456        if let Kind::Number(number) = &mut self.kind {
457            number.max = Some(max);
458        }
459        self
460    }
461
462    pub fn range(self, min: f64, max: f64) -> Self {
463        self.min(min).max(max)
464    }
465
466    /// How far Up and Down move a number while it is edited. A whole step
467    /// also makes values between steps invalid.
468    pub fn step(mut self, step: f64) -> Self {
469        if let Kind::Number(number) = &mut self.kind {
470            number.step = Some(step);
471        }
472        self
473    }
474
475    /// Decimal places a number is shown with.
476    pub fn decimals(mut self, decimals: usize) -> Self {
477        if let Kind::Number(number) = &mut self.kind {
478            number.decimals = Some(decimals);
479        }
480        self
481    }
482
483    /// Shown after a number, such as `px`, and accepted when typed.
484    pub fn unit(mut self, unit: impl Into<String>) -> Self {
485        if let Kind::Number(number) = &mut self.kind {
486            number.unit = Some(unit.into());
487        }
488        self
489    }
490
491    /// Shows a true or false property as a switch.
492    pub fn switch(mut self) -> Self {
493        if let Kind::Toggle(toggle) = &mut self.kind {
494            *toggle = Toggle::Switch;
495        }
496        self
497    }
498
499    /// Checks what is typed into a text property.
500    pub fn validate(mut self, validate: Validate) -> Self {
501        if let Kind::Text(check) = &mut self.kind {
502            *check = Some(validate);
503        }
504        self
505    }
506
507    /// Whether the value differs from the default.
508    pub fn is_modified(&self) -> bool {
509        self.default
510            .as_ref()
511            .is_some_and(|default| *default != self.value)
512    }
513
514    /// Whether the value is edited by typing: text, numbers and colours.
515    pub fn is_typed(&self) -> bool {
516        matches!(self.kind, Kind::Text(_) | Kind::Number(_) | Kind::Colour)
517    }
518
519    /// Whether the user can change the value from the grid.
520    pub fn is_editable(&self) -> bool {
521        !self.disabled && !matches!(self.kind, Kind::ReadOnly | Kind::Custom)
522    }
523
524    /// The value as shown in its row.
525    pub fn display(&self) -> String {
526        match (&self.kind, &self.value) {
527            (Kind::Number(number), Value::Number(value)) => number.display(*value),
528            _ => display(&self.value),
529        }
530    }
531
532    /// The value as text to start an edit with.
533    pub fn edit_text(&self) -> String {
534        match (&self.kind, &self.value) {
535            (Kind::Number(number), Value::Number(value)) => number.format(*value),
536            _ => display(&self.value),
537        }
538    }
539
540    /// Reads typed text into a value of this property's kind.
541    pub fn parse(&self, input: &str) -> Result<Value, String> {
542        match &self.kind {
543            Kind::Number(number) => number.parse(input).map(Value::Number),
544            Kind::Colour => parse_hex(input).map(Value::Colour),
545            Kind::Text(Some(validate)) => validate(input).map(|()| Value::Text(input.to_owned())),
546            Kind::Choice(options) if !options.iter().any(|option| option == input) => {
547                Err("Choose one of the options".to_owned())
548            }
549            _ => Ok(Value::Text(input.to_owned())),
550        }
551    }
552
553    /// Whether `value` suits this property, such as an option of a choice.
554    pub fn accepts(&self, value: &Value) -> bool {
555        match (&self.kind, value) {
556            (Kind::Text(validate), Value::Text(text)) => {
557                validate.is_none_or(|validate| validate(text).is_ok())
558            }
559            (Kind::Number(number), Value::Number(value)) => number.check(*value).is_ok(),
560            (Kind::Toggle(_), Value::Bool(_)) | (Kind::Colour, Value::Colour(_)) => true,
561            (Kind::Choice(options), Value::Text(text)) => options.contains(text),
562            (Kind::ReadOnly, Value::Text(_)) | (Kind::Custom, _) => true,
563            _ => false,
564        }
565    }
566}
567
568fn display(value: &Value) -> String {
569    match value {
570        Value::Text(text) => text.clone(),
571        Value::Number(number) => Number::default().format(*number),
572        Value::Bool(true) => "On".to_owned(),
573        Value::Bool(false) => "Off".to_owned(),
574        Value::Colour(colour) => hex(*colour),
575    }
576}
577
578/// How the properties are listed.
579#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
580pub enum Sort {
581    /// Under their categories, in the order they were given. Properties
582    /// without a category come first.
583    #[default]
584    Categorised,
585    /// One list, by name.
586    Alphabetical,
587}
588
589impl Sort {
590    pub const ALL: [Sort; 2] = [Sort::Categorised, Sort::Alphabetical];
591}
592
593/// One row of the grid.
594#[derive(Debug, Clone, PartialEq, Eq, Hash)]
595pub enum Entry<Id> {
596    /// A category heading, by name.
597    Category(String),
598    Property(Id),
599}
600
601/// Text being typed into a property, and why it is not valid yet.
602#[derive(Debug, Clone, PartialEq, Eq, Default)]
603pub struct Draft {
604    pub text: String,
605    pub error: Option<String>,
606}
607
608/// Which field the grid wants focused next.
609#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
610enum Target {
611    Filter,
612    Editor,
613}
614
615#[derive(Debug, Clone, PartialEq)]
616struct Editing<Id> {
617    id: Id,
618    /// The value before the edit, which a cancel puts back.
619    original: Value,
620}
621
622/// Everything that changes a property grid.
623#[derive(Debug, Clone, PartialEq)]
624pub enum Event<Id> {
625    /// The pointer pressed a row. A category heading also opens or closes.
626    Press(Entry<Id>),
627    ToggleCategory(String),
628    Expand(String),
629    Collapse(String),
630    Next,
631    Previous,
632    First,
633    Last,
634    /// Opens the highlighted category, or moves into it when it is open.
635    ExpandOrEnter,
636    /// Closes the highlighted category, or moves from a property to its
637    /// category.
638    CollapseOrLeave,
639    /// Edits a property: starts typing into text, numbers and colours,
640    /// flips a toggle and moves a choice to its next option.
641    Edit(Id),
642    /// Edits the highlighted property, or opens or closes the highlighted
643    /// category.
644    EditHighlighted,
645    /// Text typed into a property's field.
646    Input(Id, String),
647    /// Enter in the field being edited. Ends the edit unless it is invalid.
648    Submit,
649    /// Ends the edit and puts back the value from before it. Without an
650    /// edit, clears the filter.
651    Cancel,
652    /// Steps the number being edited up.
653    Increment,
654    /// Steps the number being edited down.
655    Decrement,
656    /// A new value from a checkbox, switch or select.
657    Set(Id, Value),
658    /// Puts a property back to its default.
659    Reset(Id),
660    ResetHighlighted,
661    /// The search text, which filters properties by name, changed.
662    Search(String),
663    /// Moves keyboard focus to the filter box.
664    FocusSearch,
665    /// Asks the app to copy a property's value, as it is shown.
666    CopyValue(Id),
667    Sort(Sort),
668    /// The pointer pressed the line between the columns.
669    ResizeStart,
670    /// The pointer dragged the line: the name column's new width.
671    Resize(f32),
672    ResizeEnd,
673    /// The grid gained or lost focus. The widget sends this itself.
674    Focus(bool),
675}
676
677/// What the app may need to act on after an [`Event`].
678#[derive(Debug, Clone, PartialEq)]
679pub enum Output<Id> {
680    /// A property has a new value.
681    Changed { id: Id, value: Value },
682    /// A property went back to its default, which is `value`.
683    Reset { id: Id, value: Value },
684    /// A value as it is shown, such as `120px` or `#3B82F6`, to write to
685    /// the clipboard.
686    CopyValue(String),
687}
688
689/// The properties, their values, and what is open, highlighted, filtered
690/// and being edited.
691#[derive(Debug, Clone)]
692pub struct State<Id> {
693    properties: Vec<Property<Id>>,
694    index: HashMap<Id, usize>,
695    categories: Vec<String>,
696    collapsed: HashSet<String>,
697    sort: Sort,
698    query: String,
699    highlighted: Option<Entry<Id>>,
700    editing: Option<Editing<Id>>,
701    drafts: HashMap<Id, Draft>,
702    name_width: Option<f32>,
703    resizing: bool,
704    focused: bool,
705    /// Counts focus requests, so the widget acts on each one once.
706    focus: (u64, Option<Target>),
707}
708
709impl<Id: Copy + Eq + Hash> State<Id> {
710    /// Holds `properties`, categorised with every category open. Ids must
711    /// be unique; a later property with the same id replaces the earlier.
712    pub fn new(properties: impl IntoIterator<Item = Property<Id>>) -> Self {
713        let mut list: Vec<Property<Id>> = Vec::new();
714        let mut index = HashMap::new();
715        for property in properties {
716            match index.get(&property.id) {
717                Some(&position) => {
718                    if let Some(slot) = list.get_mut(position) {
719                        *slot = property;
720                    }
721                }
722                None => {
723                    let _ = index.insert(property.id, list.len());
724                    list.push(property);
725                }
726            }
727        }
728        let mut categories: Vec<String> = Vec::new();
729        for category in list
730            .iter()
731            .filter_map(|property| property.category.as_ref())
732        {
733            if !categories.contains(category) {
734                categories.push(category.clone());
735            }
736        }
737        Self {
738            properties: list,
739            index,
740            categories,
741            collapsed: HashSet::new(),
742            sort: Sort::default(),
743            query: String::new(),
744            highlighted: None,
745            editing: None,
746            drafts: HashMap::new(),
747            name_width: None,
748            resizing: false,
749            focused: false,
750            focus: (0, None),
751        }
752    }
753
754    pub fn with_sort(mut self, sort: Sort) -> Self {
755        self.sort = sort;
756        self
757    }
758
759    /// Starts with these categories closed.
760    pub fn with_collapsed(
761        mut self,
762        categories: impl IntoIterator<Item = impl Into<String>>,
763    ) -> Self {
764        self.collapsed
765            .extend(categories.into_iter().map(Into::into));
766        self
767    }
768
769    /// Starts the name column at `width` pixels rather than
770    /// [`NAME_WIDTH`], which a narrow grid shrinks.
771    pub fn with_name_width(mut self, width: f32) -> Self {
772        self.name_width = Some(width.max(MIN_NAME_WIDTH));
773        self
774    }
775
776    /// Every property, in the order given.
777    pub fn properties(&self) -> &[Property<Id>] {
778        &self.properties
779    }
780
781    pub fn property(&self, id: Id) -> Option<&Property<Id>> {
782        self.index
783            .get(&id)
784            .and_then(|&position| self.properties.get(position))
785    }
786
787    fn property_mut(&mut self, id: Id) -> Option<&mut Property<Id>> {
788        self.index
789            .get(&id)
790            .and_then(|&position| self.properties.get_mut(position))
791    }
792
793    pub fn value(&self, id: Id) -> Option<&Value> {
794        self.property(id).map(|property| &property.value)
795    }
796
797    /// Sets a value from the app, such as after an undo or from a custom
798    /// editor, dropping any draft or edit of it. Returns whether it changed.
799    pub fn set(&mut self, id: Id, value: impl Into<Value>) -> bool {
800        let value = value.into();
801        let _ = self.drafts.remove(&id);
802        if self.editing_id() == Some(id) {
803            self.editing = None;
804        }
805        let Some(property) = self.property_mut(id) else {
806            return false;
807        };
808        if property.value == value {
809            return false;
810        }
811        property.value = value;
812        true
813    }
814
815    /// Category names, in the order they first appear.
816    pub fn categories(&self) -> &[String] {
817        &self.categories
818    }
819
820    pub fn is_expanded(&self, category: &str) -> bool {
821        !self.collapsed.contains(category)
822    }
823
824    pub fn sort(&self) -> Sort {
825        self.sort
826    }
827
828    /// The search text, which filters properties by name.
829    pub fn query(&self) -> &str {
830        &self.query
831    }
832
833    /// The row the keyboard acts on, which the description area describes.
834    pub fn highlighted(&self) -> Option<&Entry<Id>> {
835        self.highlighted.as_ref()
836    }
837
838    /// The highlighted property, if a property rather than a category is
839    /// highlighted.
840    pub fn highlighted_property(&self) -> Option<&Property<Id>> {
841        match self.highlighted {
842            Some(Entry::Property(id)) => self.property(id),
843            _ => None,
844        }
845    }
846
847    /// The property being typed into.
848    pub fn editing(&self) -> Option<Id> {
849        self.editing_id()
850    }
851
852    fn editing_id(&self) -> Option<Id> {
853        self.editing.as_ref().map(|editing| editing.id)
854    }
855
856    /// Text typed into a property that is being edited, or that was left
857    /// while invalid.
858    pub fn draft(&self, id: Id) -> Option<&Draft> {
859        self.drafts.get(&id)
860    }
861
862    /// Why a property's draft is not valid.
863    pub fn error(&self, id: Id) -> Option<&str> {
864        self.drafts
865            .get(&id)
866            .and_then(|draft| draft.error.as_deref())
867    }
868
869    /// Whether resetting the property would change anything: it has a
870    /// default, is enabled, and differs from the default or holds a draft.
871    pub fn can_reset(&self, id: Id) -> bool {
872        self.property(id).is_some_and(|property| {
873            !property.disabled
874                && property.default.is_some()
875                && (property.is_modified() || self.draft(id).is_some())
876        })
877    }
878
879    pub fn is_focused(&self) -> bool {
880        self.focused
881    }
882
883    /// Whether the line between the columns is being dragged.
884    pub fn is_resizing(&self) -> bool {
885        self.resizing
886    }
887
888    /// The name column's width once set or dragged.
889    pub fn name_width(&self) -> Option<f32> {
890        self.name_width
891    }
892
893    /// Whether a property passes the filter.
894    pub fn matches(&self, id: Id) -> bool {
895        self.property(id)
896            .is_some_and(|property| self.passes(property))
897    }
898
899    fn passes(&self, property: &Property<Id>) -> bool {
900        let query = self.query.trim();
901        query.is_empty() || property.name.to_lowercase().contains(&query.to_lowercase())
902    }
903
904    /// How many properties in a category pass the filter.
905    pub fn count(&self, category: &str) -> usize {
906        self.properties
907            .iter()
908            .filter(|property| property.category.as_deref() == Some(category))
909            .filter(|property| self.passes(property))
910            .count()
911    }
912
913    /// The rows on screen, in order: properties that pass the filter, under
914    /// their categories while categorised.
915    pub fn entries(&self) -> Vec<Entry<Id>> {
916        let passing = self
917            .properties
918            .iter()
919            .filter(|property| self.passes(property));
920        if self.sort == Sort::Alphabetical {
921            let mut sorted: Vec<&Property<Id>> = passing.collect();
922            sorted.sort_by_cached_key(|property| property.name.to_lowercase());
923            return sorted
924                .into_iter()
925                .map(|property| Entry::Property(property.id))
926                .collect();
927        }
928        let mut entries: Vec<Entry<Id>> = passing
929            .clone()
930            .filter(|property| property.category.is_none())
931            .map(|property| Entry::Property(property.id))
932            .collect();
933        for category in &self.categories {
934            let members: Vec<Id> = passing
935                .clone()
936                .filter(|property| property.category.as_ref() == Some(category))
937                .map(|property| property.id)
938                .collect();
939            if members.is_empty() {
940                continue;
941            }
942            entries.push(Entry::Category(category.clone()));
943            if self.is_expanded(category) {
944                entries.extend(members.into_iter().map(Entry::Property));
945            }
946        }
947        entries
948    }
949
950    /// Applies an event and returns what the app may need to act on.
951    pub fn update(&mut self, event: Event<Id>) -> Option<Output<Id>> {
952        match event {
953            Event::Press(entry) => {
954                let same = matches!(&entry, Entry::Property(id) if self.editing_id() == Some(*id));
955                if !same {
956                    self.finish();
957                }
958                if let Entry::Category(name) = &entry {
959                    self.toggle(name);
960                }
961                self.highlighted = Some(entry);
962            }
963            Event::ToggleCategory(name) => self.toggle(&name),
964            Event::Expand(name) => {
965                let _ = self.collapsed.remove(&name);
966            }
967            Event::Collapse(name) => self.collapse(&name),
968            Event::Next => self.step(Step::Next),
969            Event::Previous => self.step(Step::Previous),
970            Event::First => self.step(Step::First),
971            Event::Last => self.step(Step::Last),
972            Event::ExpandOrEnter => self.expand_or_enter(),
973            Event::CollapseOrLeave => self.collapse_or_leave(),
974            Event::Edit(id) => return self.edit(id),
975            Event::EditHighlighted => match self.highlighted.clone() {
976                Some(Entry::Property(id)) => return self.edit(id),
977                Some(Entry::Category(name)) => self.toggle(&name),
978                None => {}
979            },
980            Event::Input(id, input) => return self.input(id, input),
981            Event::Submit => self.submit(),
982            Event::Cancel => return self.cancel(),
983            Event::Increment => return self.nudge(true),
984            Event::Decrement => return self.nudge(false),
985            Event::Set(id, value) => {
986                if self.editing_id() != Some(id) {
987                    self.finish();
988                }
989                return self.choose(id, value);
990            }
991            Event::Reset(id) => return self.reset(id),
992            Event::ResetHighlighted => {
993                if let Some(Entry::Property(id)) = self.highlighted {
994                    return self.reset(id);
995                }
996            }
997            Event::Search(query) => {
998                self.finish();
999                self.query = query;
1000                self.highlight_nearest_shown();
1001            }
1002            Event::FocusSearch => {
1003                self.finish();
1004                self.request(Target::Filter);
1005            }
1006            Event::CopyValue(id) => {
1007                let value = self.property(id)?.display();
1008                if self.editing_id() != Some(id) {
1009                    self.finish();
1010                }
1011                self.highlighted = Some(Entry::Property(id));
1012                return Some(Output::CopyValue(value));
1013            }
1014            Event::Sort(sort) => {
1015                self.finish();
1016                self.sort = sort;
1017                self.keep_highlight_visible();
1018            }
1019            Event::ResizeStart => self.resizing = true,
1020            Event::Resize(width) => {
1021                if self.resizing {
1022                    self.name_width = Some(width.max(MIN_NAME_WIDTH));
1023                }
1024            }
1025            Event::ResizeEnd => self.resizing = false,
1026            Event::Focus(focused) => {
1027                self.focused = focused;
1028                if !focused {
1029                    self.finish();
1030                    self.resizing = false;
1031                    self.focus.1 = None;
1032                } else if self.highlighted.is_none() {
1033                    self.highlighted = self.first_property();
1034                }
1035            }
1036        }
1037        None
1038    }
1039
1040    /// Turns a key press into an event through `keymap`.
1041    pub fn key_event(&self, keymap: &Keymap<Action>, key: &keys::Event) -> Option<Event<Id>> {
1042        keymap
1043            .resolve_event(key)
1044            .and_then(|action| action.event(self))
1045    }
1046
1047    /// The focus request the widget has not acted on yet, by count.
1048    fn focus_request(&self) -> Option<(u64, Target)> {
1049        let (count, target) = self.focus;
1050        target.map(|target| (count, target))
1051    }
1052
1053    fn request(&mut self, target: Target) {
1054        self.focus = (self.focus.0.wrapping_add(1), Some(target));
1055    }
1056
1057    fn first_property(&self) -> Option<Entry<Id>> {
1058        let entries = self.entries();
1059        entries
1060            .iter()
1061            .find(|entry| matches!(entry, Entry::Property(_)))
1062            .or(entries.first())
1063            .cloned()
1064    }
1065
1066    /// Every property in the order the grid lists them, shown or not.
1067    fn display_order(&self) -> Vec<Id> {
1068        if self.sort == Sort::Alphabetical {
1069            let mut sorted: Vec<&Property<Id>> = self.properties.iter().collect();
1070            sorted.sort_by_cached_key(|property| property.name.to_lowercase());
1071            return sorted.into_iter().map(|property| property.id).collect();
1072        }
1073        let mut order: Vec<Id> = Vec::with_capacity(self.properties.len());
1074        for category in std::iter::once(None).chain(self.categories.iter().map(Some)) {
1075            order.extend(
1076                self.properties
1077                    .iter()
1078                    .filter(|property| property.category.as_ref() == category)
1079                    .map(|property| property.id),
1080            );
1081        }
1082        order
1083    }
1084
1085    /// After the search changed, keeps the highlight on a shown row: the
1086    /// highlighted property if it still passes, or else the nearest one
1087    /// shown at or after it, or else the last shown before it.
1088    fn highlight_nearest_shown(&mut self) {
1089        let shown = self.entries();
1090        if self.highlighted.is_none() && !self.query.trim().is_empty() {
1091            self.highlighted = self.first_property();
1092            return;
1093        }
1094        let Some(highlighted) = self.highlighted.clone() else {
1095            return;
1096        };
1097        if shown.contains(&highlighted) {
1098            return;
1099        }
1100        let order = self.display_order();
1101        let start = match &highlighted {
1102            Entry::Property(id) => order.iter().position(|other| other == id),
1103            Entry::Category(name) => order.iter().position(|&id| {
1104                self.property(id)
1105                    .is_some_and(|property| property.category.as_ref() == Some(name))
1106            }),
1107        }
1108        .unwrap_or(0);
1109        let (before, after) = order.split_at(start.min(order.len()));
1110        let is_shown = |id: &&Id| shown.contains(&Entry::Property(**id));
1111        let nearest = after
1112            .iter()
1113            .find(is_shown)
1114            .or_else(|| before.iter().rev().find(is_shown));
1115        self.highlighted = match nearest {
1116            Some(&id) => Some(Entry::Property(id)),
1117            None => self.first_property(),
1118        };
1119    }
1120
1121    fn keep_highlight_visible(&mut self) {
1122        let Some(highlighted) = &self.highlighted else {
1123            return;
1124        };
1125        if !self.entries().contains(highlighted) {
1126            self.highlighted = self.first_property();
1127        }
1128    }
1129
1130    fn toggle(&mut self, name: &str) {
1131        if self.collapsed.contains(name) {
1132            let _ = self.collapsed.remove(name);
1133        } else {
1134            self.collapse(name);
1135        }
1136    }
1137
1138    fn collapse(&mut self, name: &str) {
1139        if !self.categories.iter().any(|category| category == name) {
1140            return;
1141        }
1142        let _ = self.collapsed.insert(name.to_owned());
1143        if self.sort == Sort::Alphabetical {
1144            return;
1145        }
1146        let inside = |id: Option<Id>| {
1147            id.and_then(|id| self.property(id))
1148                .is_some_and(|property| property.category.as_deref() == Some(name))
1149        };
1150        let highlighted = match self.highlighted {
1151            Some(Entry::Property(id)) => Some(id),
1152            _ => None,
1153        };
1154        let hides_highlight = inside(highlighted);
1155        let hides_edit = inside(self.editing_id());
1156        if hides_highlight {
1157            self.highlighted = Some(Entry::Category(name.to_owned()));
1158        }
1159        if hides_edit {
1160            self.finish();
1161        }
1162    }
1163
1164    fn step(&mut self, step: Step) {
1165        self.finish();
1166        let entries = self.entries();
1167        let current = self
1168            .highlighted
1169            .as_ref()
1170            .and_then(|highlighted| entries.iter().position(|entry| entry == highlighted));
1171        let target = match (step, current) {
1172            (Step::First, _) | (Step::Next, None) => entries.first(),
1173            (Step::Last, _) | (Step::Previous, None) => entries.last(),
1174            (Step::Next, Some(index)) => entries.get(index + 1).or(entries.get(index)),
1175            (Step::Previous, Some(index)) => entries.get(index.saturating_sub(1)),
1176        };
1177        if let Some(target) = target {
1178            self.highlighted = Some(target.clone());
1179        }
1180    }
1181
1182    fn expand_or_enter(&mut self) {
1183        let Some(Entry::Category(name)) = self.highlighted.clone() else {
1184            return;
1185        };
1186        if !self.is_expanded(&name) {
1187            let _ = self.collapsed.remove(&name);
1188            return;
1189        }
1190        let entries = self.entries();
1191        let first = entries
1192            .iter()
1193            .skip_while(|entry| **entry != Entry::Category(name.clone()))
1194            .nth(1)
1195            .filter(|entry| matches!(entry, Entry::Property(_)));
1196        if let Some(first) = first {
1197            self.highlighted = Some(first.clone());
1198        }
1199    }
1200
1201    fn collapse_or_leave(&mut self) {
1202        match self.highlighted.clone() {
1203            Some(Entry::Category(name)) => self.collapse(&name),
1204            Some(Entry::Property(id)) if self.sort == Sort::Categorised => {
1205                let category = self
1206                    .property(id)
1207                    .and_then(|property| property.category.clone());
1208                if let Some(category) = category {
1209                    self.finish();
1210                    self.highlighted = Some(Entry::Category(category));
1211                }
1212            }
1213            _ => {}
1214        }
1215    }
1216
1217    /// Ends the edit. A valid draft has already been applied and goes; an
1218    /// invalid one stays so the row keeps showing it and its error.
1219    fn finish(&mut self) {
1220        let Some(editing) = self.editing.take() else {
1221            return;
1222        };
1223        if self.error(editing.id).is_none() {
1224            let _ = self.drafts.remove(&editing.id);
1225        }
1226    }
1227
1228    /// Sets a value, reporting it when it changed.
1229    fn assign(&mut self, id: Id, value: Value) -> Option<Output<Id>> {
1230        let property = self.property_mut(id)?;
1231        if property.value == value {
1232            return None;
1233        }
1234        property.value = value.clone();
1235        Some(Output::Changed { id, value })
1236    }
1237
1238    fn edit(&mut self, id: Id) -> Option<Output<Id>> {
1239        let property = self.property(id)?;
1240        if property.disabled {
1241            self.highlighted = Some(Entry::Property(id));
1242            return None;
1243        }
1244        let kind = property.kind.clone();
1245        let value = property.value.clone();
1246        let typed = property.is_typed();
1247        let text = property.edit_text();
1248        if self.editing_id() != Some(id) {
1249            self.finish();
1250        }
1251        self.highlighted = Some(Entry::Property(id));
1252        match (kind, value) {
1253            (Kind::Toggle(_), Value::Bool(on)) => self.assign(id, Value::Bool(!on)),
1254            (Kind::Choice(options), Value::Text(current)) => {
1255                let next = options
1256                    .iter()
1257                    .position(|option| *option == current)
1258                    .map_or(0, |index| (index + 1) % options.len().max(1));
1259                let option = options.get(next)?.clone();
1260                self.assign(id, Value::Text(option))
1261            }
1262            (_, value) if typed => {
1263                if self.editing_id() != Some(id) {
1264                    let _ = self.drafts.entry(id).or_insert(Draft { text, error: None });
1265                    self.editing = Some(Editing {
1266                        id,
1267                        original: value,
1268                    });
1269                }
1270                self.request(Target::Editor);
1271                None
1272            }
1273            _ => None,
1274        }
1275    }
1276
1277    fn input(&mut self, id: Id, input: String) -> Option<Output<Id>> {
1278        let property = self.property(id).filter(|property| property.is_typed())?;
1279        if property.disabled {
1280            return None;
1281        }
1282        let parsed = property.parse(&input);
1283        let value = property.value.clone();
1284        if self.editing_id() != Some(id) {
1285            self.finish();
1286            self.editing = Some(Editing {
1287                id,
1288                original: value,
1289            });
1290            self.highlighted = Some(Entry::Property(id));
1291        }
1292        match parsed {
1293            Ok(value) => {
1294                let _ = self.drafts.insert(
1295                    id,
1296                    Draft {
1297                        text: input,
1298                        error: None,
1299                    },
1300                );
1301                self.assign(id, value)
1302            }
1303            Err(error) => {
1304                let _ = self.drafts.insert(
1305                    id,
1306                    Draft {
1307                        text: input,
1308                        error: Some(error),
1309                    },
1310                );
1311                None
1312            }
1313        }
1314    }
1315
1316    fn submit(&mut self) {
1317        let Some(id) = self.editing_id() else {
1318            return;
1319        };
1320        if self.error(id).is_some() {
1321            return;
1322        }
1323        let _ = self.drafts.remove(&id);
1324        self.editing = None;
1325    }
1326
1327    fn cancel(&mut self) -> Option<Output<Id>> {
1328        let Some(editing) = self.editing.take() else {
1329            self.query.clear();
1330            return None;
1331        };
1332        let _ = self.drafts.remove(&editing.id);
1333        self.assign(editing.id, editing.original)
1334    }
1335
1336    fn nudge(&mut self, up: bool) -> Option<Output<Id>> {
1337        let id = self.editing_id()?;
1338        let property = self.property(id)?;
1339        let (Kind::Number(number), Value::Number(current)) = (&property.kind, &property.value)
1340        else {
1341            return None;
1342        };
1343        let base = self
1344            .drafts
1345            .get(&id)
1346            .and_then(|draft| number.parse(&draft.text).ok())
1347            .unwrap_or(*current);
1348        let next = number.stepped(base, up);
1349        let text = number.format(next);
1350        let _ = self.drafts.insert(id, Draft { text, error: None });
1351        self.assign(id, Value::Number(next))
1352    }
1353
1354    fn choose(&mut self, id: Id, value: Value) -> Option<Output<Id>> {
1355        let property = self.property(id)?;
1356        if !property.is_editable() || !property.accepts(&value) {
1357            return None;
1358        }
1359        self.highlighted = Some(Entry::Property(id));
1360        self.assign(id, value)
1361    }
1362
1363    fn reset(&mut self, id: Id) -> Option<Output<Id>> {
1364        let property = self.property(id)?;
1365        if property.disabled {
1366            return None;
1367        }
1368        let default = property.default.clone()?;
1369        let _ = self.drafts.remove(&id);
1370        if self.editing_id() == Some(id) {
1371            self.editing = None;
1372        }
1373        let property = self.property_mut(id)?;
1374        if property.value == default {
1375            return None;
1376        }
1377        property.value = default.clone();
1378        Some(Output::Reset { id, value: default })
1379    }
1380}
1381
1382#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1383enum Step {
1384    Next,
1385    Previous,
1386    First,
1387    Last,
1388}
1389
1390/// What a property grid keyboard shortcut does.
1391#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1392pub enum Action {
1393    Next,
1394    Previous,
1395    First,
1396    Last,
1397    Expand,
1398    Collapse,
1399    Edit,
1400    Cancel,
1401    Reset,
1402    CopyValue,
1403    FocusSearch,
1404}
1405
1406impl Action {
1407    /// The [`Event`] this action sends to `state`, or `None` when it does
1408    /// nothing, so the key is left for others.
1409    pub fn event<Id: Copy + Eq + Hash>(self, state: &State<Id>) -> Option<Event<Id>> {
1410        let number = state
1411            .editing()
1412            .and_then(|id| state.property(id))
1413            .is_some_and(|property| matches!(property.kind, Kind::Number(_)));
1414        let highlighted = state.highlighted();
1415        let property = state.highlighted_property();
1416        match self {
1417            Action::Next if number => Some(Event::Decrement),
1418            Action::Next => Some(Event::Next),
1419            Action::Previous if number => Some(Event::Increment),
1420            Action::Previous => Some(Event::Previous),
1421            Action::First => Some(Event::First),
1422            Action::Last => Some(Event::Last),
1423            Action::Expand => {
1424                matches!(highlighted, Some(Entry::Category(_))).then_some(Event::ExpandOrEnter)
1425            }
1426            Action::Collapse => {
1427                let leaves = property.is_some_and(|property| property.category.is_some())
1428                    && state.sort() == Sort::Categorised;
1429                (matches!(highlighted, Some(Entry::Category(_))) || leaves)
1430                    .then_some(Event::CollapseOrLeave)
1431            }
1432            Action::Edit => {
1433                let editable = property.is_some_and(Property::is_editable);
1434                (matches!(highlighted, Some(Entry::Category(_))) || editable)
1435                    .then_some(Event::EditHighlighted)
1436            }
1437            Action::Cancel => {
1438                (state.editing().is_some() || !state.query().is_empty()).then_some(Event::Cancel)
1439            }
1440            Action::Reset => property
1441                .filter(|property| state.can_reset(property.id))
1442                .map(|_| Event::ResetHighlighted),
1443            Action::CopyValue => property.map(|property| Event::CopyValue(property.id)),
1444            Action::FocusSearch => Some(Event::FocusSearch),
1445        }
1446    }
1447}
1448
1449impl keys::Action for Action {
1450    const ALL: &'static [Self] = &[
1451        Action::Next,
1452        Action::Previous,
1453        Action::First,
1454        Action::Last,
1455        Action::Expand,
1456        Action::Collapse,
1457        Action::Edit,
1458        Action::Cancel,
1459        Action::Reset,
1460        Action::CopyValue,
1461        Action::FocusSearch,
1462    ];
1463
1464    fn defaults() -> Keymap<Self> {
1465        default_keymap()
1466    }
1467
1468    fn name(self) -> &'static str {
1469        match self {
1470            Action::Next => "Next",
1471            Action::Previous => "Previous",
1472            Action::First => "First",
1473            Action::Last => "Last",
1474            Action::Expand => "Expand",
1475            Action::Collapse => "Collapse",
1476            Action::Edit => "Edit",
1477            Action::Cancel => "Cancel",
1478            Action::Reset => "Reset",
1479            Action::CopyValue => "CopyValue",
1480            Action::FocusSearch => "FocusSearch",
1481        }
1482    }
1483
1484    fn description(self) -> &'static str {
1485        match self {
1486            Action::Next => "Highlights the next row, or steps a number being edited down.",
1487            Action::Previous => "Highlights the previous row, or steps a number being edited up.",
1488            Action::First => "Highlights the first row.",
1489            Action::Last => "Highlights the last row.",
1490            Action::Expand => {
1491                "Opens the highlighted category, or moves to its first property when it is open."
1492            }
1493            Action::Collapse => {
1494                "Closes the highlighted category, or moves from a property to its category."
1495            }
1496            Action::Edit => {
1497                "Edits the highlighted property: types into text, flips a toggle or picks the next option."
1498            }
1499            Action::Cancel => "Cancels the edit and puts back the old value, or clears the filter.",
1500            Action::Reset => "Puts the highlighted property back to its default.",
1501            Action::CopyValue => "Copies the highlighted value as it is shown.",
1502            Action::FocusSearch => "Moves the keyboard to the filter box.",
1503        }
1504    }
1505}
1506
1507/// The default property grid shortcuts:
1508///
1509/// | Keys | Action |
1510/// | --- | --- |
1511/// | `ArrowDown` | [`Action::Next`] |
1512/// | `ArrowUp` | [`Action::Previous`] |
1513/// | `Home` | [`Action::First`] |
1514/// | `End` | [`Action::Last`] |
1515/// | `ArrowRight` | [`Action::Expand`] |
1516/// | `ArrowLeft` | [`Action::Collapse`] |
1517/// | `Enter`, `F2` | [`Action::Edit`] |
1518/// | `Escape` | [`Action::Cancel`] |
1519/// | `Delete` | [`Action::Reset`] |
1520/// | `Mod+C` (Ctrl, or Cmd on macOS) | [`Action::CopyValue`] |
1521/// | `Mod+F` | [`Action::FocusSearch`] |
1522///
1523/// While a field is being typed into it keeps the keys it uses, such as
1524/// the arrows that move its cursor; Enter ends the edit and Escape cancels
1525/// it. Up and Down step a number being edited. Escape in the filter box
1526/// clears it, and a second Escape leaves it. The grid resolves these
1527/// itself, only while it has focus, so they never clash with other
1528/// components. Pass a changed keymap with [`PropertyGrid::keymap`].
1529pub fn default_keymap() -> Keymap<Action> {
1530    Keymap::new()
1531        .bind(Chord::named(Named::ArrowDown), Action::Next)
1532        .bind(Chord::named(Named::ArrowUp), Action::Previous)
1533        .bind(Chord::named(Named::Home), Action::First)
1534        .bind(Chord::named(Named::End), Action::Last)
1535        .bind(Chord::named(Named::ArrowRight), Action::Expand)
1536        .bind(Chord::named(Named::ArrowLeft), Action::Collapse)
1537        .bind(Chord::named(Named::Enter), Action::Edit)
1538        .bind(Chord::named(Named::F2), Action::Edit)
1539        .bind(Chord::named(Named::Escape), Action::Cancel)
1540        .bind(Chord::named(Named::Delete), Action::Reset)
1541        .bind(Chord::character('c').command(), Action::CopyValue)
1542        .bind(Chord::character('f').command(), Action::FocusSearch)
1543}
1544
1545/// Focuses the filter box of a grid, for an app that moves the keyboard
1546/// there itself. [`Action::FocusSearch`] does the same from inside the
1547/// grid.
1548pub fn focus_search<Id, Message>(_state: &State<Id>) -> Task<Message> {
1549    widget::operation::focus(SEARCH)
1550}
1551
1552/// The text beside the search box: how many properties pass it, such as
1553/// "3 shown", "None" when nothing does, and empty without a search.
1554pub fn match_label<Id: Copy + Eq + Hash>(state: &State<Id>) -> String {
1555    let shown = state
1556        .properties()
1557        .iter()
1558        .filter(|property| state.passes(property))
1559        .count();
1560    shown_label(state.query(), shown)
1561}
1562
1563/// An entry of the context menu from [`menu`].
1564#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1565pub enum MenuItem {
1566    CopyValue,
1567    Reset,
1568}
1569
1570impl MenuItem {
1571    pub const ALL: [MenuItem; 2] = [MenuItem::CopyValue, MenuItem::Reset];
1572
1573    /// The event the entry sends for the property the menu opened on.
1574    pub fn event<Id>(self, id: Id) -> Event<Id> {
1575        match self {
1576            MenuItem::CopyValue => Event::CopyValue(id),
1577            MenuItem::Reset => Event::Reset(id),
1578        }
1579    }
1580
1581    /// Whether the entry would do anything for property `id`.
1582    pub fn is_enabled<Id: Copy + Eq + Hash>(self, state: &State<Id>, id: Id) -> bool {
1583        match self {
1584            MenuItem::CopyValue => state.property(id).is_some(),
1585            MenuItem::Reset => state.can_reset(id),
1586        }
1587    }
1588}
1589
1590/// A context menu to pass to [`PropertyGrid::context_menu`]: copy the
1591/// value, or reset it to its default. Turn the chosen entry into an event
1592/// with [`MenuItem::event`], and call [`sync_menu`] after each menu event
1593/// so entries that would do nothing are disabled.
1594pub fn menu<Id: Clone + PartialEq>() -> context_menu::State<MenuItem, Id> {
1595    context_menu::State::new([
1596        item(MenuItem::CopyValue, "Copy value").icon(crate::lucide!(Copy)),
1597        item(MenuItem::Reset, "Reset to default").icon(crate::lucide!(RotateCcw)),
1598    ])
1599}
1600
1601/// Disables the entries of a [`menu`] that would do nothing for the
1602/// property it is open on, such as "Reset to default" on a property that
1603/// already holds its default.
1604pub fn sync_menu<Id: Copy + Eq + Hash>(
1605    menu: &mut context_menu::State<MenuItem, Id>,
1606    state: &State<Id>,
1607) {
1608    let Some(&id) = menu.target() else {
1609        return;
1610    };
1611    for item in MenuItem::ALL {
1612        menu.set_disabled(item, !item.is_enabled(state, id));
1613    }
1614}
1615
1616/// How a row is shown.
1617#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1618pub struct Status {
1619    /// The row the keyboard acts on.
1620    pub highlighted: bool,
1621    /// The grid has keyboard focus, so the highlight carries a ring.
1622    pub focused: bool,
1623    pub hovered: bool,
1624    pub disabled: bool,
1625    /// A category heading rather than a property.
1626    pub heading: bool,
1627}
1628
1629/// The colours of a row.
1630#[derive(Debug, Clone, Copy, PartialEq)]
1631pub struct RowStyle {
1632    pub background: Option<Color>,
1633    pub text: Color,
1634    pub icon: Color,
1635    /// The ring around the highlighted row while the grid has focus.
1636    pub ring: Option<Color>,
1637}
1638
1639/// Resolves a row's colours. Headings sit on a muted band. The grid selects
1640/// nothing, so the highlighted row only carries a ring, and only while the
1641/// grid has focus.
1642pub fn row_style(tokens: &Tokens, status: Status) -> RowStyle {
1643    let hovered = status.hovered && !status.disabled;
1644    let background = match (hovered, status.heading) {
1645        (true, true) => Some(tokens.muted),
1646        (true, false) => Some(fade(tokens.accent, 0.6)),
1647        (false, true) => Some(fade(tokens.muted, 0.6)),
1648        (false, false) => None,
1649    };
1650    let text = if status.disabled {
1651        tokens.muted_foreground
1652    } else {
1653        tokens.foreground
1654    };
1655    RowStyle {
1656        background,
1657        text,
1658        icon: tokens.muted_foreground,
1659        ring: (status.highlighted && status.focused).then_some(tokens.ring),
1660    }
1661}
1662
1663/// The colour of a value: muted when it cannot be edited.
1664pub fn value_colour(tokens: &Tokens, editable: bool) -> Color {
1665    if editable {
1666        tokens.foreground
1667    } else {
1668        tokens.muted_foreground
1669    }
1670}
1671
1672/// The dot before the name of a property that differs from its default.
1673pub fn mark_colour(tokens: &Tokens) -> Color {
1674    tokens.primary
1675}
1676
1677/// The line between the name and value columns, brighter while dragged or
1678/// hovered.
1679pub fn divider_colour(tokens: &Tokens, active: bool) -> Color {
1680    if active {
1681        tokens.ring
1682    } else {
1683        fade(tokens.border, 0.7)
1684    }
1685}
1686
1687/// The border around the grid.
1688pub fn frame_style(tokens: &Tokens) -> container::Style {
1689    container::Style {
1690        border: Border {
1691            color: tokens.border,
1692            width: 1.0,
1693            radius: radius::MD.into(),
1694        },
1695        ..container::Style::default()
1696    }
1697}
1698
1699/// The ring around a colour swatch. Dark themes lift it towards the text
1700/// colour, so a near-black swatch still shows its edge.
1701pub fn swatch_ring(tokens: &Tokens) -> Color {
1702    if tokens.is_dark {
1703        mix(tokens.border, tokens.foreground, 0.4)
1704    } else {
1705        tokens.border
1706    }
1707}
1708
1709/// A colour swatch: the colour inside a thin ring.
1710pub fn swatch_style(tokens: &Tokens, colour: Color) -> container::Style {
1711    container::Style {
1712        background: Some(Background::Color(colour)),
1713        border: Border {
1714            color: swatch_ring(tokens),
1715            width: 1.0,
1716            radius: radius::SM.into(),
1717        },
1718        ..container::Style::default()
1719    }
1720}
1721
1722/// The name column's width in a row `width` pixels wide. A dragged or set
1723/// width is kept between [`MIN_NAME_WIDTH`] and what leaves the value
1724/// column [`MIN_VALUE_WIDTH`]. Otherwise names take [`NAME_WIDTH`], and in
1725/// a narrow grid the value column gives up its room first, down to
1726/// [`VALUE_WIDTH`], so names stay readable.
1727pub fn name_width(requested: Option<f32>, width: f32) -> f32 {
1728    let room = width - HANDLE_WIDTH - RESET_WIDTH;
1729    let max = (room - MIN_VALUE_WIDTH).max(MIN_NAME_WIDTH);
1730    let wanted = requested.unwrap_or_else(|| NAME_WIDTH.min(room - VALUE_WIDTH));
1731    wanted.clamp(MIN_NAME_WIDTH, max)
1732}
1733
1734type OnEvent<'a, Id, Message> = dyn Fn(Event<Id>) -> Message + 'a;
1735type Render<'a, Id, Message> = dyn Fn(&'a Property<Id>) -> Element<'a, Message> + 'a;
1736type BuildMenu<'a, Id, Message> =
1737    dyn FnOnce(Keymap<context_menu::Action>) -> RowMenu<'a, Id, Message> + 'a;
1738
1739/// A property grid builder. Convert it into an [`Element`] to render.
1740pub struct PropertyGrid<'a, Id, Message> {
1741    state: &'a State<Id>,
1742    customs: Vec<(Id, Box<Render<'a, Id, Message>>)>,
1743    toolbar: bool,
1744    description: bool,
1745    placeholder: String,
1746    width: Length,
1747    height: Length,
1748    keymap: Keymap<Action>,
1749    menu: Option<Box<BuildMenu<'a, Id, Message>>>,
1750    menu_keymap: Keymap<context_menu::Action>,
1751    on_event: Option<Box<OnEvent<'a, Id, Message>>>,
1752}
1753
1754/// Renders `state`. Without [`on_event`](PropertyGrid::on_event) every
1755/// editor renders disabled.
1756pub fn property_grid<Id, Message>(state: &State<Id>) -> PropertyGrid<'_, Id, Message> {
1757    PropertyGrid {
1758        state,
1759        customs: Vec::new(),
1760        toolbar: true,
1761        description: true,
1762        placeholder: "Filter".to_owned(),
1763        width: Length::Fill,
1764        height: Length::Shrink,
1765        keymap: default_keymap(),
1766        menu: None,
1767        menu_keymap: context_menu::default_keymap(),
1768        on_event: None,
1769    }
1770}
1771
1772impl<Id: fmt::Debug, Message> fmt::Debug for PropertyGrid<'_, Id, Message> {
1773    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1774        f.debug_struct("PropertyGrid")
1775            .field("state", self.state)
1776            .field("toolbar", &self.toolbar)
1777            .field("description", &self.description)
1778            .field("placeholder", &self.placeholder)
1779            .field("width", &self.width)
1780            .field("height", &self.height)
1781            .field("keymap", &self.keymap)
1782            .finish_non_exhaustive()
1783    }
1784}
1785
1786impl<'a, Id, Message> PropertyGrid<'a, Id, Message> {
1787    /// Shows the filter box and the sort buttons above the rows. On by
1788    /// default.
1789    pub fn toolbar(mut self, toolbar: bool) -> Self {
1790        self.toolbar = toolbar;
1791        self
1792    }
1793
1794    /// Shows the highlighted property's name and description under the
1795    /// rows. On by default. Errors show there even when it is off.
1796    pub fn description(mut self, description: bool) -> Self {
1797        self.description = description;
1798        self
1799    }
1800
1801    /// The filter box placeholder.
1802    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1803        self.placeholder = placeholder.into();
1804        self
1805    }
1806
1807    pub fn width(mut self, width: impl Into<Length>) -> Self {
1808        self.width = width.into();
1809        self
1810    }
1811
1812    /// A fixed or filling height scrolls the rows. Defaults to shrinking to
1813    /// fit them.
1814    pub fn height(mut self, height: impl Into<Length>) -> Self {
1815        self.height = height.into();
1816        self
1817    }
1818
1819    /// Replaces the [`default_keymap`] the focused grid resolves keys with.
1820    pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
1821        self.keymap = keymap;
1822        self
1823    }
1824
1825    pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
1826        self.on_event = Some(Box::new(on_event));
1827        self
1828    }
1829
1830    /// Draws the value of property `id` with `render`, such as a slider.
1831    /// Keep it within a row's height. The app handles its messages and
1832    /// stores the result with [`State::set`].
1833    pub fn custom(
1834        mut self,
1835        id: Id,
1836        render: impl Fn(&'a Property<Id>) -> Element<'a, Message> + 'a,
1837    ) -> Self
1838    where
1839        Id: PartialEq,
1840    {
1841        self.customs.retain(|(custom, _)| *custom != id);
1842        self.customs.push((id, Box::new(render)));
1843        self
1844    }
1845
1846    /// Gives every property row the context menu of `menu`, keyed by
1847    /// property, such as the one from [`menu()`]. It opens on a right-click
1848    /// or a long press, and on the highlighted property with its opening
1849    /// chord while the grid has focus.
1850    pub fn context_menu<MenuId>(
1851        mut self,
1852        menu: &'a context_menu::State<MenuId, Id>,
1853        on_event: impl Fn(context_menu::Event<MenuId, Id>) -> Message + 'a,
1854    ) -> Self
1855    where
1856        MenuId: Copy + PartialEq + 'a,
1857        Id: Clone + PartialEq + 'a,
1858        Message: Clone + 'a,
1859    {
1860        let on_event: Rc<dyn Fn(context_menu::Event<MenuId, Id>) -> Message + 'a> =
1861            Rc::new(on_event);
1862        self.menu = Some(Box::new(move |keymap| row_menu(menu, keymap, on_event)));
1863        self
1864    }
1865
1866    /// Replaces the context menu's default keymap.
1867    pub fn context_menu_keymap(mut self, keymap: Keymap<context_menu::Action>) -> Self {
1868        self.menu_keymap = keymap;
1869        self
1870    }
1871
1872    pub fn is_enabled(&self) -> bool {
1873        self.on_event.is_some()
1874    }
1875}
1876
1877impl<'a, Id, Message> From<PropertyGrid<'a, Id, Message>> for Element<'a, Message>
1878where
1879    Id: Copy + Eq + Hash + 'a,
1880    Message: Clone + 'a,
1881{
1882    fn from(grid: PropertyGrid<'a, Id, Message>) -> Self {
1883        let PropertyGrid {
1884            state,
1885            customs,
1886            toolbar,
1887            description,
1888            placeholder,
1889            width,
1890            height,
1891            keymap,
1892            menu,
1893            menu_keymap,
1894            on_event,
1895        } = grid;
1896        let menu = menu
1897            .filter(|_| on_event.is_some())
1898            .map(|build| build(menu_keymap));
1899        let view = Rc::new(View {
1900            state,
1901            customs,
1902            toolbar,
1903            description,
1904            placeholder,
1905            height,
1906            keymap,
1907            menu,
1908            entries: state.entries(),
1909            on_event: on_event.map(Rc::from),
1910        });
1911        responsive(move |size| view.body(size.width))
1912            .width(width)
1913            .height(height)
1914            .into()
1915    }
1916}
1917
1918/// Everything the grid's parts are built from.
1919struct View<'a, Id, Message> {
1920    state: &'a State<Id>,
1921    customs: Vec<(Id, Box<Render<'a, Id, Message>>)>,
1922    toolbar: bool,
1923    description: bool,
1924    placeholder: String,
1925    height: Length,
1926    keymap: Keymap<Action>,
1927    menu: Option<RowMenu<'a, Id, Message>>,
1928    entries: Vec<Entry<Id>>,
1929    on_event: Option<Rc<OnEvent<'a, Id, Message>>>,
1930}
1931
1932fn muted(theme: &Theme) -> Color {
1933    Tokens::of(theme).muted_foreground
1934}
1935
1936impl<'a, Id, Message> View<'a, Id, Message>
1937where
1938    Id: Copy + Eq + Hash + 'a,
1939    Message: Clone + 'a,
1940{
1941    fn send(&self, event: Event<Id>) -> Option<Message> {
1942        self.on_event.as_ref().map(|on_event| on_event(event))
1943    }
1944
1945    fn body(self: &Rc<Self>, width: f32) -> Element<'a, Message> {
1946        let state = self.state;
1947        // The frame's border and the rows' padding on both sides.
1948        let inner = width - 2.0 - space::SM * 2.0;
1949        let names = name_width(state.name_width(), inner);
1950
1951        let highlight = state
1952            .highlighted()
1953            .and_then(|highlighted| self.entries.iter().position(|entry| entry == highlighted));
1954        let list = {
1955            let view = Rc::clone(self);
1956            rows(self.entries.len(), ROW_HEIGHT, move |range| {
1957                view.lines(range, names)
1958            })
1959            .highlight(highlight)
1960            .height(if self.height == Length::Shrink {
1961                Length::Shrink
1962            } else {
1963                Length::Fill
1964            })
1965        };
1966
1967        let mut inside = column![list];
1968        if let Some(footer) = self.footer() {
1969            inside = inside.push(container(Space::new()).width(Length::Fill).height(1).style(
1970                |theme| container::Style {
1971                    background: Some(Background::Color(Tokens::of(theme).border)),
1972                    ..container::Style::default()
1973                },
1974            ));
1975            inside = inside.push(footer);
1976        }
1977        let mut inside: Element<'a, Message> = inside.into();
1978        if state.is_resizing()
1979            && let Some(on_event) = self.on_event.clone()
1980        {
1981            let end = on_event(Event::ResizeEnd);
1982            inside = mouse_area(inside)
1983                .on_move(move |point| {
1984                    on_event(Event::Resize(point.x - space::SM - HANDLE_WIDTH / 2.0))
1985                })
1986                .on_release(end.clone())
1987                .on_exit(end)
1988                .interaction(mouse::Interaction::ResizingHorizontally)
1989                .into();
1990        }
1991        let frame = container(inside)
1992            .padding(1)
1993            .width(Length::Fill)
1994            .height(if self.height == Length::Shrink {
1995                Length::Shrink
1996            } else {
1997                Length::Fill
1998            })
1999            .clip(true)
2000            .style(|theme| frame_style(&Tokens::of(theme)));
2001
2002        let mut parts = column![].spacing(space::SM);
2003        if self.toolbar {
2004            parts = parts.push(self.toolbar());
2005        }
2006        parts = parts.push(frame);
2007        let parts: Element<'a, Message> = parts.into();
2008
2009        let Some(on_event) = self.on_event.clone() else {
2010            return parts;
2011        };
2012        let request = state.focus_request().map(|(count, target)| match target {
2013            Target::Filter => (count, SEARCH, false),
2014            Target::Editor => (count, EDITOR_ID, true),
2015        });
2016        let focuser = Focuser {
2017            content: parts,
2018            request,
2019        };
2020        let cancel = state.editing().map(|_| on_event(Event::Cancel));
2021        let on_focus = on_event.clone();
2022        let keymap = self.keymap.clone();
2023        let view = Rc::clone(self);
2024        scope(focuser)
2025            .escape(EDITOR_ID, cancel)
2026            .on_focus(move |focused| on_focus(Event::Focus(focused)))
2027            .on_key(move |key| {
2028                let opened = view
2029                    .menu
2030                    .as_ref()
2031                    .zip(state.highlighted_property())
2032                    .and_then(|(menu, property)| menu.key(key, property.id));
2033                opened.or_else(|| state.key_event(&keymap, key).map(|event| on_event(event)))
2034            })
2035            .into()
2036    }
2037
2038    fn toolbar(&self) -> Element<'a, Message> {
2039        let state = self.state;
2040        let on_event = self.on_event.clone();
2041        let field = input(&self.placeholder, state.query())
2042            .id(SEARCH)
2043            .icon(crate::lucide!(Search))
2044            .size(input::Size::Sm)
2045            .width(Length::Fill)
2046            .on_input_maybe(on_event.map(|on_event| move |query| on_event(Event::Search(query))));
2047        let filter = search_keys(field, SEARCH).on_clear(
2048            self.send(Event::Search(String::new()))
2049                .filter(|_| !state.query().is_empty()),
2050        );
2051        let sort = |sort: Sort, glyph, label| {
2052            icon_button(glyph)
2053                .label(label)
2054                .size(button::Size::Sm)
2055                .pressed(state.sort() == sort)
2056                .on_press_maybe(self.send(Event::Sort(sort)))
2057        };
2058        row![
2059            filter,
2060            search_count(match_label(state)),
2061            sort(Sort::Categorised, crate::lucide!(ListTree), "Categorised"),
2062            sort(
2063                Sort::Alphabetical,
2064                crate::lucide!(ArrowDownAZ),
2065                "Alphabetical"
2066            ),
2067        ]
2068        .spacing(space::XS)
2069        .align_y(Alignment::Center)
2070        .into()
2071    }
2072
2073    /// The description area, or an error when the description is off.
2074    fn footer(&self) -> Option<Element<'a, Message>> {
2075        let state = self.state;
2076        let property = state.highlighted_property();
2077        let error = state
2078            .editing()
2079            .and_then(|id| state.error(id))
2080            .or_else(|| property.and_then(|property| state.error(property.id)));
2081        if !self.description && error.is_none() {
2082            return None;
2083        }
2084        let (title, body) = match state.highlighted() {
2085            Some(Entry::Property(_)) => (
2086                property
2087                    .map(|property| property.name.as_str())
2088                    .unwrap_or_default(),
2089                property.and_then(|property| property.description.as_deref()),
2090            ),
2091            Some(Entry::Category(name)) => (name.as_str(), None),
2092            None => ("", None),
2093        };
2094        let mut content = column![].spacing(2).width(Length::Fill);
2095        if self.description && title.is_empty() {
2096            content = content.push(widget::text(NOTHING_SELECTED).size(text_size::SM).style(
2097                |theme: &Theme| widget::text::Style {
2098                    color: Some(muted(theme)),
2099                },
2100            ));
2101        } else if self.description {
2102            content = content.push(ellipsis(title).font(theme::semibold()));
2103        }
2104        match (error, body) {
2105            (Some(error), _) => {
2106                content = content.push(widget::text(error.to_owned()).size(text_size::XS).style(
2107                    |theme: &Theme| widget::text::Style {
2108                        color: Some(Tokens::of(theme).destructive),
2109                    },
2110                ));
2111            }
2112            (None, Some(body)) if self.description => {
2113                content = content.push(widget::text(body).size(text_size::XS).style(
2114                    |theme: &Theme| widget::text::Style {
2115                        color: Some(muted(theme)),
2116                    },
2117                ));
2118            }
2119            _ => {}
2120        }
2121        let least = if self.description {
2122            DESCRIPTION_HEIGHT
2123        } else {
2124            DESCRIPTION_HEIGHT / 2.0
2125        };
2126        // The strut keeps the least height; longer text grows the area.
2127        let strut = Space::new().height(least - space::SM * 2.0);
2128        Some(
2129            container(row![content, strut])
2130                .padding([space::SM, space::MD])
2131                .width(Length::Fill)
2132                .into(),
2133        )
2134    }
2135
2136    fn lines(&self, range: Range<usize>, names: f32) -> Element<'a, Message> {
2137        let entries = self.entries.get(range).unwrap_or_default();
2138        column(entries.iter().map(|entry| match entry {
2139            Entry::Category(name) => self.heading(name),
2140            Entry::Property(id) => {
2141                let row = self.property(*id, names);
2142                match &self.menu {
2143                    Some(menu) => menu.wrap(*id, row),
2144                    None => row,
2145                }
2146            }
2147        }))
2148        .into()
2149    }
2150
2151    fn status(&self, entry: &Entry<Id>, disabled: bool) -> Status {
2152        let state = self.state;
2153        let target = self
2154            .menu
2155            .as_ref()
2156            .and_then(RowMenu::target)
2157            .map(Entry::Property);
2158        let menu_open_here = target.as_ref() == Some(entry);
2159        Status {
2160            highlighted: ringed(target.as_ref(), entry, state.highlighted() == Some(entry)),
2161            focused: state.is_focused() || menu_open_here,
2162            hovered: false,
2163            disabled,
2164            heading: matches!(entry, Entry::Category(_)),
2165        }
2166    }
2167
2168    fn heading(&self, name: &str) -> Element<'a, Message> {
2169        let state = self.state;
2170        let entry = Entry::Category(name.to_owned());
2171        let status = self.status(&entry, self.on_event.is_none());
2172        let enabled = self.on_event.is_some();
2173        let glyph = if state.is_expanded(name) {
2174            crate::lucide!(ChevronDown)
2175        } else {
2176            crate::lucide!(ChevronRight)
2177        };
2178        let colours = move |theme: &Theme| row_style(&Tokens::of(theme), status);
2179        let count = state.count(name);
2180        let body = row![
2181            themed(glyph, ICON_SIZE, opacity(enabled), move |theme| colours(
2182                theme
2183            )
2184            .icon),
2185            ellipsis(name.to_owned())
2186                .font(theme::semibold())
2187                .colour(move |theme| colours(theme).text),
2188            widget::text(count.to_string())
2189                .size(text_size::XS)
2190                .wrapping(widget::text::Wrapping::None)
2191                .style(|theme: &Theme| widget::text::Style {
2192                    color: Some(muted(theme)),
2193                }),
2194        ]
2195        .spacing(space::SM)
2196        .align_y(Alignment::Center)
2197        .padding(Padding::ZERO.left(space::SM).right(space::SM))
2198        .height(ROW_HEIGHT);
2199        let row = pressable(body, move |theme, hovered| {
2200            look(row_style(&Tokens::of(theme), Status { hovered, ..status }))
2201        });
2202        match self.send(Event::Press(entry)) {
2203            Some(message) => row.on_press(move |_| message.clone()).into(),
2204            None => row.into(),
2205        }
2206    }
2207
2208    fn property(&self, id: Id, names: f32) -> Element<'a, Message> {
2209        let state = self.state;
2210        let Some(property) = state.property(id) else {
2211            return Space::new().height(ROW_HEIGHT).into();
2212        };
2213        let entry = Entry::Property(id);
2214        let on_event = self.on_event.clone().filter(|_| !property.disabled);
2215        let enabled = on_event.is_some();
2216        let status = self.status(&entry, !enabled);
2217        let colours = move |theme: &Theme| row_style(&Tokens::of(theme), status);
2218        let modified = property.is_modified();
2219
2220        let mark: Element<'a, Message> = if modified {
2221            container(
2222                container(Space::new())
2223                    .width(MARK_SIZE)
2224                    .height(MARK_SIZE)
2225                    .style(|theme| container::Style {
2226                        background: Some(Background::Color(mark_colour(&Tokens::of(theme)))),
2227                        border: Border {
2228                            radius: radius::FULL.into(),
2229                            ..Border::default()
2230                        },
2231                        ..container::Style::default()
2232                    }),
2233            )
2234            .width(MARK_WIDTH)
2235            .into()
2236        } else {
2237            Space::new().width(MARK_WIDTH).into()
2238        };
2239        let name = ellipsis(property.name.as_str())
2240            .font(if modified {
2241                theme::semibold()
2242            } else {
2243                theme::font()
2244            })
2245            .colour(move |theme| colours(theme).text);
2246        let name = container(row![mark, name].align_y(Alignment::Center))
2247            .width(names)
2248            .height(Length::Fill)
2249            .align_y(Alignment::Center);
2250
2251        let handle = self.handle();
2252        let value = container(self.value(property))
2253            .width(Length::Fill)
2254            .height(Length::Fill)
2255            .align_y(Alignment::Center);
2256
2257        let reset: Element<'a, Message> = match &on_event {
2258            Some(on_event) if state.can_reset(id) => {
2259                let icon = themed(crate::lucide!(RotateCcw), RESET_ICON_SIZE, 1.0, muted);
2260                let button = mouse_area(container(icon).center(RESET_WIDTH))
2261                    .on_press(on_event(Event::Reset(id)))
2262                    .interaction(mouse::Interaction::Pointer);
2263                tooltip(button, "Reset to default").into()
2264            }
2265            _ => Space::new().width(RESET_WIDTH).into(),
2266        };
2267
2268        let body = row![name, handle, value, reset]
2269            .align_y(Alignment::Center)
2270            .padding(Padding::ZERO.left(space::SM).right(space::SM))
2271            .height(ROW_HEIGHT);
2272        let row = pressable(body, move |theme, hovered| {
2273            look(row_style(&Tokens::of(theme), Status { hovered, ..status }))
2274        });
2275        let Some(on_event) = self.on_event.clone() else {
2276            return row.into();
2277        };
2278        let press = on_event(Event::Press(entry));
2279        let edit = property.is_editable().then(|| on_event(Event::Edit(id)));
2280        row.on_press(move |_| press.clone())
2281            .on_double_click(edit)
2282            .into()
2283    }
2284
2285    /// The line between the columns, which drags to resize them.
2286    fn handle(&self) -> Element<'a, Message> {
2287        let resizing = self.state.is_resizing();
2288        let line = container(container(Space::new()).width(1).height(Length::Fill).style(
2289            move |theme| container::Style {
2290                background: Some(Background::Color(divider_colour(
2291                    &Tokens::of(theme),
2292                    resizing,
2293                ))),
2294                ..container::Style::default()
2295            },
2296        ))
2297        .center_x(HANDLE_WIDTH)
2298        .height(Length::Fill);
2299        match self.send(Event::ResizeStart) {
2300            Some(start) => mouse_area(line)
2301                .on_press(start)
2302                .interaction(mouse::Interaction::ResizingHorizontally)
2303                .into(),
2304            None => line.into(),
2305        }
2306    }
2307
2308    fn value(&self, property: &'a Property<Id>) -> Element<'a, Message> {
2309        let state = self.state;
2310        let id = property.id;
2311        if let Some((_, render)) = self.customs.iter().find(|(custom, _)| *custom == id) {
2312            return render(property);
2313        }
2314        let on_event = self.on_event.clone().filter(|_| !property.disabled);
2315        let editable = on_event.is_some() && property.is_editable();
2316
2317        match (&property.kind, &property.value) {
2318            (Kind::Toggle(toggle), Value::Bool(on)) => {
2319                let set =
2320                    on_event.map(|on_event| move |on| on_event(Event::Set(id, Value::Bool(on))));
2321                let control: Element<'a, Message> = match toggle {
2322                    Toggle::Checkbox => checkbox(*on).on_toggle_maybe(set).into(),
2323                    Toggle::Switch => switch(*on).on_toggle_maybe(set).into(),
2324                };
2325                container(control)
2326                    .padding(Padding::ZERO.left(space::SM))
2327                    .into()
2328            }
2329            (Kind::Choice(options), Value::Text(current)) => {
2330                select(options.as_slice(), Some(current.clone()))
2331                    .width(Length::Fill)
2332                    .on_select_maybe(on_event.map(|on_event| {
2333                        move |option: String| on_event(Event::Set(id, Value::Text(option)))
2334                    }))
2335                    .into()
2336            }
2337            _ if property.is_typed() && editable => match state.draft(id) {
2338                Some(draft) => self.field(property, draft),
2339                None => self.shown(property, true),
2340            },
2341            _ => self.shown(property, editable),
2342        }
2343    }
2344
2345    /// A value shown as text, which starts an edit when pressed.
2346    fn shown(&self, property: &'a Property<Id>, editable: bool) -> Element<'a, Message> {
2347        let label = ellipsis(property.display())
2348            .colour(move |theme| value_colour(&Tokens::of(theme), editable));
2349        let content: Element<'a, Message> = match property.value {
2350            Value::Colour(colour) => row![swatch(colour), label]
2351                .spacing(space::SM)
2352                .align_y(Alignment::Center)
2353                .into(),
2354            _ => label.into(),
2355        };
2356        let content = container(content)
2357            .padding(Padding::ZERO.left(space::SM))
2358            .width(Length::Fill)
2359            .height(Length::Fill)
2360            .align_y(Alignment::Center);
2361        match self
2362            .send(Event::Edit(property.id))
2363            .filter(|_| editable && property.is_typed())
2364        {
2365            Some(edit) => mouse_area(content)
2366                .on_press(edit)
2367                .interaction(mouse::Interaction::Text)
2368                .into(),
2369            None => content.into(),
2370        }
2371    }
2372
2373    /// The text field of a property being edited, or left while invalid.
2374    fn field(&self, property: &'a Property<Id>, draft: &'a Draft) -> Element<'a, Message> {
2375        let id = property.id;
2376        let mut field = input("", &draft.text)
2377            .size(input::Size::Sm)
2378            .invalid(draft.error.is_some())
2379            .on_input_maybe(
2380                self.on_event
2381                    .clone()
2382                    .map(|on_event| move |text| on_event(Event::Input(id, text))),
2383            )
2384            .on_submit_maybe(self.send(Event::Submit));
2385        if self.state.editing() == Some(id) {
2386            field = field.id(EDITOR_ID);
2387        }
2388        let Value::Colour(current) = property.value else {
2389            return field.into();
2390        };
2391        let colour = parse_hex(&draft.text).unwrap_or(current);
2392        row![swatch(colour), field]
2393            .spacing(space::SM)
2394            .align_y(Alignment::Center)
2395            .padding(Padding::ZERO.left(space::SM))
2396            .into()
2397    }
2398}
2399
2400fn swatch<'a, Message: 'a>(colour: Color) -> Element<'a, Message> {
2401    container(Space::new())
2402        .width(SWATCH_SIZE)
2403        .height(SWATCH_SIZE)
2404        .style(move |theme| swatch_style(&Tokens::of(theme), colour))
2405        .into()
2406}
2407
2408fn look(style: RowStyle) -> Look {
2409    Look {
2410        background: style.background,
2411        border: Border {
2412            color: style.ring.unwrap_or(Color::TRANSPARENT),
2413            width: if style.ring.is_some() { 1.0 } else { 0.0 },
2414            radius: radius::SM.into(),
2415        },
2416        divider: None,
2417    }
2418}
2419
2420/// Focuses a field inside the grid once for each request the state makes,
2421/// selecting its text when asked.
2422struct Focuser<'a, Message> {
2423    content: Element<'a, Message>,
2424    request: Option<(u64, widget::Id, bool)>,
2425}
2426
2427/// The last focus request acted on.
2428#[derive(Debug, Default)]
2429struct Handled(u64);
2430
2431impl<Message: Clone> Widget<Message, Theme, Renderer> for Focuser<'_, Message> {
2432    fn tag(&self) -> tree::Tag {
2433        tree::Tag::of::<Handled>()
2434    }
2435
2436    fn state(&self) -> tree::State {
2437        tree::State::new(Handled::default())
2438    }
2439
2440    fn children(&self) -> Vec<Tree> {
2441        vec![Tree::new(&self.content)]
2442    }
2443
2444    fn diff(&self, tree: &mut Tree) {
2445        tree.diff_children(std::slice::from_ref(&self.content));
2446    }
2447
2448    fn size(&self) -> Size<Length> {
2449        self.content.as_widget().size()
2450    }
2451
2452    fn size_hint(&self) -> Size<Length> {
2453        self.content.as_widget().size_hint()
2454    }
2455
2456    fn layout(
2457        &mut self,
2458        tree: &mut Tree,
2459        renderer: &Renderer,
2460        limits: &layout::Limits,
2461    ) -> layout::Node {
2462        let Tree {
2463            state, children, ..
2464        } = tree;
2465        let Some(child) = children.first_mut() else {
2466            return layout::Node::new(Size::ZERO);
2467        };
2468        let node = self.content.as_widget_mut().layout(child, renderer, limits);
2469        let handled = state.downcast_mut::<Handled>();
2470        // The field may only exist from this layout on, so focus it here,
2471        // once the new rows have been built.
2472        if let Some((count, id, select_all)) = &self.request
2473            && handled.0 != *count
2474        {
2475            handled.0 = *count;
2476            let layout = Layout::new(&node);
2477            let mut focus = operation::focusable::focus(id.clone());
2478            self.content
2479                .as_widget_mut()
2480                .operate(child, layout, renderer, &mut focus);
2481            if *select_all {
2482                let mut select = operation::text_input::select_all(id.clone());
2483                self.content
2484                    .as_widget_mut()
2485                    .operate(child, layout, renderer, &mut select);
2486            }
2487        }
2488        node
2489    }
2490
2491    fn draw(
2492        &self,
2493        tree: &Tree,
2494        renderer: &mut Renderer,
2495        theme: &Theme,
2496        style: &renderer::Style,
2497        layout: Layout<'_>,
2498        cursor: mouse::Cursor,
2499        viewport: &Rectangle,
2500    ) {
2501        if let Some(child) = tree.children.first() {
2502            self.content
2503                .as_widget()
2504                .draw(child, renderer, theme, style, layout, cursor, viewport);
2505        }
2506    }
2507
2508    fn operate(
2509        &mut self,
2510        tree: &mut Tree,
2511        layout: Layout<'_>,
2512        renderer: &Renderer,
2513        operation: &mut dyn Operation,
2514    ) {
2515        if let Some(child) = tree.children.first_mut() {
2516            self.content
2517                .as_widget_mut()
2518                .operate(child, layout, renderer, operation);
2519        }
2520    }
2521
2522    fn update(
2523        &mut self,
2524        tree: &mut Tree,
2525        event: &iced::Event,
2526        layout: Layout<'_>,
2527        cursor: mouse::Cursor,
2528        renderer: &Renderer,
2529        clipboard: &mut dyn Clipboard,
2530        shell: &mut Shell<'_, Message>,
2531        viewport: &Rectangle,
2532    ) {
2533        let Some(child) = tree.children.first_mut() else {
2534            return;
2535        };
2536        self.content.as_widget_mut().update(
2537            child, event, layout, cursor, renderer, clipboard, shell, viewport,
2538        );
2539    }
2540
2541    fn mouse_interaction(
2542        &self,
2543        tree: &Tree,
2544        layout: Layout<'_>,
2545        cursor: mouse::Cursor,
2546        viewport: &Rectangle,
2547        renderer: &Renderer,
2548    ) -> mouse::Interaction {
2549        tree.children
2550            .first()
2551            .map_or(mouse::Interaction::None, |child| {
2552                self.content
2553                    .as_widget()
2554                    .mouse_interaction(child, layout, cursor, viewport, renderer)
2555            })
2556    }
2557
2558    fn overlay<'b>(
2559        &'b mut self,
2560        tree: &'b mut Tree,
2561        layout: Layout<'b>,
2562        renderer: &Renderer,
2563        viewport: &Rectangle,
2564        translation: Vector,
2565    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
2566        let child = tree.children.first_mut()?;
2567        self.content
2568            .as_widget_mut()
2569            .overlay(child, layout, renderer, viewport, translation)
2570    }
2571}
2572
2573impl<'a, Message: Clone + 'a> From<Focuser<'a, Message>> for Element<'a, Message> {
2574    fn from(focuser: Focuser<'a, Message>) -> Self {
2575        Element::new(focuser)
2576    }
2577}
2578
2579#[cfg(test)]
2580mod tests {
2581    use iced::keyboard::{Key, Modifiers};
2582
2583    use super::*;
2584    use crate::theme::{dark, light};
2585
2586    fn even(text: &str) -> Result<(), String> {
2587        if text.len().is_multiple_of(2) {
2588            Ok(())
2589        } else {
2590            Err("Needs an even length".to_owned())
2591        }
2592    }
2593
2594    /// ```text
2595    /// 1 Id (read-only, no category)
2596    /// Layout: 2 Width, 3 Height, 4 Visible
2597    /// Appearance: 5 Fill, 6 Blend, 7 Label, 8 Locked (disabled)
2598    /// ```
2599    fn sample() -> State<u32> {
2600        State::new([
2601            read_only(1, "Id", "rect-1"),
2602            number(2, "Width", 120.0)
2603                .category("Layout")
2604                .range(0.0, 400.0)
2605                .step(1.0)
2606                .unit("px")
2607                .default(100.0),
2608            number(3, "Height", 0.5)
2609                .category("Layout")
2610                .range(0.0, 1.0)
2611                .step(0.25),
2612            toggle(4, "Visible", true).category("Layout").default(true),
2613            colour(5, "Fill", Color::from_rgb8(0x3b, 0x82, 0xf6))
2614                .category("Appearance")
2615                .default(Color::WHITE),
2616            choice(6, "Blend", ["Normal", "Multiply", "Screen"], "Normal")
2617                .category("Appearance")
2618                .description("How the layer mixes with what is under it."),
2619            text(7, "Label", "Box")
2620                .category("Appearance")
2621                .validate(even),
2622            toggle(8, "Locked", false)
2623                .category("Appearance")
2624                .disabled(true)
2625                .default(true),
2626        ])
2627    }
2628
2629    fn property_entries(ids: &[u32]) -> Vec<Entry<u32>> {
2630        ids.iter().map(|&id| Entry::Property(id)).collect()
2631    }
2632
2633    fn category(name: &str) -> Entry<u32> {
2634        Entry::Category(name.to_owned())
2635    }
2636
2637    fn key(key: Key, modifiers: Modifiers) -> keys::Event {
2638        keys::Event { key, modifiers }
2639    }
2640
2641    fn named(named: Named) -> keys::Event {
2642        key(Key::Named(named), Modifiers::empty())
2643    }
2644
2645    fn changed(id: u32, value: impl Into<Value>) -> Option<Output<u32>> {
2646        Some(Output::Changed {
2647            id,
2648            value: value.into(),
2649        })
2650    }
2651
2652    #[test]
2653    fn lists_uncategorised_properties_first_then_each_category() {
2654        let state = sample();
2655        assert_eq!(state.categories(), ["Layout", "Appearance"]);
2656        let mut expected = vec![Entry::Property(1), category("Layout")];
2657        expected.extend(property_entries(&[2, 3, 4]));
2658        expected.push(category("Appearance"));
2659        expected.extend(property_entries(&[5, 6, 7, 8]));
2660        assert_eq!(state.entries(), expected);
2661        assert_eq!(state.count("Layout"), 3);
2662    }
2663
2664    #[test]
2665    fn alphabetical_lists_every_property_by_name() {
2666        let state = sample().with_sort(Sort::Alphabetical);
2667        assert_eq!(state.entries(), property_entries(&[6, 5, 3, 1, 7, 8, 4, 2]));
2668        assert_eq!(Sort::ALL.len(), 2);
2669        assert_eq!(Sort::default(), Sort::Categorised);
2670    }
2671
2672    #[test]
2673    fn a_repeated_id_replaces_the_earlier_property() {
2674        let state = State::new([text(1, "Old", "a"), text(1, "New", "b")]);
2675        assert_eq!(state.properties().len(), 1);
2676        assert_eq!(state.property(1).map(|p| p.name.as_str()), Some("New"));
2677        assert!(State::<u8>::new([]).entries().is_empty());
2678    }
2679
2680    #[test]
2681    fn the_filter_matches_names_and_drops_empty_categories() {
2682        let mut state = sample();
2683        let _ = state.update(Event::Press(Entry::Property(2)));
2684        let _ = state.update(Event::Search("FI".into()));
2685        assert_eq!(
2686            state.entries(),
2687            vec![category("Appearance"), Entry::Property(5)]
2688        );
2689        assert!(state.matches(5) && !state.matches(2));
2690        assert_eq!(state.count("Layout"), 0);
2691        assert_eq!(
2692            state.highlighted(),
2693            Some(&Entry::Property(5)),
2694            "a hidden highlight moves to the nearest match after it"
2695        );
2696        let _ = state.update(Event::Search("nothing".into()));
2697        assert!(state.entries().is_empty());
2698        assert_eq!(state.highlighted(), None);
2699    }
2700
2701    #[test]
2702    fn searching_keeps_the_highlight_on_the_nearest_shown_property() {
2703        let mut state = sample();
2704        let _ = state.update(Event::Press(Entry::Property(6)));
2705        let _ = state.update(Event::Search("ab".into()));
2706        assert_eq!(
2707            state.highlighted(),
2708            Some(&Entry::Property(7)),
2709            "Label follows Blend"
2710        );
2711        let _ = state.update(Event::Search("la".into()));
2712        assert_eq!(state.highlighted(), Some(&Entry::Property(7)), "stays put");
2713        let _ = state.update(Event::Search("wid".into()));
2714        assert_eq!(
2715            state.highlighted(),
2716            Some(&Entry::Property(2)),
2717            "the last before it when nothing follows"
2718        );
2719
2720        let mut fresh = sample();
2721        let _ = fresh.update(Event::Search("e".into()));
2722        assert_eq!(
2723            fresh.highlighted(),
2724            Some(&Entry::Property(3)),
2725            "typing highlights the first shown property"
2726        );
2727    }
2728
2729    #[test]
2730    fn pressing_a_heading_toggles_it_and_collapsing_lifts_the_highlight() {
2731        let mut state = sample();
2732        let _ = state.update(Event::Press(Entry::Property(3)));
2733        let _ = state.update(Event::Collapse("Layout".into()));
2734        assert!(!state.is_expanded("Layout"));
2735        assert_eq!(state.highlighted(), Some(&category("Layout")));
2736        assert!(!state.entries().contains(&Entry::Property(3)));
2737        let _ = state.update(Event::Press(category("Layout")));
2738        assert!(state.is_expanded("Layout"));
2739        let _ = state.update(Event::ToggleCategory("Layout".into()));
2740        assert!(!state.is_expanded("Layout"));
2741        let _ = state.update(Event::Expand("Layout".into()));
2742        assert!(state.is_expanded("Layout"));
2743        let _ = state.update(Event::Collapse("Missing".into()));
2744        assert!(
2745            state.is_expanded("Missing"),
2746            "unknown categories are ignored"
2747        );
2748    }
2749
2750    #[test]
2751    fn with_collapsed_starts_closed() {
2752        let state = sample().with_collapsed(["Appearance"]);
2753        assert!(!state.is_expanded("Appearance"));
2754        assert_eq!(state.entries().last(), Some(&category("Appearance")));
2755    }
2756
2757    #[test]
2758    fn arrows_walk_the_rows_without_wrapping() {
2759        let mut state = sample();
2760        let _ = state.update(Event::Next);
2761        assert_eq!(state.highlighted(), Some(&Entry::Property(1)));
2762        let _ = state.update(Event::Next);
2763        assert_eq!(state.highlighted(), Some(&category("Layout")));
2764        let _ = state.update(Event::Last);
2765        assert_eq!(state.highlighted(), Some(&Entry::Property(8)));
2766        let _ = state.update(Event::Next);
2767        assert_eq!(state.highlighted(), Some(&Entry::Property(8)));
2768        let _ = state.update(Event::First);
2769        let _ = state.update(Event::Previous);
2770        assert_eq!(state.highlighted(), Some(&Entry::Property(1)));
2771    }
2772
2773    #[test]
2774    fn right_opens_then_enters_and_left_closes_then_leaves() {
2775        let mut state = sample().with_collapsed(["Layout"]);
2776        let _ = state.update(Event::Press(category("Layout")));
2777        assert!(state.is_expanded("Layout"));
2778        let _ = state.update(Event::CollapseOrLeave);
2779        assert!(!state.is_expanded("Layout"));
2780        let _ = state.update(Event::ExpandOrEnter);
2781        assert!(state.is_expanded("Layout"));
2782        let _ = state.update(Event::ExpandOrEnter);
2783        assert_eq!(state.highlighted(), Some(&Entry::Property(2)));
2784        let _ = state.update(Event::CollapseOrLeave);
2785        assert_eq!(state.highlighted(), Some(&category("Layout")));
2786        assert!(state.is_expanded("Layout"), "leaving does not close");
2787    }
2788
2789    #[test]
2790    fn editing_text_starts_a_draft_and_asks_for_focus() {
2791        let mut state = sample();
2792        assert!(state.update(Event::Edit(2)).is_none());
2793        assert_eq!(state.editing(), Some(2));
2794        assert_eq!(state.draft(2).map(|d| d.text.as_str()), Some("120"));
2795        assert_eq!(state.highlighted(), Some(&Entry::Property(2)));
2796        assert_eq!(state.focus_request(), Some((1, Target::Editor)));
2797        let _ = state.update(Event::Edit(2));
2798        assert_eq!(state.focus_request(), Some((2, Target::Editor)));
2799    }
2800
2801    #[test]
2802    fn a_valid_edit_changes_the_value_as_it_is_typed() {
2803        let mut state = sample();
2804        let _ = state.update(Event::Edit(2));
2805        assert_eq!(state.update(Event::Input(2, "12".into())), changed(2, 12.0));
2806        assert_eq!(state.update(Event::Input(2, "12px".into())), None);
2807        assert_eq!(state.value(2), Some(&Value::Number(12.0)));
2808        let _ = state.update(Event::Submit);
2809        assert_eq!(state.editing(), None);
2810        assert!(state.draft(2).is_none());
2811    }
2812
2813    #[test]
2814    fn an_invalid_edit_keeps_its_draft_and_error() {
2815        let mut state = sample();
2816        let _ = state.update(Event::Edit(2));
2817        assert_eq!(state.update(Event::Input(2, "9000".into())), None);
2818        assert_eq!(state.error(2), Some("Must be at most 400px"));
2819        assert_eq!(state.value(2), Some(&Value::Number(120.0)));
2820        let _ = state.update(Event::Submit);
2821        assert_eq!(
2822            state.editing(),
2823            Some(2),
2824            "Enter does not leave an invalid edit"
2825        );
2826
2827        let _ = state.update(Event::Press(Entry::Property(3)));
2828        assert_eq!(state.editing(), None);
2829        assert_eq!(
2830            state.draft(2).map(|d| d.text.as_str()),
2831            Some("9000"),
2832            "the invalid draft stays after leaving"
2833        );
2834        assert_eq!(state.update(Event::Input(2, "40".into())), changed(2, 40.0));
2835        assert_eq!(state.editing(), Some(2), "typing again resumes the edit");
2836        assert_eq!(state.error(2), None);
2837    }
2838
2839    #[test]
2840    fn leaving_a_valid_edit_drops_its_draft() {
2841        let mut state = sample();
2842        let _ = state.update(Event::Edit(7));
2843        let _ = state.update(Event::Input(7, "Card".into()));
2844        let _ = state.update(Event::Focus(false));
2845        assert_eq!(state.editing(), None);
2846        assert!(state.draft(7).is_none());
2847        assert_eq!(state.value(7), Some(&Value::from("Card")));
2848    }
2849
2850    #[test]
2851    fn cancel_puts_back_the_value_from_before_the_edit() {
2852        let mut state = sample();
2853        let _ = state.update(Event::Edit(7));
2854        let _ = state.update(Event::Input(7, "Card".into()));
2855        let _ = state.update(Event::Input(7, "Car".into()));
2856        assert_eq!(state.error(7), Some("Needs an even length"));
2857        assert_eq!(state.update(Event::Cancel), changed(7, "Box"));
2858        assert_eq!(state.editing(), None);
2859        assert!(state.draft(7).is_none());
2860
2861        let _ = state.update(Event::Edit(7));
2862        assert_eq!(state.update(Event::Cancel), None, "nothing changed");
2863    }
2864
2865    #[test]
2866    fn cancel_without_an_edit_clears_the_search() {
2867        let mut state = sample();
2868        let _ = state.update(Event::Search("wid".into()));
2869        let _ = state.update(Event::Cancel);
2870        assert_eq!(state.query(), "");
2871    }
2872
2873    #[test]
2874    fn edit_flips_toggles_and_steps_choices() {
2875        let mut state = sample();
2876        assert_eq!(state.update(Event::Edit(4)), changed(4, false));
2877        assert_eq!(state.editing(), None);
2878        assert_eq!(state.update(Event::Edit(6)), changed(6, "Multiply"));
2879        let _ = state.update(Event::Edit(6));
2880        assert_eq!(
2881            state.update(Event::Edit(6)),
2882            changed(6, "Normal"),
2883            "wraps round"
2884        );
2885    }
2886
2887    #[test]
2888    fn read_only_and_disabled_properties_do_not_edit() {
2889        let mut state = sample();
2890        assert_eq!(state.update(Event::Edit(1)), None);
2891        assert_eq!(state.editing(), None);
2892        assert_eq!(state.update(Event::Edit(8)), None);
2893        assert_eq!(state.update(Event::Set(8, Value::Bool(true))), None);
2894        assert_eq!(state.update(Event::Input(8, "x".into())), None);
2895        assert_eq!(state.update(Event::Reset(8)), None);
2896        assert_eq!(state.highlighted(), Some(&Entry::Property(8)));
2897    }
2898
2899    #[test]
2900    fn set_accepts_only_values_that_suit_the_property() {
2901        let mut state = sample();
2902        assert_eq!(
2903            state.update(Event::Set(6, "Screen".into())),
2904            changed(6, "Screen")
2905        );
2906        assert_eq!(state.update(Event::Set(6, "Overlay".into())), None);
2907        assert_eq!(state.update(Event::Set(6, Value::Bool(true))), None);
2908        assert_eq!(
2909            state.update(Event::Set(4, Value::Bool(true))),
2910            None,
2911            "unchanged"
2912        );
2913        assert_eq!(state.update(Event::Set(99, Value::Bool(true))), None);
2914    }
2915
2916    #[test]
2917    fn the_menu_disables_reset_on_a_property_at_its_default() {
2918        let disabled = |menu: &context_menu::State<MenuItem, u32>, item| {
2919            menu.menu().item(item).is_some_and(|item| item.disabled)
2920        };
2921        let mut state = sample();
2922        let mut menu = menu();
2923        let _ = menu.update(context_menu::Event::OpenFromKeyboard(2));
2924        sync_menu(&mut menu, &state);
2925        assert!(state.can_reset(2));
2926        assert!(!disabled(&menu, MenuItem::Reset));
2927        assert!(!disabled(&menu, MenuItem::CopyValue));
2928
2929        let _ = state.update(Event::Reset(2));
2930        sync_menu(&mut menu, &state);
2931        assert!(!state.can_reset(2));
2932        assert!(disabled(&menu, MenuItem::Reset));
2933        assert!(!disabled(&menu, MenuItem::CopyValue));
2934        assert!(!state.can_reset(8), "disabled");
2935    }
2936
2937    #[test]
2938    fn reset_puts_back_the_default() {
2939        let mut state = sample();
2940        assert!(state.property(2).is_some_and(Property::is_modified));
2941        assert_eq!(
2942            state.update(Event::Reset(2)),
2943            Some(Output::Reset {
2944                id: 2,
2945                value: Value::Number(100.0)
2946            })
2947        );
2948        assert!(!state.property(2).is_some_and(Property::is_modified));
2949        assert_eq!(state.update(Event::Reset(2)), None, "already the default");
2950        assert_eq!(state.update(Event::Reset(3)), None, "no default");
2951    }
2952
2953    #[test]
2954    fn reset_drops_an_invalid_draft() {
2955        let mut state = sample();
2956        let _ = state.update(Event::Edit(2));
2957        let _ = state.update(Event::Input(2, "abc".into()));
2958        let _ = state.update(Event::Press(Entry::Property(2)));
2959        let _ = state.update(Event::ResetHighlighted);
2960        assert!(state.draft(2).is_none());
2961        assert_eq!(state.editing(), None);
2962        assert_eq!(state.value(2), Some(&Value::Number(100.0)));
2963    }
2964
2965    #[test]
2966    fn up_and_down_step_the_number_being_edited() {
2967        let mut state = sample();
2968        let _ = state.update(Event::Edit(3));
2969        assert_eq!(state.update(Event::Increment), changed(3, 0.75));
2970        assert_eq!(state.update(Event::Increment), changed(3, 1.0));
2971        assert_eq!(state.update(Event::Increment), None, "clamped to the max");
2972        assert_eq!(state.draft(3).map(|d| d.text.as_str()), Some("1.00"));
2973        assert_eq!(state.update(Event::Decrement), changed(3, 0.75));
2974        let _ = state.update(Event::Submit);
2975        assert_eq!(state.update(Event::Increment), None, "only while editing");
2976    }
2977
2978    #[test]
2979    fn numbers_parse_with_their_unit_and_limits() {
2980        let number = Number {
2981            min: Some(0.0),
2982            max: Some(100.0),
2983            step: Some(5.0),
2984            decimals: None,
2985            unit: Some("%".into()),
2986        };
2987        assert_eq!(number.parse(" 25% "), Ok(25.0));
2988        assert_eq!(number.parse("25"), Ok(25.0));
2989        assert_eq!(number.parse("-5"), Err("Must be at least 0%".into()));
2990        assert_eq!(number.parse("105"), Err("Must be at most 100%".into()));
2991        assert_eq!(number.parse("12"), Err("Must be a multiple of 5".into()));
2992        assert_eq!(number.parse("abc"), Err("Enter a number".into()));
2993        assert_eq!(number.parse("inf"), Err("Enter a number".into()));
2994        assert_eq!(number.display(25.0), "25%");
2995        assert_eq!(number.stepped(98.0, true), 100.0);
2996
2997        let whole = Number {
2998            step: Some(1.0),
2999            ..Number::default()
3000        };
3001        assert_eq!(whole.parse("1.5"), Err("Must be a whole number".into()));
3002        let fine = Number {
3003            step: Some(0.1),
3004            ..Number::default()
3005        };
3006        assert_eq!(
3007            fine.parse("0.15"),
3008            Ok(0.15),
3009            "fractional steps do not validate"
3010        );
3011        assert_eq!(fine.format(0.15), "0.1");
3012        assert_eq!(fine.stepped(0.2, true), 0.3);
3013        assert_eq!(Number::default().format(-0.0), "0");
3014        assert_eq!(Number::default().format(2.5), "2.5");
3015        let fixed = Number {
3016            decimals: Some(2),
3017            ..Number::default()
3018        };
3019        assert_eq!(fixed.format(1.0), "1.00");
3020    }
3021
3022    #[test]
3023    fn hex_colours_round_trip() {
3024        let blue = Color::from_rgb8(0x3b, 0x82, 0xf6);
3025        assert_eq!(hex(blue), "#3B82F6");
3026        assert_eq!(parse_hex("#3b82f6"), Ok(blue));
3027        assert_eq!(parse_hex("3B82F6"), Ok(blue));
3028        assert_eq!(parse_hex("#fff"), Ok(Color::WHITE));
3029        assert_eq!(parse_hex(" #000 "), Ok(Color::BLACK));
3030        let faded = parse_hex("#00000080").map(hex);
3031        assert_eq!(faded, Ok("#00000080".to_owned()));
3032        assert_eq!(parse_hex("#0008").map(hex), Ok("#00000088".to_owned()));
3033        for bad in ["", "#", "#12", "#12345", "#ggg", "blue", "#3B82F6F6F6"] {
3034            assert!(parse_hex(bad).is_err(), "{bad}");
3035        }
3036    }
3037
3038    #[test]
3039    fn colour_edits_parse_hex() {
3040        let mut state = sample();
3041        let _ = state.update(Event::Edit(5));
3042        assert_eq!(state.draft(5).map(|d| d.text.as_str()), Some("#3B82F6"));
3043        assert_eq!(state.update(Event::Input(5, "#zz".into())), None);
3044        assert!(state.error(5).is_some());
3045        assert_eq!(
3046            state.update(Event::Input(5, "#fff".into())),
3047            changed(5, Color::WHITE)
3048        );
3049        assert!(!state.property(5).is_some_and(Property::is_modified));
3050    }
3051
3052    #[test]
3053    fn property_text_and_parsing_follow_the_kind() {
3054        let state = sample();
3055        let show = |id| {
3056            state
3057                .property(id)
3058                .map(Property::display)
3059                .unwrap_or_default()
3060        };
3061        assert_eq!(show(2), "120px");
3062        assert_eq!(show(3), "0.50");
3063        assert_eq!(show(4), "On");
3064        assert_eq!(show(5), "#3B82F6");
3065        assert_eq!(
3066            state.property(2).map(Property::edit_text),
3067            Some("120".into())
3068        );
3069        let blend = state.property(6).map(|p| p.parse("Screen"));
3070        assert_eq!(blend, Some(Ok(Value::from("Screen"))));
3071        assert!(state.property(6).is_some_and(|p| p.parse("Nope").is_err()));
3072        assert!(state.property(1).is_some_and(|p| !p.is_editable()));
3073        assert!(state.property(8).is_some_and(|p| !p.is_editable()));
3074        assert!(state.property(4).is_some_and(|p| !p.is_typed()));
3075        let shown = custom::<u8>(1, "Opacity", 0.5);
3076        assert_eq!(shown.display(), "0.5");
3077        assert!(shown.accepts(&Value::Bool(true)));
3078        assert_eq!(Toggle::ALL.len(), 2);
3079    }
3080
3081    #[test]
3082    fn number_settings_only_apply_to_numbers() {
3083        let label = text::<u8>(1, "Label", "a").min(1.0).unit("px").switch();
3084        assert!(matches!(label.kind, Kind::Text(None)));
3085        let flag = toggle::<u8>(2, "Flag", true).switch();
3086        assert!(matches!(flag.kind, Kind::Toggle(Toggle::Switch)));
3087        let size = number::<u8>(3, "Size", 1.0).decimals(1).validate(even);
3088        assert!(matches!(
3089            size.kind,
3090            Kind::Number(Number {
3091                decimals: Some(1),
3092                ..
3093            })
3094        ));
3095    }
3096
3097    #[test]
3098    fn values_convert_and_read_back() {
3099        assert_eq!(Value::from(2).as_number(), Some(2.0));
3100        assert_eq!(Value::from(0.5f32).as_number(), Some(0.5));
3101        assert_eq!(Value::from("a").as_text(), Some("a"));
3102        assert_eq!(Value::from(String::from("b")).as_text(), Some("b"));
3103        assert_eq!(Value::from(true).as_bool(), Some(true));
3104        assert_eq!(Value::from(Color::BLACK).as_colour(), Some(Color::BLACK));
3105        assert_eq!(Value::from(true).as_text(), None);
3106        assert_eq!(Value::from("a").as_number(), None);
3107        assert_eq!(Value::from("a").as_bool(), None);
3108        assert_eq!(Value::from("a").as_colour(), None);
3109    }
3110
3111    #[test]
3112    fn set_from_the_app_drops_the_edit() {
3113        let mut state = sample();
3114        let _ = state.update(Event::Edit(7));
3115        assert!(state.set(7, "Card"));
3116        assert_eq!(state.editing(), None);
3117        assert!(state.draft(7).is_none());
3118        assert!(!state.set(7, "Card"));
3119        assert!(!state.set(99, "Card"));
3120    }
3121
3122    #[test]
3123    fn the_name_column_drags_only_while_resizing() {
3124        let mut state = sample().with_name_width(10.0);
3125        assert_eq!(state.name_width(), Some(MIN_NAME_WIDTH));
3126        let _ = state.update(Event::Resize(150.0));
3127        assert_eq!(state.name_width(), Some(MIN_NAME_WIDTH));
3128        let _ = state.update(Event::ResizeStart);
3129        assert!(state.is_resizing());
3130        let _ = state.update(Event::Resize(150.0));
3131        assert_eq!(state.name_width(), Some(150.0));
3132        let _ = state.update(Event::Resize(2.0));
3133        assert_eq!(state.name_width(), Some(MIN_NAME_WIDTH));
3134        let _ = state.update(Event::ResizeEnd);
3135        assert!(!state.is_resizing());
3136    }
3137
3138    #[test]
3139    fn the_name_column_keeps_room_for_values() {
3140        assert_eq!(name_width(None, 600.0), NAME_WIDTH);
3141        let narrow = name_width(None, 260.0);
3142        let room = 260.0 - HANDLE_WIDTH - RESET_WIDTH;
3143        assert_eq!(narrow, room - VALUE_WIDTH, "the values give way first");
3144        assert!(narrow > room * 0.45, "names keep a fair share");
3145        assert_eq!(
3146            name_width(None, 300.0 + NAME_WIDTH),
3147            NAME_WIDTH,
3148            "values only give way when room runs short"
3149        );
3150        assert_eq!(
3151            name_width(Some(500.0), 300.0),
3152            300.0 - HANDLE_WIDTH - RESET_WIDTH - MIN_VALUE_WIDTH
3153        );
3154        assert_eq!(name_width(Some(10.0), 300.0), MIN_NAME_WIDTH);
3155        assert_eq!(
3156            name_width(None, 100.0),
3157            MIN_NAME_WIDTH,
3158            "never below the minimum"
3159        );
3160    }
3161
3162    #[test]
3163    fn focus_highlights_the_first_property_and_losing_it_ends_edits() {
3164        let mut state = sample();
3165        let _ = state.update(Event::Focus(true));
3166        assert!(state.is_focused());
3167        assert_eq!(state.highlighted(), Some(&Entry::Property(1)));
3168        let _ = state.update(Event::Edit(2));
3169        let _ = state.update(Event::ResizeStart);
3170        let _ = state.update(Event::Focus(false));
3171        assert_eq!(state.editing(), None);
3172        assert!(!state.is_resizing());
3173    }
3174
3175    #[test]
3176    fn focus_search_asks_for_the_filter_box() {
3177        let mut state = sample();
3178        let _ = state.update(Event::Edit(7));
3179        let _ = state.update(Event::FocusSearch);
3180        assert_eq!(state.editing(), None);
3181        assert_eq!(state.focus_request(), Some((2, Target::Filter)));
3182        let _ = state.update(Event::Sort(Sort::Alphabetical));
3183        assert_eq!(state.sort(), Sort::Alphabetical);
3184    }
3185
3186    #[test]
3187    fn copying_gives_the_value_as_shown_and_highlights_it() {
3188        let mut state = sample();
3189        assert_eq!(
3190            state.update(Event::CopyValue(2)),
3191            Some(Output::CopyValue("120px".into()))
3192        );
3193        assert_eq!(state.highlighted(), Some(&Entry::Property(2)));
3194        assert_eq!(
3195            state.update(Event::CopyValue(5)),
3196            Some(Output::CopyValue("#3B82F6".into()))
3197        );
3198        assert_eq!(state.update(Event::CopyValue(99)), None);
3199    }
3200
3201    #[test]
3202    fn menu_items_map_to_events() {
3203        assert_eq!(MenuItem::ALL.len(), 2);
3204        assert_eq!(MenuItem::CopyValue.event(4), Event::CopyValue(4));
3205        assert_eq!(MenuItem::Reset.event(4), Event::Reset(4));
3206        assert!(!menu::<u32>().is_open());
3207    }
3208
3209    #[test]
3210    fn the_match_label_counts_what_passes_the_search() {
3211        let mut state = sample();
3212        assert_eq!(match_label(&state), "");
3213        let _ = state.update(Event::Search("i".into()));
3214        assert_eq!(
3215            match_label(&state),
3216            "5 shown",
3217            "Id, Width, Height, Visible, Fill"
3218        );
3219        let _ = state.update(Event::Search("zebra".into()));
3220        assert_eq!(match_label(&state), "None");
3221    }
3222
3223    #[test]
3224    fn default_keymap_resolves_each_action() {
3225        let keymap = default_keymap();
3226        let cases = [
3227            (named(Named::ArrowDown), Action::Next),
3228            (named(Named::ArrowUp), Action::Previous),
3229            (named(Named::Home), Action::First),
3230            (named(Named::End), Action::Last),
3231            (named(Named::ArrowRight), Action::Expand),
3232            (named(Named::ArrowLeft), Action::Collapse),
3233            (named(Named::Enter), Action::Edit),
3234            (named(Named::F2), Action::Edit),
3235            (named(Named::Escape), Action::Cancel),
3236            (named(Named::Delete), Action::Reset),
3237            (
3238                key(Key::Character("c".into()), Modifiers::COMMAND),
3239                Action::CopyValue,
3240            ),
3241            (
3242                key(Key::Character("f".into()), Modifiers::COMMAND),
3243                Action::FocusSearch,
3244            ),
3245        ];
3246        for (press, action) in cases {
3247            assert_eq!(keymap.resolve_event(&press), Some(action), "{press:?}");
3248        }
3249        for action in <Action as keys::Action>::ALL {
3250            assert!(!keymap.chords(action).is_empty(), "{action:?} unbound");
3251            assert!(keys::Action::description(*action).ends_with('.'));
3252            assert!(!keys::Action::name(*action).is_empty());
3253        }
3254    }
3255
3256    #[test]
3257    fn keymaps_can_be_changed() {
3258        let keymap = default_keymap()
3259            .bind(Chord::character('j'), Action::Next)
3260            .unbind(&Chord::named(Named::Delete));
3261        let mut state = sample();
3262        let _ = state.update(Event::Focus(true));
3263        let j = key(Key::Character("j".into()), Modifiers::empty());
3264        assert_eq!(state.key_event(&keymap, &j), Some(Event::Next));
3265        let _ = state.update(Event::Press(Entry::Property(2)));
3266        assert_eq!(state.key_event(&keymap, &named(Named::Delete)), None);
3267        assert_eq!(
3268            state.key_event(&default_keymap(), &named(Named::Delete)),
3269            Some(Event::ResetHighlighted)
3270        );
3271    }
3272
3273    #[test]
3274    fn actions_leave_keys_they_cannot_use() {
3275        let mut state = sample();
3276        assert_eq!(Action::Edit.event(&state), None, "nothing highlighted");
3277        assert_eq!(Action::Cancel.event(&state), None);
3278        assert_eq!(Action::FocusSearch.event(&state), Some(Event::FocusSearch));
3279        assert_eq!(Action::CopyValue.event(&state), None);
3280
3281        let _ = state.update(Event::Press(Entry::Property(1)));
3282        assert_eq!(Action::CopyValue.event(&state), Some(Event::CopyValue(1)));
3283        assert_eq!(Action::Edit.event(&state), None, "read-only");
3284        assert_eq!(Action::Expand.event(&state), None);
3285        assert_eq!(Action::Collapse.event(&state), None, "no category");
3286        assert_eq!(Action::Reset.event(&state), None);
3287
3288        let _ = state.update(Event::Press(Entry::Property(3)));
3289        assert_eq!(Action::Collapse.event(&state), Some(Event::CollapseOrLeave));
3290        assert_eq!(Action::Reset.event(&state), None, "no default");
3291        assert_eq!(Action::Edit.event(&state), Some(Event::EditHighlighted));
3292        let _ = state.update(Event::EditHighlighted);
3293        assert_eq!(Action::Next.event(&state), Some(Event::Decrement));
3294        assert_eq!(Action::Previous.event(&state), Some(Event::Increment));
3295        assert_eq!(Action::Cancel.event(&state), Some(Event::Cancel));
3296
3297        let _ = state.update(Event::Press(Entry::Property(2)));
3298        assert_eq!(Action::Next.event(&state), Some(Event::Next));
3299        assert_eq!(Action::Reset.event(&state), Some(Event::ResetHighlighted));
3300
3301        let _ = state.update(Event::Press(category("Layout")));
3302        assert_eq!(Action::Expand.event(&state), Some(Event::ExpandOrEnter));
3303        assert_eq!(Action::Edit.event(&state), Some(Event::EditHighlighted));
3304        let _ = state.update(Event::EditHighlighted);
3305        assert!(state.is_expanded("Layout"), "Enter toggles a heading");
3306
3307        let mut alphabetical = sample().with_sort(Sort::Alphabetical);
3308        let _ = alphabetical.update(Event::Press(Entry::Property(3)));
3309        assert_eq!(Action::Collapse.event(&alphabetical), None);
3310    }
3311
3312    #[test]
3313    fn rows_resolve_their_colours_in_both_themes() {
3314        for theme in [light(), dark()] {
3315            let tokens = Tokens::of(&theme);
3316            let idle = row_style(&tokens, Status::default());
3317            assert_eq!(idle.background, None);
3318            assert_eq!(idle.text, tokens.foreground);
3319            assert_eq!(idle.ring, None);
3320
3321            let highlighted = Status {
3322                highlighted: true,
3323                ..Status::default()
3324            };
3325            assert_eq!(
3326                row_style(&tokens, highlighted).background,
3327                None,
3328                "nothing is selected, so the highlight has no fill"
3329            );
3330            assert_eq!(
3331                row_style(&tokens, highlighted).ring,
3332                None,
3333                "no ring unfocused"
3334            );
3335            let focused = Status {
3336                focused: true,
3337                ..highlighted
3338            };
3339            assert_eq!(row_style(&tokens, focused).ring, Some(tokens.ring));
3340            let focused_heading = Status {
3341                heading: true,
3342                ..focused
3343            };
3344            assert_eq!(
3345                row_style(&tokens, focused_heading).background,
3346                Some(fade(tokens.muted, 0.6)),
3347                "a highlighted heading keeps its band and adds the ring"
3348            );
3349            assert_eq!(row_style(&tokens, focused_heading).ring, Some(tokens.ring));
3350
3351            let hovered = Status {
3352                hovered: true,
3353                ..Status::default()
3354            };
3355            assert_eq!(
3356                row_style(&tokens, hovered).background,
3357                Some(fade(tokens.accent, 0.6))
3358            );
3359            let disabled = Status {
3360                disabled: true,
3361                ..hovered
3362            };
3363            assert_eq!(row_style(&tokens, disabled).background, None);
3364            assert_eq!(row_style(&tokens, disabled).text, tokens.muted_foreground);
3365
3366            let heading = Status {
3367                heading: true,
3368                ..Status::default()
3369            };
3370            assert_eq!(
3371                row_style(&tokens, heading).background,
3372                Some(fade(tokens.muted, 0.6))
3373            );
3374            assert_eq!(
3375                row_style(
3376                    &tokens,
3377                    Status {
3378                        hovered: true,
3379                        ..heading
3380                    }
3381                )
3382                .background,
3383                Some(tokens.muted)
3384            );
3385
3386            assert_eq!(value_colour(&tokens, true), tokens.foreground);
3387            assert_eq!(value_colour(&tokens, false), tokens.muted_foreground);
3388            assert_eq!(mark_colour(&tokens), tokens.primary);
3389            assert_eq!(divider_colour(&tokens, true), tokens.ring);
3390            assert_ne!(divider_colour(&tokens, false), tokens.ring);
3391            assert_eq!(frame_style(&tokens).border.color, tokens.border);
3392            let swatch = swatch_style(&tokens, Color::WHITE);
3393            assert_eq!(swatch.background, Some(Background::Color(Color::WHITE)));
3394            assert_eq!(swatch.border.color, swatch_ring(&tokens));
3395        }
3396    }
3397
3398    #[test]
3399    fn swatches_keep_a_visible_ring_on_a_dark_background() {
3400        let tokens = Tokens::of(&dark());
3401        let ring = swatch_ring(&tokens);
3402        let near_black = Color::from_rgb8(0x18, 0x18, 0x1b);
3403        let lift = |colour: Color| colour.r + colour.g + colour.b;
3404        assert!(
3405            lift(ring) - lift(near_black) > 0.6,
3406            "{ring:?} stands out from {near_black:?}"
3407        );
3408        assert!(lift(ring) > lift(tokens.border), "stronger than the border");
3409    }
3410
3411    #[test]
3412    fn the_builder_starts_with_the_toolbar_and_description() {
3413        let state = sample();
3414        let grid: PropertyGrid<'_, u32, ()> = property_grid(&state);
3415        assert!(grid.toolbar && grid.description);
3416        assert!(!grid.is_enabled());
3417        assert_eq!(grid.height, Length::Shrink);
3418        let grid = grid
3419            .toolbar(false)
3420            .description(false)
3421            .placeholder("Search")
3422            .height(200)
3423            .keymap(Keymap::new())
3424            .custom(1, |_| Space::new().into())
3425            .custom(1, |_| Space::new().into())
3426            .on_event(|_| ());
3427        assert!(grid.is_enabled());
3428        assert_eq!(
3429            grid.customs.len(),
3430            1,
3431            "a second renderer replaces the first"
3432        );
3433        assert!(format!("{grid:?}").contains("Search"));
3434    }
3435}