1use 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
53pub const ROW_HEIGHT: f32 = 36.0;
55pub const NAME_WIDTH: f32 = 160.0;
57pub const MIN_NAME_WIDTH: f32 = 64.0;
59pub const MIN_VALUE_WIDTH: f32 = 88.0;
61pub const VALUE_WIDTH: f32 = 112.0;
64pub const HANDLE_WIDTH: f32 = 9.0;
66pub const RESET_WIDTH: f32 = 24.0;
68pub const DESCRIPTION_HEIGHT: f32 = 64.0;
71pub const SEARCH: widget::Id = widget::Id::new("iced-cube-property-grid-filter");
73pub const EDITOR_ID: widget::Id = widget::Id::new("iced-cube-property-grid-editor");
75
76pub const NOTHING_SELECTED: &str = "Select a property to see its description.";
78
79const 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#[derive(Debug, Clone, PartialEq)]
88pub enum Value {
89 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#[derive(Debug, Clone, PartialEq, Default)]
170pub struct Number {
171 pub min: Option<f64>,
172 pub max: Option<f64>,
173 pub step: Option<f64>,
176 pub decimals: Option<usize>,
179 pub unit: Option<String>,
181}
182
183impl Number {
184 pub fn precision(&self) -> Option<usize> {
186 self.decimals.or_else(|| self.step.map(decimals_of))
187 }
188
189 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 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 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 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 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
260fn decimals_of(step: f64) -> usize {
262 let text = format!("{step}");
263 text.split_once('.')
264 .map_or(0, |(_, fraction)| fraction.len())
265}
266
267pub 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
277pub 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
307pub type Validate = fn(&str) -> Result<(), String>;
309
310#[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#[derive(Debug, Clone)]
324pub enum Kind {
325 Text(Option<Validate>),
327 Number(Number),
328 Toggle(Toggle),
329 Choice(Vec<String>),
331 Colour,
333 ReadOnly,
335 Custom,
337}
338
339#[derive(Debug, Clone)]
341pub struct Property<Id> {
342 pub id: Id,
343 pub name: String,
344 pub category: Option<String>,
346 pub description: Option<String>,
348 pub kind: Kind,
349 pub value: Value,
350 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
368pub 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
373pub 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
385pub 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
391pub 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
406pub fn colour<Id>(id: Id, name: impl Into<String>, value: Color) -> Property<Id> {
408 property(id, name, Kind::Colour, Value::Colour(value))
409}
410
411pub 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
416pub 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 pub fn default(mut self, value: impl Into<Value>) -> Self {
436 self.default = Some(value.into());
437 self
438 }
439
440 pub fn disabled(mut self, disabled: bool) -> Self {
442 self.disabled = disabled;
443 self
444 }
445
446 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 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 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 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 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 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 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 pub fn is_modified(&self) -> bool {
509 self.default
510 .as_ref()
511 .is_some_and(|default| *default != self.value)
512 }
513
514 pub fn is_typed(&self) -> bool {
516 matches!(self.kind, Kind::Text(_) | Kind::Number(_) | Kind::Colour)
517 }
518
519 pub fn is_editable(&self) -> bool {
521 !self.disabled && !matches!(self.kind, Kind::ReadOnly | Kind::Custom)
522 }
523
524 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
580pub enum Sort {
581 #[default]
584 Categorised,
585 Alphabetical,
587}
588
589impl Sort {
590 pub const ALL: [Sort; 2] = [Sort::Categorised, Sort::Alphabetical];
591}
592
593#[derive(Debug, Clone, PartialEq, Eq, Hash)]
595pub enum Entry<Id> {
596 Category(String),
598 Property(Id),
599}
600
601#[derive(Debug, Clone, PartialEq, Eq, Default)]
603pub struct Draft {
604 pub text: String,
605 pub error: Option<String>,
606}
607
608#[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 original: Value,
620}
621
622#[derive(Debug, Clone, PartialEq)]
624pub enum Event<Id> {
625 Press(Entry<Id>),
627 ToggleCategory(String),
628 Expand(String),
629 Collapse(String),
630 Next,
631 Previous,
632 First,
633 Last,
634 ExpandOrEnter,
636 CollapseOrLeave,
639 Edit(Id),
642 EditHighlighted,
645 Input(Id, String),
647 Submit,
649 Cancel,
652 Increment,
654 Decrement,
656 Set(Id, Value),
658 Reset(Id),
660 ResetHighlighted,
661 Search(String),
663 FocusSearch,
665 CopyValue(Id),
667 Sort(Sort),
668 ResizeStart,
670 Resize(f32),
672 ResizeEnd,
673 Focus(bool),
675}
676
677#[derive(Debug, Clone, PartialEq)]
679pub enum Output<Id> {
680 Changed { id: Id, value: Value },
682 Reset { id: Id, value: Value },
684 CopyValue(String),
687}
688
689#[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 focus: (u64, Option<Target>),
707}
708
709impl<Id: Copy + Eq + Hash> State<Id> {
710 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 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 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 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 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 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 pub fn query(&self) -> &str {
830 &self.query
831 }
832
833 pub fn highlighted(&self) -> Option<&Entry<Id>> {
835 self.highlighted.as_ref()
836 }
837
838 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 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 pub fn draft(&self, id: Id) -> Option<&Draft> {
859 self.drafts.get(&id)
860 }
861
862 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 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 pub fn is_resizing(&self) -> bool {
885 self.resizing
886 }
887
888 pub fn name_width(&self) -> Option<f32> {
890 self.name_width
891 }
892
893 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 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 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 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 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 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 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 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 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 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#[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 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
1507pub 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
1545pub fn focus_search<Id, Message>(_state: &State<Id>) -> Task<Message> {
1549 widget::operation::focus(SEARCH)
1550}
1551
1552pub 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#[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 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 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
1590pub 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
1601pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1618pub struct Status {
1619 pub highlighted: bool,
1621 pub focused: bool,
1623 pub hovered: bool,
1624 pub disabled: bool,
1625 pub heading: bool,
1627}
1628
1629#[derive(Debug, Clone, Copy, PartialEq)]
1631pub struct RowStyle {
1632 pub background: Option<Color>,
1633 pub text: Color,
1634 pub icon: Color,
1635 pub ring: Option<Color>,
1637}
1638
1639pub 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
1663pub fn value_colour(tokens: &Tokens, editable: bool) -> Color {
1665 if editable {
1666 tokens.foreground
1667 } else {
1668 tokens.muted_foreground
1669 }
1670}
1671
1672pub fn mark_colour(tokens: &Tokens) -> Color {
1674 tokens.primary
1675}
1676
1677pub 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
1687pub 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
1699pub 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
1709pub 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
1722pub 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
1739pub 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
1754pub 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 pub fn toolbar(mut self, toolbar: bool) -> Self {
1790 self.toolbar = toolbar;
1791 self
1792 }
1793
1794 pub fn description(mut self, description: bool) -> Self {
1797 self.description = description;
1798 self
1799 }
1800
1801 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 pub fn height(mut self, height: impl Into<Length>) -> Self {
1815 self.height = height.into();
1816 self
1817 }
1818
1819 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 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 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 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
1918struct 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 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 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 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 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 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 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
2420struct Focuser<'a, Message> {
2423 content: Element<'a, Message>,
2424 request: Option<(u64, widget::Id, bool)>,
2425}
2426
2427#[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 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 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}