1use std::collections::HashSet;
24use std::fmt;
25use std::ops::Range;
26use std::rc::Rc;
27
28use iced::keyboard::key::Named;
29use iced::keyboard::{Key, Modifiers};
30use iced::widget::{self, Space, column, container, mouse_area, row, text};
31use iced::{Alignment, Border, Color, Element, Font, Length, Task, Theme, mouse};
32use serde_json::Value;
33
34use crate::data::ellipsis::ellipsis;
35use crate::data::interaction::{
36 Look, RowMenu, match_count_label, pressable, ringed, row_menu, scope, search_count, search_keys,
37};
38use crate::data::rows::rows;
39use crate::feedback::alert::{self, alert};
40use crate::icon::{opacity, themed};
41use crate::keys::{self, Chord, Keymap};
42use crate::overlay::context_menu;
43use crate::overlay::dropdown_menu::{item, separator};
44use crate::overlay::menu::{ICON_SIZE, ROW_PADDING};
45use crate::primitives::button::Size;
46use crate::primitives::icon_button::icon_button;
47use crate::primitives::input::{self, input};
48use crate::theme::{self, Tokens, fade, mix, radius, space, text_size};
49
50pub const ROW_HEIGHT: f32 = 32.0;
52pub const INDENT: f32 = 20.0;
54pub const KEY_WIDTH: f32 = 180.0;
56pub const SEARCH: widget::Id = widget::Id::new("json-viewer-search");
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
61pub struct NodeId(usize);
62
63impl NodeId {
64 pub fn index(self) -> usize {
67 self.0
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub enum Kind {
74 Object,
75 Array,
76 String,
77 Number,
78 Bool,
79 Null,
80}
81
82impl Kind {
83 pub const ALL: [Kind; 6] = [
84 Kind::Object,
85 Kind::Array,
86 Kind::String,
87 Kind::Number,
88 Kind::Bool,
89 Kind::Null,
90 ];
91
92 pub fn of(value: &Value) -> Kind {
93 match value {
94 Value::Object(_) => Kind::Object,
95 Value::Array(_) => Kind::Array,
96 Value::String(_) => Kind::String,
97 Value::Number(_) => Kind::Number,
98 Value::Bool(_) => Kind::Bool,
99 Value::Null => Kind::Null,
100 }
101 }
102
103 pub fn is_container(self) -> bool {
105 matches!(self, Kind::Object | Kind::Array)
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Hash)]
111pub enum Segment {
112 Key(String),
114 Index(usize),
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct ParseError {
121 pub message: String,
122 pub line: usize,
124 pub column: usize,
125}
126
127impl fmt::Display for ParseError {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 write!(
130 f,
131 "{} at line {}, column {}",
132 self.message, self.line, self.column
133 )
134 }
135}
136
137impl std::error::Error for ParseError {}
138
139impl From<serde_json::Error> for ParseError {
140 fn from(error: serde_json::Error) -> Self {
141 let (line, column) = (error.line(), error.column());
142 let full = error.to_string();
143 let suffix = format!(" at line {line} column {column}");
144 let message = full.strip_suffix(&suffix).unwrap_or(&full).to_owned();
145 Self {
146 message,
147 line,
148 column,
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
154struct Item {
155 segment: Option<Segment>,
156 parent: Option<NodeId>,
157 depth: usize,
158 kind: Kind,
159 text: String,
161 key_lower: Option<String>,
163 text_lower: String,
164 children: Vec<NodeId>,
165}
166
167#[derive(Debug, Clone, PartialEq)]
169pub enum Event {
170 Toggle(NodeId),
172 Expand(NodeId),
173 Collapse(NodeId),
174 ExpandSubtree(NodeId),
176 CollapseSubtree(NodeId),
178 ExpandAll,
179 CollapseAll,
180 ExpandToDepth(usize),
183 Press(NodeId),
185 Next,
186 Previous,
187 First,
188 Last,
189 ExpandOrEnter,
192 CollapseOrLeave,
195 ToggleHighlighted,
196 ExpandSiblings,
198 Search(String),
201 NextMatch,
202 PreviousMatch,
203 OnlyMatches(bool),
205 CopyValue(NodeId),
206 CopyPath(NodeId),
207 FocusSearch,
209 Focus(bool),
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub enum Output {
216 CopyValue(String),
218 CopyPath(String),
220 FocusSearch,
222}
223
224#[derive(Debug, Clone)]
226pub struct State {
227 value: Value,
228 items: Vec<Item>,
229 roots: Vec<NodeId>,
230 expanded: HashSet<NodeId>,
231 highlighted: Option<NodeId>,
232 focused: bool,
233 query: String,
234 matches: Vec<NodeId>,
235 matched: HashSet<NodeId>,
236 trail: HashSet<NodeId>,
238 current: Option<usize>,
239 only_matches: bool,
240 error: Option<ParseError>,
241 search_id: widget::Id,
242}
243
244impl Default for State {
245 fn default() -> Self {
246 Self::new(Value::Null)
247 }
248}
249
250impl State {
251 pub fn new(value: Value) -> Self {
254 let (items, roots) = flatten(&value);
255 Self {
256 value,
257 items,
258 roots,
259 expanded: HashSet::new(),
260 highlighted: None,
261 focused: false,
262 query: String::new(),
263 matches: Vec::new(),
264 matched: HashSet::new(),
265 trail: HashSet::new(),
266 current: None,
267 only_matches: false,
268 error: None,
269 search_id: SEARCH,
270 }
271 }
272
273 pub fn parse(json: &str) -> Self {
276 match serde_json::from_str(json) {
277 Ok(value) => Self::new(value),
278 Err(error) => Self {
279 error: Some(error.into()),
280 ..Self::new(Value::Null)
281 }
282 .without_rows(),
283 }
284 }
285
286 fn without_rows(mut self) -> Self {
287 self.items.clear();
288 self.roots.clear();
289 self
290 }
291
292 pub fn with_depth(mut self, depth: usize) -> Self {
295 self.expand_to_depth(depth);
296 self
297 }
298
299 pub fn with_only_matches(mut self, only_matches: bool) -> Self {
301 self.only_matches = only_matches;
302 self
303 }
304
305 pub fn with_search_id(mut self, id: impl Into<widget::Id>) -> Self {
308 self.search_id = id.into();
309 self
310 }
311
312 pub fn search_id(&self) -> &widget::Id {
314 &self.search_id
315 }
316
317 pub fn value(&self) -> &Value {
319 &self.value
320 }
321
322 pub fn error(&self) -> Option<&ParseError> {
324 self.error.as_ref()
325 }
326
327 pub fn len(&self) -> usize {
329 self.items.len()
330 }
331
332 pub fn is_empty(&self) -> bool {
333 self.items.is_empty()
334 }
335
336 pub fn roots(&self) -> &[NodeId] {
338 &self.roots
339 }
340
341 pub fn kind(&self, id: NodeId) -> Option<Kind> {
342 self.item(id).map(|item| item.kind)
343 }
344
345 pub fn segment(&self, id: NodeId) -> Option<&Segment> {
347 self.item(id).and_then(|item| item.segment.as_ref())
348 }
349
350 pub fn parent(&self, id: NodeId) -> Option<NodeId> {
352 self.item(id).and_then(|item| item.parent)
353 }
354
355 pub fn children(&self, id: NodeId) -> &[NodeId] {
356 self.item(id).map_or(&[], |item| &item.children)
357 }
358
359 pub fn depth(&self, id: NodeId) -> usize {
361 self.item(id).map_or(0, |item| item.depth)
362 }
363
364 pub fn is_expanded(&self, id: NodeId) -> bool {
365 self.expanded.contains(&id)
366 }
367
368 pub fn highlighted(&self) -> Option<NodeId> {
370 self.highlighted
371 }
372
373 pub fn is_focused(&self) -> bool {
374 self.focused
375 }
376
377 pub fn query(&self) -> &str {
379 &self.query
380 }
381
382 pub fn matches(&self) -> &[NodeId] {
385 &self.matches
386 }
387
388 pub fn is_match(&self, id: NodeId) -> bool {
389 self.matched.contains(&id)
390 }
391
392 pub fn current_match(&self) -> Option<NodeId> {
394 self.current
395 .and_then(|index| self.matches.get(index))
396 .copied()
397 }
398
399 pub fn is_filtering(&self) -> bool {
402 self.only_matches && !self.query.trim().is_empty()
403 }
404
405 pub fn only_matches(&self) -> bool {
407 self.only_matches
408 }
409
410 pub fn visible(&self) -> Vec<NodeId> {
412 let filtering = self.is_filtering();
413 let mut out = Vec::new();
414 let mut stack: Vec<(NodeId, bool)> =
415 self.roots.iter().rev().map(|&id| (id, false)).collect();
416 while let Some((id, inside)) = stack.pop() {
417 let matched = self.matched.contains(&id);
418 if filtering && !inside && !matched && !self.trail.contains(&id) {
419 continue;
420 }
421 out.push(id);
422 if !self.expanded.contains(&id) {
423 continue;
424 }
425 let inside = inside || matched;
426 stack.extend(self.children(id).iter().rev().map(|&child| (child, inside)));
427 }
428 out
429 }
430
431 pub fn path(&self, id: NodeId) -> String {
434 let mut path = String::from("$");
435 for segment in self.segments(id).into_iter().rev() {
436 match segment {
437 Segment::Index(index) => path.push_str(&format!("[{index}]")),
438 Segment::Key(key) if is_identifier(key) => {
439 path.push('.');
440 path.push_str(key);
441 }
442 Segment::Key(key) => {
443 let quoted = serde_json::to_string(key).unwrap_or_default();
444 path.push_str(&format!("[{quoted}]"));
445 }
446 }
447 }
448 path
449 }
450
451 pub fn value_at(&self, id: NodeId) -> Option<&Value> {
453 self.item(id)?;
454 self.segments(id)
455 .into_iter()
456 .rev()
457 .try_fold(&self.value, |value, segment| match segment {
458 Segment::Key(key) => value.get(key.as_str()),
459 Segment::Index(index) => value.get(*index),
460 })
461 }
462
463 pub fn pretty(&self, id: NodeId) -> Option<String> {
465 self.value_at(id)
466 .and_then(|value| serde_json::to_string_pretty(value).ok())
467 }
468
469 pub fn update(&mut self, event: Event) -> Option<Output> {
471 match event {
472 Event::Toggle(id) if self.is_expanded(id) => self.collapse(id),
473 Event::Toggle(id) | Event::Expand(id) => self.expand(id),
474 Event::Collapse(id) => self.collapse(id),
475 Event::ExpandSubtree(id) => {
476 let ids: Vec<NodeId> = self
477 .subtree(id)
478 .into_iter()
479 .filter(|&id| self.has_children(id))
480 .collect();
481 self.expanded.extend(ids);
482 }
483 Event::CollapseSubtree(id) => {
484 for id in self.subtree(id) {
485 let _ = self.expanded.remove(&id);
486 }
487 self.reveal_highlight();
488 }
489 Event::ExpandAll => {
490 let all: Vec<NodeId> = (0..self.items.len())
491 .map(NodeId)
492 .filter(|&id| self.has_children(id))
493 .collect();
494 self.expanded.extend(all);
495 }
496 Event::CollapseAll => {
497 self.expanded.clear();
498 self.reveal_highlight();
499 }
500 Event::ExpandToDepth(depth) => self.expand_to_depth(depth),
501 Event::Press(id) if self.item(id).is_some() => self.highlighted = Some(id),
502 Event::Press(_) => {}
503 Event::Next => self.step(Step::Next),
504 Event::Previous => self.step(Step::Previous),
505 Event::First => self.step(Step::First),
506 Event::Last => self.step(Step::Last),
507 Event::ExpandOrEnter => self.expand_or_enter(),
508 Event::CollapseOrLeave => self.collapse_or_leave(),
509 Event::ToggleHighlighted => {
510 if let Some(id) = self.highlighted {
511 return self.update(Event::Toggle(id));
512 }
513 }
514 Event::ExpandSiblings => self.expand_siblings(),
515 Event::Search(query) => self.search(query),
516 Event::NextMatch => self.cycle(true),
517 Event::PreviousMatch => self.cycle(false),
518 Event::OnlyMatches(only_matches) => self.only_matches = only_matches,
519 Event::CopyValue(id) => {
520 self.highlight(id);
521 return self.pretty(id).map(Output::CopyValue);
522 }
523 Event::CopyPath(id) => {
524 self.highlight(id);
525 return self.item(id).map(|_| Output::CopyPath(self.path(id)));
526 }
527 Event::FocusSearch => return Some(Output::FocusSearch),
528 Event::Focus(focused) => {
529 self.focused = focused;
530 if focused && self.highlighted.is_none() {
531 self.highlighted = self.roots.first().copied();
532 }
533 }
534 }
535 None
536 }
537
538 pub fn key_event(&self, keymap: &Keymap<Action>, key: &keys::Event) -> Option<Event> {
541 let action = keymap.resolve_event(key).or_else(|| {
542 let symbol = matches!(&key.key, Key::Character(text) if !text.chars().any(char::is_alphanumeric));
543 (symbol && key.modifiers.shift())
544 .then(|| keymap.resolve(&key.key, key.modifiers.difference(Modifiers::SHIFT)))
545 .flatten()
546 })?;
547 action.event(self)
548 }
549
550 fn item(&self, id: NodeId) -> Option<&Item> {
551 self.items.get(id.0)
552 }
553
554 fn has_children(&self, id: NodeId) -> bool {
555 !self.children(id).is_empty()
556 }
557
558 fn highlight(&mut self, id: NodeId) {
559 if self.item(id).is_some() {
560 self.highlighted = Some(id);
561 }
562 }
563
564 fn segments(&self, id: NodeId) -> Vec<&Segment> {
566 let mut segments = Vec::new();
567 let mut current = Some(id);
568 while let Some(node) = current {
569 let Some(item) = self.item(node) else {
570 break;
571 };
572 segments.extend(item.segment.as_ref());
573 current = item.parent;
574 }
575 segments
576 }
577
578 fn subtree(&self, id: NodeId) -> Vec<NodeId> {
580 let mut out = Vec::new();
581 let mut stack = vec![id];
582 while let Some(node) = stack.pop() {
583 if self.item(node).is_none() {
584 continue;
585 }
586 out.push(node);
587 stack.extend(self.children(node).iter().copied());
588 }
589 out
590 }
591
592 fn expand(&mut self, id: NodeId) {
593 if self.has_children(id) {
594 let _ = self.expanded.insert(id);
595 }
596 }
597
598 fn collapse(&mut self, id: NodeId) {
599 if self.expanded.remove(&id) {
600 self.reveal_highlight();
601 }
602 }
603
604 fn expand_to_depth(&mut self, depth: usize) {
605 self.expanded = (0..self.items.len())
606 .map(NodeId)
607 .filter(|&id| self.depth(id) < depth && self.has_children(id))
608 .collect();
609 self.reveal_highlight();
610 }
611
612 fn reveal_highlight(&mut self) {
615 let Some(id) = self.highlighted else {
616 return;
617 };
618 let mut target = id;
619 let mut current = self.parent(id);
620 while let Some(parent) = current {
621 if !self.expanded.contains(&parent) {
622 target = parent;
623 }
624 current = self.parent(parent);
625 }
626 self.highlighted = Some(target);
627 }
628
629 fn step(&mut self, step: Step) {
630 let rows = self.visible();
631 let current = self
632 .highlighted
633 .and_then(|id| rows.iter().position(|&row| row == id));
634 let target = match (step, current) {
635 (Step::First, _) | (Step::Next, None) => rows.first(),
636 (Step::Last, _) | (Step::Previous, None) => rows.last(),
637 (Step::Next, Some(index)) => rows.get(index + 1).or(rows.get(index)),
638 (Step::Previous, Some(index)) => rows.get(index.saturating_sub(1)),
639 };
640 if let Some(&target) = target {
641 self.highlighted = Some(target);
642 }
643 }
644
645 fn expand_or_enter(&mut self) {
646 let Some(id) = self.highlighted else {
647 return;
648 };
649 if !self.is_expanded(id) {
650 self.expand(id);
651 return;
652 }
653 let first = self
654 .visible()
655 .into_iter()
656 .find(|&row| self.parent(row) == Some(id));
657 if let Some(first) = first {
658 self.highlighted = Some(first);
659 }
660 }
661
662 fn collapse_or_leave(&mut self) {
663 let Some(id) = self.highlighted else {
664 return;
665 };
666 if self.is_expanded(id) {
667 self.collapse(id);
668 return;
669 }
670 if let Some(parent) = self.parent(id) {
671 self.highlighted = Some(parent);
672 }
673 }
674
675 fn expand_siblings(&mut self) {
676 let Some(id) = self.highlighted else {
677 return;
678 };
679 let siblings = match self.parent(id) {
680 Some(parent) => self.children(parent).to_vec(),
681 None => self.roots.clone(),
682 };
683 for sibling in siblings {
684 self.expand(sibling);
685 }
686 }
687
688 fn search(&mut self, query: String) {
689 let needle = query.trim().to_lowercase();
690 self.query = query;
691 self.matches.clear();
692 self.matched.clear();
693 self.trail.clear();
694 self.current = None;
695 if needle.is_empty() {
696 return;
697 }
698 self.matches = self
699 .items
700 .iter()
701 .enumerate()
702 .filter(|(_, item)| {
703 item.key_lower
704 .as_ref()
705 .is_some_and(|key| key.contains(&needle))
706 || item.text_lower.contains(&needle)
707 })
708 .map(|(index, _)| NodeId(index))
709 .collect();
710 self.matched = self.matches.iter().copied().collect();
711 for &id in &self.matches {
712 let mut current = self.parent(id);
713 while let Some(parent) = current {
714 if !self.trail.insert(parent) {
715 break;
716 }
717 current = self.parent(parent);
718 }
719 }
720 self.expanded.extend(self.trail.iter().copied());
721 let at = self
724 .highlighted
725 .map_or(0, |from| self.matches.partition_point(|&id| id < from));
726 let index = at.min(self.matches.len().saturating_sub(1));
727 if let Some(&nearest) = self.matches.get(index) {
728 self.current = Some(index);
729 self.highlighted = Some(nearest);
730 }
731 }
732
733 fn cycle(&mut self, forward: bool) {
734 let count = self.matches.len();
735 if count == 0 {
736 return;
737 }
738 let index = match (self.current, forward) {
739 (None, true) => 0,
740 (None, false) => count - 1,
741 (Some(index), true) => (index + 1) % count,
742 (Some(index), false) => (index + count - 1) % count,
743 };
744 self.current = Some(index);
745 self.highlighted = self.matches.get(index).copied();
746 }
747}
748
749#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750enum Step {
751 Next,
752 Previous,
753 First,
754 Last,
755}
756
757fn is_identifier(key: &str) -> bool {
759 let mut chars = key.chars();
760 let Some(first) = chars.next() else {
761 return false;
762 };
763 (first.is_ascii_alphabetic() || first == '_' || first == '$')
764 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
765}
766
767fn flatten(value: &Value) -> (Vec<Item>, Vec<NodeId>) {
771 let mut items = Vec::new();
772 let root = push(&mut items, value, None, None, 0);
773 let members = items
774 .get(root.0)
775 .map(|item| item.children.clone())
776 .unwrap_or_default();
777 if members.is_empty() {
778 return (items, vec![root]);
779 }
780 for item in items.iter_mut().skip(1) {
781 item.depth -= 1;
782 if item.parent == Some(root) {
783 item.parent = None;
784 }
785 }
786 (items, members)
787}
788
789fn push(
790 items: &mut Vec<Item>,
791 value: &Value,
792 segment: Option<Segment>,
793 parent: Option<NodeId>,
794 depth: usize,
795) -> NodeId {
796 let id = NodeId(items.len());
797 let text = match value {
798 Value::Object(_) | Value::Array(_) => String::new(),
799 scalar => serde_json::to_string(scalar).unwrap_or_default(),
800 };
801 let raw = match value {
802 Value::String(string) => string.as_str(),
803 _ => text.as_str(),
804 };
805 let key_lower = match &segment {
806 Some(Segment::Key(key)) => Some(key.to_lowercase()),
807 _ => None,
808 };
809 items.push(Item {
810 text_lower: raw.to_lowercase(),
811 segment,
812 parent,
813 depth,
814 kind: Kind::of(value),
815 text,
816 key_lower,
817 children: Vec::new(),
818 });
819 let children: Vec<NodeId> = match value {
820 Value::Object(map) => map
821 .iter()
822 .map(|(key, child)| {
823 push(
824 items,
825 child,
826 Some(Segment::Key(key.clone())),
827 Some(id),
828 depth + 1,
829 )
830 })
831 .collect(),
832 Value::Array(list) => list
833 .iter()
834 .enumerate()
835 .map(|(index, child)| {
836 push(
837 items,
838 child,
839 Some(Segment::Index(index)),
840 Some(id),
841 depth + 1,
842 )
843 })
844 .collect(),
845 _ => Vec::new(),
846 };
847 if let Some(item) = items.get_mut(id.0) {
848 item.children = children;
849 }
850 id
851}
852
853#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
855pub enum Action {
856 Next,
857 Previous,
858 First,
859 Last,
860 Expand,
861 Collapse,
862 Toggle,
863 ExpandSiblings,
864 ExpandAll,
865 CollapseAll,
866 CopyValue,
867 CopyPath,
868 FocusSearch,
869 ClearSearch,
870 NextMatch,
871 PreviousMatch,
872}
873
874impl Action {
875 pub fn event(self, state: &State) -> Option<Event> {
878 let highlighted = state.highlighted();
879 let searching = !state.matches().is_empty();
880 match self {
881 Action::Next => Some(Event::Next),
882 Action::Previous => Some(Event::Previous),
883 Action::First => Some(Event::First),
884 Action::Last => Some(Event::Last),
885 Action::Expand => highlighted.map(|_| Event::ExpandOrEnter),
886 Action::Collapse => highlighted.map(|_| Event::CollapseOrLeave),
887 Action::Toggle => highlighted
888 .filter(|&id| state.has_children(id))
889 .map(|_| Event::ToggleHighlighted),
890 Action::ExpandSiblings => highlighted.map(|_| Event::ExpandSiblings),
891 Action::ExpandAll => Some(Event::ExpandAll),
892 Action::CollapseAll => Some(Event::CollapseAll),
893 Action::CopyValue => highlighted.map(Event::CopyValue),
894 Action::CopyPath => highlighted.map(Event::CopyPath),
895 Action::FocusSearch => Some(Event::FocusSearch),
896 Action::ClearSearch => {
897 (!state.query().is_empty()).then(|| Event::Search(String::new()))
898 }
899 Action::NextMatch => searching.then_some(Event::NextMatch),
900 Action::PreviousMatch => searching.then_some(Event::PreviousMatch),
901 }
902 }
903}
904
905impl keys::Action for Action {
906 const ALL: &'static [Self] = &[
907 Action::Next,
908 Action::Previous,
909 Action::First,
910 Action::Last,
911 Action::Expand,
912 Action::Collapse,
913 Action::Toggle,
914 Action::ExpandSiblings,
915 Action::ExpandAll,
916 Action::CollapseAll,
917 Action::CopyValue,
918 Action::CopyPath,
919 Action::FocusSearch,
920 Action::ClearSearch,
921 Action::NextMatch,
922 Action::PreviousMatch,
923 ];
924
925 fn defaults() -> Keymap<Self> {
926 default_keymap()
927 }
928
929 fn name(self) -> &'static str {
930 match self {
931 Action::Next => "Next",
932 Action::Previous => "Previous",
933 Action::First => "First",
934 Action::Last => "Last",
935 Action::Expand => "Expand",
936 Action::Collapse => "Collapse",
937 Action::Toggle => "Toggle",
938 Action::ExpandSiblings => "ExpandSiblings",
939 Action::ExpandAll => "ExpandAll",
940 Action::CollapseAll => "CollapseAll",
941 Action::CopyValue => "CopyValue",
942 Action::CopyPath => "CopyPath",
943 Action::FocusSearch => "FocusSearch",
944 Action::ClearSearch => "ClearSearch",
945 Action::NextMatch => "NextMatch",
946 Action::PreviousMatch => "PreviousMatch",
947 }
948 }
949
950 fn description(self) -> &'static str {
951 match self {
952 Action::Next => "Highlights the next row.",
953 Action::Previous => "Highlights the previous row.",
954 Action::First => "Highlights the first row.",
955 Action::Last => "Highlights the last row.",
956 Action::Expand => {
957 "Expands the highlighted object or array, or moves to its first member when it is open."
958 }
959 Action::Collapse => {
960 "Collapses the highlighted object or array, or moves to its parent when it is closed."
961 }
962 Action::Toggle => "Expands or collapses the highlighted object or array.",
963 Action::ExpandSiblings => "Expands the highlighted node and all of its siblings.",
964 Action::ExpandAll => "Expands every object and array.",
965 Action::CollapseAll => "Collapses every object and array.",
966 Action::CopyValue => "Copies the highlighted value as pretty-printed JSON.",
967 Action::CopyPath => "Copies the path of the highlighted value.",
968 Action::FocusSearch => "Moves to the search box.",
969 Action::ClearSearch => "Clears the search.",
970 Action::NextMatch => "Highlights the next search match.",
971 Action::PreviousMatch => "Highlights the previous search match.",
972 }
973 }
974}
975
976pub fn default_keymap() -> Keymap<Action> {
1007 Keymap::new()
1008 .bind(Chord::named(Named::ArrowDown), Action::Next)
1009 .bind(Chord::named(Named::ArrowUp), Action::Previous)
1010 .bind(Chord::named(Named::Home), Action::First)
1011 .bind(Chord::named(Named::End), Action::Last)
1012 .bind(Chord::named(Named::ArrowRight), Action::Expand)
1013 .bind(Chord::named(Named::ArrowLeft), Action::Collapse)
1014 .bind(Chord::named(Named::Enter), Action::Toggle)
1015 .bind(Chord::character('*'), Action::ExpandSiblings)
1016 .bind(
1017 Chord::named(Named::ArrowRight).command().shift(),
1018 Action::ExpandAll,
1019 )
1020 .bind(
1021 Chord::named(Named::ArrowLeft).command().shift(),
1022 Action::CollapseAll,
1023 )
1024 .bind(Chord::character('c').command(), Action::CopyValue)
1025 .bind(Chord::character('c').command().shift(), Action::CopyPath)
1026 .bind(Chord::character('f').command(), Action::FocusSearch)
1027 .bind(Chord::named(Named::Escape), Action::ClearSearch)
1028 .bind(Chord::named(Named::F3), Action::NextMatch)
1029 .bind(Chord::named(Named::F3).shift(), Action::PreviousMatch)
1030}
1031
1032#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1034pub enum MenuItem {
1035 CopyValue,
1036 CopyPath,
1037 ExpandAll,
1038 CollapseAll,
1039}
1040
1041impl MenuItem {
1042 pub const ALL: [MenuItem; 4] = [
1043 MenuItem::CopyValue,
1044 MenuItem::CopyPath,
1045 MenuItem::ExpandAll,
1046 MenuItem::CollapseAll,
1047 ];
1048
1049 pub fn event(self, id: NodeId) -> Event {
1051 match self {
1052 MenuItem::CopyValue => Event::CopyValue(id),
1053 MenuItem::CopyPath => Event::CopyPath(id),
1054 MenuItem::ExpandAll => Event::ExpandSubtree(id),
1055 MenuItem::CollapseAll => Event::CollapseSubtree(id),
1056 }
1057 }
1058}
1059
1060pub fn menu() -> context_menu::State<MenuItem, NodeId> {
1064 context_menu::State::new([
1065 item(MenuItem::CopyValue, "Copy value").icon(crate::lucide!(Copy)),
1066 item(MenuItem::CopyPath, "Copy path").icon(crate::lucide!(Route)),
1067 separator(),
1068 item(MenuItem::ExpandAll, "Expand all").icon(crate::lucide!(ChevronsUpDown)),
1069 item(MenuItem::CollapseAll, "Collapse all").icon(crate::lucide!(ChevronsDownUp)),
1070 ])
1071}
1072
1073#[derive(Debug, Clone, Copy, PartialEq)]
1075pub struct Syntax {
1076 pub key: Color,
1077 pub punctuation: Color,
1079 pub string: Color,
1080 pub number: Color,
1081 pub boolean: Color,
1082 pub null: Color,
1083}
1084
1085pub fn syntax(tokens: &Tokens) -> Syntax {
1091 let readable = |colour: Color| {
1092 if tokens.is_dark {
1093 colour
1094 } else {
1095 mix(colour, tokens.foreground, 0.25)
1096 }
1097 };
1098 Syntax {
1099 key: tokens.foreground,
1100 punctuation: tokens.muted_foreground,
1101 string: readable(tokens.success),
1102 number: readable(tokens.warning),
1103 boolean: tokens.primary,
1104 null: tokens.muted_foreground,
1105 }
1106}
1107
1108pub fn value_colour(tokens: &Tokens, kind: Kind) -> Color {
1110 let syntax = syntax(tokens);
1111 match kind {
1112 Kind::Object | Kind::Array => syntax.punctuation,
1113 Kind::String => syntax.string,
1114 Kind::Number => syntax.number,
1115 Kind::Bool => syntax.boolean,
1116 Kind::Null => syntax.null,
1117 }
1118}
1119
1120pub fn value_font(kind: Kind) -> Font {
1123 match kind {
1124 Kind::Bool => theme::semibold(),
1125 _ => theme::font(),
1126 }
1127}
1128
1129#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1131pub struct Status {
1132 pub highlighted: bool,
1134 pub focused: bool,
1136 pub hovered: bool,
1137 pub matched: bool,
1139 pub current: bool,
1141 pub disabled: bool,
1142}
1143
1144#[derive(Debug, Clone, Copy, PartialEq)]
1146pub struct RowStyle {
1147 pub background: Option<Color>,
1148 pub ring: Option<Color>,
1149}
1150
1151pub fn frame_style(tokens: &Tokens) -> container::Style {
1153 container::Style {
1154 border: Border {
1155 color: tokens.border,
1156 width: 1.0,
1157 radius: radius::MD.into(),
1158 },
1159 ..container::Style::default()
1160 }
1161}
1162
1163pub fn row_style(tokens: &Tokens, status: Status) -> RowStyle {
1167 if status.disabled {
1168 return RowStyle {
1169 background: None,
1170 ring: None,
1171 };
1172 }
1173 let background = if status.current {
1174 Some(fade(tokens.warning, 0.35))
1175 } else if status.matched {
1176 Some(fade(tokens.warning, 0.15))
1177 } else if status.hovered {
1178 Some(fade(tokens.accent, 0.6))
1179 } else {
1180 None
1181 };
1182 RowStyle {
1183 background,
1184 ring: (status.highlighted && status.focused).then_some(tokens.ring),
1185 }
1186}
1187
1188type OnEvent<'a, Message> = dyn Fn(Event) -> Message + 'a;
1189type BuildMenu<'a, Message> =
1190 dyn FnOnce(Keymap<context_menu::Action>) -> RowMenu<'a, NodeId, Message> + 'a;
1191
1192pub struct JsonViewer<'a, Message> {
1194 state: &'a State,
1195 search_bar: bool,
1196 placeholder: String,
1197 frame: bool,
1198 key_width: f32,
1199 width: Length,
1200 height: Length,
1201 keymap: Keymap<Action>,
1202 menu: Option<Box<BuildMenu<'a, Message>>>,
1203 menu_keymap: Keymap<context_menu::Action>,
1204 on_event: Option<Box<OnEvent<'a, Message>>>,
1205}
1206
1207pub fn json_viewer<Message>(state: &State) -> JsonViewer<'_, Message> {
1210 JsonViewer {
1211 state,
1212 search_bar: false,
1213 placeholder: "Search".to_owned(),
1214 frame: true,
1215 key_width: KEY_WIDTH,
1216 width: Length::Fill,
1217 height: Length::Shrink,
1218 keymap: default_keymap(),
1219 menu: None,
1220 menu_keymap: context_menu::default_keymap(),
1221 on_event: None,
1222 }
1223}
1224
1225impl<Message> fmt::Debug for JsonViewer<'_, Message> {
1226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1227 f.debug_struct("JsonViewer")
1228 .field("state", self.state)
1229 .field("search_bar", &self.search_bar)
1230 .field("placeholder", &self.placeholder)
1231 .field("frame", &self.frame)
1232 .field("key_width", &self.key_width)
1233 .field("width", &self.width)
1234 .field("height", &self.height)
1235 .field("keymap", &self.keymap)
1236 .finish_non_exhaustive()
1237 }
1238}
1239
1240impl<'a, Message> JsonViewer<'a, Message> {
1241 pub fn search_bar(mut self, search_bar: bool) -> Self {
1245 self.search_bar = search_bar;
1246 self
1247 }
1248
1249 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1251 self.placeholder = placeholder.into();
1252 self
1253 }
1254
1255 pub fn frame(mut self, frame: bool) -> Self {
1259 self.frame = frame;
1260 self
1261 }
1262
1263 pub fn key_width(mut self, width: f32) -> Self {
1266 self.key_width = width;
1267 self
1268 }
1269
1270 pub fn width(mut self, width: impl Into<Length>) -> Self {
1271 self.width = width.into();
1272 self
1273 }
1274
1275 pub fn height(mut self, height: impl Into<Length>) -> Self {
1278 self.height = height.into();
1279 self
1280 }
1281
1282 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
1284 self.keymap = keymap;
1285 self
1286 }
1287
1288 pub fn on_event(mut self, on_event: impl Fn(Event) -> Message + 'a) -> Self {
1289 self.on_event = Some(Box::new(on_event));
1290 self
1291 }
1292
1293 pub fn context_menu<MenuId>(
1298 mut self,
1299 menu: &'a context_menu::State<MenuId, NodeId>,
1300 on_event: impl Fn(context_menu::Event<MenuId, NodeId>) -> Message + 'a,
1301 ) -> Self
1302 where
1303 MenuId: Copy + PartialEq + 'a,
1304 Message: Clone + 'a,
1305 {
1306 let on_event: Rc<dyn Fn(context_menu::Event<MenuId, NodeId>) -> Message + 'a> =
1307 Rc::new(on_event);
1308 self.menu = Some(Box::new(move |keymap| row_menu(menu, keymap, on_event)));
1309 self
1310 }
1311
1312 pub fn context_menu_keymap(mut self, keymap: Keymap<context_menu::Action>) -> Self {
1314 self.menu_keymap = keymap;
1315 self
1316 }
1317
1318 pub fn is_enabled(&self) -> bool {
1319 self.on_event.is_some()
1320 }
1321}
1322
1323struct Context<'a, Message> {
1325 state: &'a State,
1326 key_width: f32,
1327 menu_target: Option<NodeId>,
1329 on_event: Option<Rc<OnEvent<'a, Message>>>,
1330}
1331
1332impl<'a, Message> From<JsonViewer<'a, Message>> for Element<'a, Message>
1333where
1334 Message: Clone + 'a,
1335{
1336 fn from(viewer: JsonViewer<'a, Message>) -> Self {
1337 let JsonViewer {
1338 state,
1339 search_bar,
1340 placeholder,
1341 frame,
1342 key_width,
1343 width,
1344 height,
1345 keymap,
1346 menu,
1347 menu_keymap,
1348 on_event,
1349 } = viewer;
1350 if let Some(error) = state.error() {
1351 return container(
1352 alert("Invalid JSON")
1353 .variant(alert::Variant::Destructive)
1354 .description(error.to_string()),
1355 )
1356 .width(width)
1357 .into();
1358 }
1359 let on_event: Option<Rc<OnEvent<'a, Message>>> = on_event.map(Rc::from);
1360 let menu = menu
1361 .filter(|_| on_event.is_some())
1362 .map(|build| Rc::new(build(menu_keymap)));
1363 let lines = state.visible();
1364 let highlight = state
1365 .highlighted()
1366 .and_then(|id| lines.iter().position(|&line| line == id));
1367 let count = lines.len();
1368 let context = Rc::new(Context {
1369 state,
1370 key_width,
1371 menu_target: menu.as_ref().and_then(|menu| menu.target()),
1372 on_event: on_event.clone(),
1373 });
1374
1375 let build = {
1376 let menu = menu.clone();
1377 move |range: Range<usize>| -> Element<'a, Message> {
1378 let lines = lines.get(range).unwrap_or_default();
1379 column(lines.iter().map(|&id| {
1380 let row = row_view(&context, id);
1381 match &menu {
1382 Some(menu) => menu.wrap(id, row),
1383 None => row,
1384 }
1385 }))
1386 .into()
1387 }
1388 };
1389 let body: Element<'a, Message> = if count == 0 && state.is_filtering() {
1390 container(
1391 text(format!("Nothing matches \"{}\"", state.query().trim()))
1392 .size(text_size::SM)
1393 .wrapping(text::Wrapping::None)
1394 .style(|theme: &Theme| text::Style {
1395 color: Some(Tokens::of(theme).muted_foreground),
1396 }),
1397 )
1398 .padding(ROW_PADDING)
1399 .height(ROW_HEIGHT)
1400 .into()
1401 } else {
1402 rows(count, ROW_HEIGHT, build)
1403 .highlight(highlight)
1404 .height(height)
1405 .into()
1406 };
1407 let body: Element<'a, Message> = if frame {
1408 container(body)
1409 .padding(1)
1410 .width(Length::Fill)
1411 .height(if matches!(height, Length::Fixed(_)) {
1412 Length::Shrink
1413 } else {
1414 height
1415 })
1416 .clip(true)
1417 .style(|theme| frame_style(&Tokens::of(theme)))
1418 .into()
1419 } else {
1420 body
1421 };
1422 let mut parts = column![].spacing(space::SM).width(width);
1423 if search_bar {
1424 parts = parts.push(toolbar(state, &placeholder, on_event.clone()));
1425 }
1426 let parts = parts.push(body);
1427
1428 let Some(on_event) = on_event else {
1429 return parts.into();
1430 };
1431 let on_focus = on_event.clone();
1432 scope(parts)
1433 .on_focus(move |focused| on_focus(Event::Focus(focused)))
1434 .on_key(move |key| {
1435 let opened = menu
1436 .as_ref()
1437 .zip(state.highlighted())
1438 .and_then(|(menu, target)| menu.key(key, target));
1439 opened.or_else(|| state.key_event(&keymap, key).map(&*on_event))
1440 })
1441 .into()
1442 }
1443}
1444
1445pub fn focus_search<Message>(state: &State) -> Task<Message> {
1448 widget::operation::focus(state.search_id().clone())
1449}
1450
1451pub fn match_label(state: &State) -> String {
1455 let current = state.current.map(|index| index + 1);
1456 match_count_label(state.query(), state.matches().len(), current)
1457}
1458
1459fn toolbar<'a, Message: Clone + 'a>(
1460 state: &'a State,
1461 placeholder: &str,
1462 on_event: Option<Rc<OnEvent<'a, Message>>>,
1463) -> Element<'a, Message> {
1464 let id = state.search_id().clone();
1465 let send = |event: Event| on_event.as_ref().map(|on_event| on_event(event));
1466 let searching = !state.matches().is_empty();
1467 let field = {
1468 let on_input = on_event
1469 .clone()
1470 .map(|on_event| move |query| on_event(Event::Search(query)));
1471 input(placeholder, state.query())
1472 .id(id.clone())
1473 .icon(crate::lucide!(Search))
1474 .size(input::Size::Sm)
1475 .width(Length::Fill)
1476 .on_input_maybe(on_input)
1477 };
1478 let field = search_keys(field, id)
1479 .on_clear(send(Event::Search(String::new())).filter(|_| !state.query().is_empty()))
1480 .on_next(send(Event::NextMatch).filter(|_| searching))
1481 .on_previous(send(Event::PreviousMatch).filter(|_| searching));
1482 let count = search_count(match_label(state));
1483 row![
1484 field,
1485 count,
1486 icon_button(crate::lucide!(ChevronUp))
1487 .label("Previous match")
1488 .size(Size::Sm)
1489 .on_press_maybe(send(Event::PreviousMatch).filter(|_| searching)),
1490 icon_button(crate::lucide!(ChevronDown))
1491 .label("Next match")
1492 .size(Size::Sm)
1493 .on_press_maybe(send(Event::NextMatch).filter(|_| searching)),
1494 icon_button(crate::lucide!(ListFilter))
1495 .label("Only matches")
1496 .size(Size::Sm)
1497 .pressed(state.only_matches())
1498 .on_press_maybe(send(Event::OnlyMatches(!state.only_matches()))),
1499 ]
1500 .spacing(space::XS)
1501 .align_y(Alignment::Center)
1502 .into()
1503}
1504
1505fn indent<'a, Message: 'a>(depth: usize) -> Element<'a, Message> {
1507 Space::new()
1508 .width(INDENT * depth as f32)
1509 .height(Length::Fill)
1510 .into()
1511}
1512
1513fn muted<'a>(content: String, enabled: bool) -> text::Text<'a> {
1514 text(content)
1515 .size(text_size::SM)
1516 .wrapping(text::Wrapping::None)
1517 .style(move |theme: &Theme| {
1518 let tokens = Tokens::of(theme);
1519 text::Style {
1520 color: Some(if enabled {
1521 syntax(&tokens).punctuation
1522 } else {
1523 tokens.muted_foreground
1524 }),
1525 }
1526 })
1527}
1528
1529fn row_view<'a, Message: Clone + 'a>(
1530 context: &Context<'a, Message>,
1531 id: NodeId,
1532) -> Element<'a, Message> {
1533 let state = context.state;
1534 let Some(item) = state.item(id) else {
1535 return Space::new().into();
1536 };
1537 let on_event = context.on_event.as_ref();
1538 let enabled = on_event.is_some();
1539 let expanded = state.is_expanded(id);
1540 let highlighted = state.highlighted() == Some(id);
1541 let menu_open_here = context.menu_target == Some(id);
1542 let status = Status {
1543 highlighted: ringed(context.menu_target.as_ref(), &id, highlighted),
1544 focused: state.is_focused() || menu_open_here,
1545 hovered: false,
1546 matched: state.is_match(id),
1547 current: state.current_match() == Some(id),
1548 disabled: !enabled,
1549 };
1550
1551 let chevron: Element<'a, Message> = if state.has_children(id) {
1552 let glyph = if expanded {
1553 crate::lucide!(ChevronDown)
1554 } else {
1555 crate::lucide!(ChevronRight)
1556 };
1557 let icon = container(themed(glyph, ICON_SIZE, opacity(enabled), |theme| {
1558 Tokens::of(theme).muted_foreground
1559 }))
1560 .width(INDENT)
1561 .height(Length::Fill)
1562 .center_y(Length::Fill);
1563 match on_event {
1564 Some(on_event) => mouse_area(icon)
1565 .on_press(on_event(Event::Toggle(id)))
1566 .interaction(mouse::Interaction::Pointer)
1567 .into(),
1568 None => icon.into(),
1569 }
1570 } else {
1571 Space::new().width(INDENT).into()
1572 };
1573
1574 let mut content = row![]
1575 .spacing(space::XS)
1576 .align_y(Alignment::Center)
1577 .height(Length::Fill);
1578 match &item.segment {
1579 Some(Segment::Key(key)) => {
1580 let key = ellipsis(key.as_str())
1581 .width(Length::Shrink)
1582 .colour(move |theme| {
1583 let tokens = Tokens::of(theme);
1584 if enabled {
1585 syntax(&tokens).key
1586 } else {
1587 tokens.muted_foreground
1588 }
1589 });
1590 content = content.push(
1591 row![
1592 container(key).max_width(context.key_width),
1593 muted(":".to_owned(), enabled)
1594 ]
1595 .align_y(Alignment::Center),
1596 );
1597 }
1598 Some(Segment::Index(index)) => {
1599 content = content.push(muted(format!("{index}:"), enabled));
1600 }
1601 None => {}
1602 }
1603 let kind = item.kind;
1604 let value: Element<'a, Message> = match kind {
1605 Kind::Object | Kind::Array => {
1606 let (open, close) = if kind == Kind::Object {
1607 ('{', '}')
1608 } else {
1609 ('[', ']')
1610 };
1611 let count = item.children.len();
1612 let summary = if count == 0 {
1613 format!("{open}{close}")
1614 } else {
1615 format!("{open}{count}{close}")
1616 };
1617 row![muted(summary, enabled), Space::new().width(Length::Fill)].into()
1618 }
1619 _ => ellipsis(item.text.as_str())
1620 .font(value_font(kind))
1621 .colour(move |theme| {
1622 let tokens = Tokens::of(theme);
1623 if enabled {
1624 value_colour(&tokens, kind)
1625 } else {
1626 tokens.muted_foreground
1627 }
1628 })
1629 .into(),
1630 };
1631 content = content.push(value);
1632 if let Some(on_event) = on_event.filter(|_| highlighted) {
1633 content = content.push(
1634 row![
1635 icon_button(crate::lucide!(Copy))
1636 .label("Copy value")
1637 .size(Size::Sm)
1638 .on_press(on_event(Event::CopyValue(id))),
1639 icon_button(crate::lucide!(Route))
1640 .label("Copy path")
1641 .size(Size::Sm)
1642 .on_press(on_event(Event::CopyPath(id))),
1643 ]
1644 .align_y(Alignment::Center),
1645 );
1646 }
1647
1648 let body = container(row![indent(item.depth), chevron, content].height(Length::Fill))
1649 .padding(
1650 iced::Padding::ZERO
1651 .left(ROW_PADDING.left)
1652 .right(ROW_PADDING.right),
1653 )
1654 .width(Length::Fill)
1655 .height(ROW_HEIGHT);
1656 let row = pressable(body, move |theme, hovered| {
1657 let style = row_style(&Tokens::of(theme), Status { hovered, ..status });
1658 Look {
1659 background: style.background,
1660 border: Border {
1661 color: style.ring.unwrap_or(Color::TRANSPARENT),
1662 width: if style.ring.is_some() { 1.0 } else { 0.0 },
1663 radius: radius::SM.into(),
1664 },
1665 divider: None,
1666 }
1667 });
1668 let Some(on_event) = on_event.cloned() else {
1669 return row.into();
1670 };
1671 let toggle = state.has_children(id).then(|| on_event(Event::Toggle(id)));
1672 row.on_press(move |_| on_event(Event::Press(id)))
1673 .on_double_click(toggle)
1674 .into()
1675}
1676
1677#[cfg(test)]
1678mod tests {
1679 use super::*;
1680 use crate::theme::{dark, light};
1681 use serde_json::json;
1682
1683 fn sample() -> State {
1695 State::new(json!({
1696 "name": "Ada",
1697 "tags": ["admin", "ops"],
1698 "address": { "city": "London", "first line": null },
1699 "active": true,
1700 "age": 36
1701 }))
1702 }
1703
1704 fn id(state: &State, path: &str) -> NodeId {
1705 (0..state.len())
1706 .map(NodeId)
1707 .find(|&id| state.path(id) == path)
1708 .unwrap_or_else(|| panic!("no node at {path}"))
1709 }
1710
1711 fn key(key: Key, modifiers: Modifiers) -> keys::Event {
1712 keys::Event { key, modifiers }
1713 }
1714
1715 #[test]
1716 fn flattens_in_document_order_with_the_members_as_top_rows() {
1717 let state = sample();
1718 assert_eq!(state.len(), 10);
1719 let paths: Vec<String> = state.roots().iter().map(|&id| state.path(id)).collect();
1720 assert_eq!(
1721 paths,
1722 ["$.name", "$.tags", "$.address", "$.active", "$.age"]
1723 );
1724 let tags = id(&state, "$.tags");
1725 assert_eq!(state.kind(tags), Some(Kind::Array));
1726 assert_eq!(state.children(tags).len(), 2);
1727 assert_eq!(state.parent(tags), None);
1728 assert_eq!(state.depth(tags), 0);
1729 let first = state.children(tags)[0];
1730 assert_eq!(state.parent(first), Some(tags));
1731 assert_eq!(state.depth(first), 1);
1732 assert_eq!(state.segment(first), Some(&Segment::Index(0)));
1733 assert_eq!(state.visible().len(), 5, "everything starts collapsed");
1734 }
1735
1736 #[test]
1737 fn a_scalar_or_an_empty_document_is_one_row() {
1738 for value in [json!(42), json!("hi"), json!({}), json!([]), json!(null)] {
1739 let state = State::new(value.clone());
1740 assert_eq!(state.roots(), &[NodeId(0)], "{value}");
1741 assert_eq!(state.path(NodeId(0)), "$");
1742 assert_eq!(state.value_at(NodeId(0)), Some(&value));
1743 }
1744 assert_eq!(State::default().len(), 1);
1745 }
1746
1747 #[test]
1748 fn kinds_and_containers() {
1749 assert_eq!(Kind::ALL.len(), 6);
1750 assert!(Kind::Object.is_container() && Kind::Array.is_container());
1751 assert!(!Kind::String.is_container());
1752 assert_eq!(Kind::of(&json!(1.5)), Kind::Number);
1753 assert_eq!(Kind::of(&json!(false)), Kind::Bool);
1754 }
1755
1756 #[test]
1757 fn invalid_text_keeps_the_error_and_has_no_rows() {
1758 let state = State::parse("{\n \"a\": 1,\n \"b\": }");
1759 let error = state.error().expect("an error");
1760 assert_eq!((error.line, error.column), (3, 8));
1761 assert_eq!(error.message, "expected value");
1762 assert_eq!(error.to_string(), "expected value at line 3, column 8");
1763 assert!(state.is_empty());
1764 assert!(state.visible().is_empty());
1765
1766 let valid = State::parse("[1, 2]");
1767 assert!(valid.error().is_none());
1768 assert_eq!(valid.roots().len(), 2);
1769 }
1770
1771 #[test]
1772 fn paths_use_dots_for_identifiers_and_brackets_otherwise() {
1773 let state = State::new(json!({
1774 "users": [{ "name": "Ada" }, { "first name": "Grace", "$id": 2, "9lives": true }]
1775 }));
1776 assert_eq!(id(&state, "$.users[0].name").index(), 3);
1777 let second = state.children(id(&state, "$.users"))[1];
1778 let paths: Vec<String> = state
1779 .children(second)
1780 .iter()
1781 .map(|&id| state.path(id))
1782 .collect();
1783 assert_eq!(
1784 paths,
1785 [
1786 "$.users[1][\"first name\"]",
1787 "$.users[1].$id",
1788 "$.users[1][\"9lives\"]"
1789 ]
1790 );
1791 assert!(!is_identifier(""));
1792 assert!(is_identifier("_a1"));
1793 }
1794
1795 #[test]
1796 fn copying_gives_pretty_json_and_the_path() {
1797 let mut state = sample();
1798 let address = id(&state, "$.address");
1799 assert_eq!(
1800 state.update(Event::CopyValue(address)),
1801 Some(Output::CopyValue(
1802 "{\n \"city\": \"London\",\n \"first line\": null\n}".into()
1803 ))
1804 );
1805 assert_eq!(state.highlighted(), Some(address));
1806 let city = state.children(address)[0];
1807 assert_eq!(
1808 state.update(Event::CopyValue(city)),
1809 Some(Output::CopyValue("\"London\"".into()))
1810 );
1811 assert_eq!(
1812 state.update(Event::CopyPath(city)),
1813 Some(Output::CopyPath("$.address.city".into()))
1814 );
1815 assert_eq!(state.update(Event::CopyValue(NodeId(999))), None);
1816 assert_eq!(state.update(Event::CopyPath(NodeId(999))), None);
1817 }
1818
1819 #[test]
1820 fn expanding_and_collapsing() {
1821 let mut state = sample();
1822 let tags = id(&state, "$.tags");
1823 let name = id(&state, "$.name");
1824 let _ = state.update(Event::Toggle(tags));
1825 assert!(state.is_expanded(tags));
1826 assert_eq!(state.visible().len(), 7);
1827 let _ = state.update(Event::Expand(name));
1828 assert!(!state.is_expanded(name), "scalars never expand");
1829 let _ = state.update(Event::Toggle(tags));
1830 assert!(!state.is_expanded(tags));
1831
1832 let _ = state.update(Event::ExpandAll);
1833 assert_eq!(state.visible().len(), 9);
1834 let _ = state.update(Event::CollapseAll);
1835 assert_eq!(state.visible().len(), 5);
1836
1837 let empty = State::new(json!({ "a": {}, "b": [] }));
1838 let mut empty = empty;
1839 let _ = empty.update(Event::ExpandAll);
1840 assert!(empty.visible().iter().all(|&id| !empty.is_expanded(id)));
1841 }
1842
1843 #[test]
1844 fn expand_to_depth_opens_only_shallow_containers() {
1845 let value = json!({ "a": { "b": { "c": { "d": 1 } } } });
1846 let state = State::new(value.clone()).with_depth(2);
1847 assert_eq!(state.visible().len(), 3, "a, b and c");
1848 let mut state = state;
1849 let _ = state.update(Event::ExpandToDepth(0));
1850 assert_eq!(state.visible().len(), 1);
1851 let _ = state.update(Event::ExpandToDepth(10));
1852 assert_eq!(state.visible().len(), 4);
1853 }
1854
1855 #[test]
1856 fn subtrees_expand_and_collapse_together() {
1857 let mut state = State::new(json!({ "a": { "b": { "c": 1 } }, "d": { "e": 2 } }));
1858 let a = id(&state, "$.a");
1859 let _ = state.update(Event::ExpandSubtree(a));
1860 assert_eq!(state.visible().len(), 4);
1861 let c = id(&state, "$.a.b.c");
1862 let _ = state.update(Event::Press(c));
1863 let _ = state.update(Event::CollapseSubtree(a));
1864 assert_eq!(state.visible().len(), 2);
1865 assert_eq!(state.highlighted(), Some(a), "the highlight moves up");
1866 let _ = state.update(Event::Expand(a));
1867 assert!(!state.is_expanded(id(&state, "$.a.b")));
1868 }
1869
1870 #[test]
1871 fn collapsing_moves_a_hidden_highlight_to_the_outermost_closed_node() {
1872 let mut state = State::new(json!({ "a": { "b": { "c": 1 } } })).with_depth(5);
1873 let c = id(&state, "$.a.b.c");
1874 let _ = state.update(Event::Press(c));
1875 let _ = state.update(Event::CollapseAll);
1876 assert_eq!(state.highlighted(), Some(id(&state, "$.a")));
1877 }
1878
1879 #[test]
1880 fn keyboard_navigation() {
1881 let mut state = sample();
1882 let _ = state.update(Event::Focus(true));
1883 assert!(state.is_focused());
1884 assert_eq!(state.highlighted(), Some(id(&state, "$.name")));
1885 let _ = state.update(Event::Next);
1886 let tags = id(&state, "$.tags");
1887 assert_eq!(state.highlighted(), Some(tags));
1888 let _ = state.update(Event::ExpandOrEnter);
1889 assert!(state.is_expanded(tags));
1890 let _ = state.update(Event::ExpandOrEnter);
1891 assert_eq!(state.highlighted(), Some(id(&state, "$.tags[0]")));
1892 let _ = state.update(Event::CollapseOrLeave);
1893 assert_eq!(state.highlighted(), Some(tags), "leaves to the parent");
1894 let _ = state.update(Event::CollapseOrLeave);
1895 assert!(!state.is_expanded(tags));
1896 let _ = state.update(Event::Last);
1897 assert_eq!(state.highlighted(), Some(id(&state, "$.age")));
1898 let _ = state.update(Event::Next);
1899 assert_eq!(state.highlighted(), Some(id(&state, "$.age")), "stays");
1900 let _ = state.update(Event::First);
1901 let _ = state.update(Event::Previous);
1902 assert_eq!(state.highlighted(), Some(id(&state, "$.name")));
1903 let _ = state.update(Event::CollapseOrLeave);
1904 assert_eq!(state.highlighted(), Some(id(&state, "$.name")), "no parent");
1905
1906 let _ = state.update(Event::Press(tags));
1907 let _ = state.update(Event::ToggleHighlighted);
1908 assert!(state.is_expanded(tags));
1909 let _ = state.update(Event::ExpandSiblings);
1910 assert!(state.is_expanded(id(&state, "$.address")));
1911 let _ = state.update(Event::Focus(false));
1912 assert!(!state.is_focused());
1913 }
1914
1915 #[test]
1916 fn search_finds_keys_and_values_and_expands_their_parents() {
1917 let mut state = sample();
1918 let _ = state.update(Event::Search("LONDON".into()));
1919 let city = id(&state, "$.address.city");
1920 assert_eq!(state.matches(), &[city]);
1921 assert!(state.is_expanded(id(&state, "$.address")));
1922 assert_eq!(state.highlighted(), Some(city));
1923 assert_eq!(state.current_match(), Some(city));
1924 assert_eq!(match_label(&state), "1 of 1");
1925
1926 let _ = state.update(Event::Search("a".into()));
1927 let found: Vec<String> = state.matches().iter().map(|&id| state.path(id)).collect();
1928 assert_eq!(
1929 found,
1930 [
1931 "$.name",
1932 "$.tags",
1933 "$.tags[0]",
1934 "$.address",
1935 "$.active",
1936 "$.age"
1937 ]
1938 );
1939 let _ = state.update(Event::Search("1".into()));
1940 assert!(state.matches().is_empty(), "indices are not searched");
1941 assert_eq!(match_label(&state), "None");
1942 let _ = state.update(Event::Search(" ".into()));
1943 assert!(state.matches().is_empty());
1944 assert_eq!(match_label(&state), "");
1945 let _ = state.update(Event::Search("36".into()));
1946 assert_eq!(state.matches(), &[id(&state, "$.age")], "numbers as text");
1947 }
1948
1949 #[test]
1950 fn typing_jumps_to_the_nearest_match_at_or_after_the_highlight() {
1951 let mut state = sample().with_depth(1);
1952 let address = id(&state, "$.address");
1953 let _ = state.update(Event::Press(address));
1954 let _ = state.update(Event::Search("a".into()));
1955 assert_eq!(state.current_match(), Some(address), "at the highlight");
1956 let _ = state.update(Event::Search("ag".into()));
1957 assert_eq!(state.current_match(), Some(id(&state, "$.age")), "after it");
1958 assert_eq!(match_label(&state), "2 of 2", "tags comes first");
1959 let _ = state.update(Event::Search("name".into()));
1960 assert_eq!(
1961 state.current_match(),
1962 Some(id(&state, "$.name")),
1963 "the last before it when nothing follows"
1964 );
1965 }
1966
1967 #[test]
1968 fn matches_cycle_both_ways() {
1969 let mut state = sample();
1970 let _ = state.update(Event::NextMatch);
1971 assert_eq!(state.current_match(), None, "nothing to cycle");
1972 let _ = state.update(Event::Search("ad".into()));
1973 let matches = state.matches().to_vec();
1974 assert_eq!(matches.len(), 3);
1975 let _ = state.update(Event::NextMatch);
1976 assert_eq!(state.current_match(), Some(matches[1]));
1977 assert_eq!(state.highlighted(), Some(matches[1]));
1978 let _ = state.update(Event::NextMatch);
1979 let _ = state.update(Event::NextMatch);
1980 assert_eq!(state.current_match(), Some(matches[0]), "wraps");
1981 let _ = state.update(Event::PreviousMatch);
1982 assert_eq!(state.current_match(), Some(matches[2]), "wraps back");
1983 assert_eq!(match_label(&state), "3 of 3");
1984 }
1985
1986 #[test]
1987 fn the_filter_shows_matches_their_parents_and_their_insides() {
1988 let mut state = State::new(json!({
1989 "users": [{ "name": "Ada", "role": "admin" }, { "name": "Grace" }],
1990 "meta": { "count": 2 }
1991 }))
1992 .with_only_matches(true);
1993 assert!(state.only_matches() && !state.is_filtering());
1994 let _ = state.update(Event::Search("grace".into()));
1995 assert!(state.is_filtering());
1996 let shown: Vec<String> = state.visible().iter().map(|&id| state.path(id)).collect();
1997 assert_eq!(shown, ["$.users", "$.users[1]", "$.users[1].name"]);
1998
1999 let _ = state.update(Event::Search("users".into()));
2000 let _ = state.update(Event::Expand(id(&state, "$.users")));
2001 let shown: Vec<String> = state.visible().iter().map(|&id| state.path(id)).collect();
2002 assert_eq!(
2003 shown,
2004 ["$.users", "$.users[0]", "$.users[1]", "$.users[1].name"],
2005 "a match shows what is inside"
2006 );
2007 let _ = state.update(Event::OnlyMatches(false));
2008 assert_eq!(state.visible().len(), 5);
2009 let _ = state.update(Event::Search("nothing".into()));
2010 let _ = state.update(Event::OnlyMatches(true));
2011 assert!(state.visible().is_empty());
2012 }
2013
2014 #[test]
2015 fn a_large_document_flattens_and_searches_quickly() {
2016 let users: Vec<Value> = (0..2_000)
2017 .map(|index| json!({ "id": index, "name": format!("user {index}"), "active": index % 2 == 0 }))
2018 .collect();
2019 let started = std::time::Instant::now();
2020 let mut state = State::new(json!({ "users": users }));
2021 assert_eq!(state.len(), 1 + 1 + 2_000 * 4);
2022 let _ = state.update(Event::ExpandAll);
2023 assert_eq!(state.visible().len(), 1 + 2_000 * 4);
2024 let _ = state.update(Event::Search("user 1999".into()));
2025 assert_eq!(state.matches().len(), 1);
2026 let _ = state.update(Event::Last);
2027 assert!(started.elapsed().as_secs() < 5, "{:?}", started.elapsed());
2028 }
2029
2030 #[test]
2031 fn deeply_nested_values_are_fine() {
2032 let mut value = json!(1);
2033 for _ in 0..100 {
2034 value = json!([value]);
2035 }
2036 let mut state = State::new(value);
2037 let _ = state.update(Event::ExpandAll);
2038 assert_eq!(state.visible().len(), 100);
2039 let last = *state.visible().last().expect("a row");
2040 assert_eq!(state.path(last).matches('[').count(), 100);
2041 assert_eq!(
2042 state.update(Event::CopyValue(last)),
2043 Some(Output::CopyValue("1".into()))
2044 );
2045 }
2046
2047 #[test]
2048 fn strings_are_shown_escaped_on_one_line() {
2049 let state = State::new(json!({ "note": "two\nlines \"quoted\"" }));
2050 let note = state.roots()[0];
2051 assert_eq!(
2052 state.item(note).map(|item| item.text.as_str()),
2053 Some("\"two\\nlines \\\"quoted\\\"\"")
2054 );
2055 }
2056
2057 #[test]
2058 fn default_keymap_covers_every_action() {
2059 let keymap = default_keymap();
2060 let press = |chord: &str| {
2061 let chord: Chord = chord.parse().ok()?;
2062 keymap.resolve(chord.key(), chord.modifiers())
2063 };
2064 assert_eq!(press("ArrowDown"), Some(Action::Next));
2065 assert_eq!(press("ArrowUp"), Some(Action::Previous));
2066 assert_eq!(press("Home"), Some(Action::First));
2067 assert_eq!(press("End"), Some(Action::Last));
2068 assert_eq!(press("ArrowRight"), Some(Action::Expand));
2069 assert_eq!(press("ArrowLeft"), Some(Action::Collapse));
2070 assert_eq!(press("Enter"), Some(Action::Toggle));
2071 assert_eq!(press("Space"), None, "left free, as nothing is selected");
2072 assert_eq!(press("*"), Some(Action::ExpandSiblings));
2073 assert_eq!(press("Mod+Shift+ArrowRight"), Some(Action::ExpandAll));
2074 assert_eq!(press("Mod+Shift+ArrowLeft"), Some(Action::CollapseAll));
2075 assert_eq!(
2076 press("Shift+ArrowRight"),
2077 None,
2078 "Shift+Arrow extends a selection elsewhere"
2079 );
2080 assert_eq!(press("Shift+ArrowLeft"), None);
2081 assert_eq!(press("Mod+C"), Some(Action::CopyValue));
2082 assert_eq!(press("Mod+Shift+C"), Some(Action::CopyPath));
2083 assert_eq!(press("Mod+F"), Some(Action::FocusSearch));
2084 assert_eq!(press("Escape"), Some(Action::ClearSearch));
2085 assert_eq!(press("F3"), Some(Action::NextMatch));
2086 assert_eq!(press("Shift+F3"), Some(Action::PreviousMatch));
2087 for action in <Action as keys::Action>::ALL {
2088 assert!(!keymap.chords(action).is_empty(), "{action:?}");
2089 assert!(keys::Action::description(*action).ends_with('.'));
2090 }
2091 assert_eq!(Keymap::<Action>::defaults(), keymap);
2092 }
2093
2094 #[test]
2095 fn keymap_overrides_and_unbinding() {
2096 let keymap = default_keymap()
2097 .unbind(&Chord::character('*'))
2098 .bind(Chord::character('l'), Action::Expand);
2099 let mut state = sample();
2100 let _ = state.update(Event::First);
2101 assert!(
2102 state
2103 .key_event(&keymap, &key(Key::Character("*".into()), Modifiers::SHIFT))
2104 .is_none()
2105 );
2106 assert_eq!(
2107 state.key_event(
2108 &keymap,
2109 &key(Key::Character("l".into()), Modifiers::empty())
2110 ),
2111 Some(Event::ExpandOrEnter)
2112 );
2113 assert!(default_keymap().clear().is_empty());
2114 }
2115
2116 #[test]
2117 fn key_events_resolve_shifted_symbols_and_copy_chords() {
2118 let keymap = default_keymap();
2119 let mut state = sample();
2120 let _ = state.update(Event::First);
2121 let name = id(&state, "$.name");
2122 assert_eq!(
2123 state.key_event(&keymap, &key(Key::Character("*".into()), Modifiers::SHIFT)),
2124 Some(Event::ExpandSiblings)
2125 );
2126 assert_eq!(
2127 state.key_event(
2128 &keymap,
2129 &key(Key::Character("c".into()), Modifiers::COMMAND)
2130 ),
2131 Some(Event::CopyValue(name))
2132 );
2133 assert_eq!(
2134 state.key_event(
2135 &keymap,
2136 &key(
2137 Key::Character("C".into()),
2138 Modifiers::COMMAND | Modifiers::SHIFT
2139 )
2140 ),
2141 Some(Event::CopyPath(name))
2142 );
2143 assert!(
2144 state
2145 .key_event(
2146 &keymap,
2147 &key(Key::Character("x".into()), Modifiers::empty())
2148 )
2149 .is_none()
2150 );
2151 }
2152
2153 #[test]
2154 fn actions_map_to_events_only_when_they_apply() {
2155 let mut state = sample();
2156 assert_eq!(Action::Next.event(&state), Some(Event::Next));
2157 assert_eq!(Action::Expand.event(&state), None, "nothing highlighted");
2158 assert_eq!(Action::CopyValue.event(&state), None);
2159 assert_eq!(Action::NextMatch.event(&state), None, "no search");
2160 assert_eq!(Action::FocusSearch.event(&state), Some(Event::FocusSearch));
2161 assert_eq!(Action::ClearSearch.event(&state), None, "no search");
2162 assert_eq!(Action::ExpandAll.event(&state), Some(Event::ExpandAll));
2163 assert_eq!(Action::CollapseAll.event(&state), Some(Event::CollapseAll));
2164 let _ = state.update(Event::First);
2165 assert_eq!(Action::Toggle.event(&state), None, "a scalar");
2166 let _ = state.update(Event::Next);
2167 assert_eq!(Action::Toggle.event(&state), Some(Event::ToggleHighlighted));
2168 assert_eq!(Action::Collapse.event(&state), Some(Event::CollapseOrLeave));
2169 assert_eq!(
2170 Action::ExpandSiblings.event(&state),
2171 Some(Event::ExpandSiblings)
2172 );
2173 let tags = id(&state, "$.tags");
2174 assert_eq!(Action::CopyPath.event(&state), Some(Event::CopyPath(tags)));
2175 let _ = state.update(Event::Search("o".into()));
2176 assert_eq!(
2177 Action::ClearSearch.event(&state),
2178 Some(Event::Search(String::new()))
2179 );
2180 assert_eq!(Action::NextMatch.event(&state), Some(Event::NextMatch));
2181 assert_eq!(
2182 Action::PreviousMatch.event(&state),
2183 Some(Event::PreviousMatch)
2184 );
2185 assert_eq!(state.update(Event::FocusSearch), Some(Output::FocusSearch));
2186 }
2187
2188 #[test]
2189 fn menu_items_map_to_events() {
2190 let node = NodeId(3);
2191 assert_eq!(MenuItem::ALL.len(), 4);
2192 assert_eq!(MenuItem::CopyValue.event(node), Event::CopyValue(node));
2193 assert_eq!(MenuItem::CopyPath.event(node), Event::CopyPath(node));
2194 assert_eq!(MenuItem::ExpandAll.event(node), Event::ExpandSubtree(node));
2195 assert_eq!(
2196 MenuItem::CollapseAll.event(node),
2197 Event::CollapseSubtree(node)
2198 );
2199 assert!(!menu().is_open());
2200 }
2201
2202 #[test]
2203 fn syntax_colours_come_from_tokens_in_both_themes() {
2204 for theme in [light(), dark()] {
2205 let tokens = Tokens::of(&theme);
2206 let syntax = syntax(&tokens);
2207 assert_eq!(syntax.key, tokens.foreground);
2208 assert_eq!(syntax.punctuation, tokens.muted_foreground);
2209 assert_eq!(syntax.null, tokens.muted_foreground);
2210 assert_ne!(syntax.string, tokens.foreground);
2211 assert_ne!(syntax.number, tokens.foreground);
2212 let colours = [syntax.string, syntax.number, syntax.boolean];
2213 for (index, colour) in colours.iter().enumerate() {
2214 assert!(colours.iter().skip(index + 1).all(|other| other != colour));
2215 }
2216 if tokens.is_dark {
2217 assert_eq!(syntax.string, tokens.success);
2218 } else {
2219 assert_eq!(syntax.string, mix(tokens.success, tokens.foreground, 0.25));
2220 }
2221 assert_eq!(value_colour(&tokens, Kind::Number), syntax.number);
2222 assert_eq!(value_colour(&tokens, Kind::Bool), syntax.boolean);
2223 assert_eq!(value_colour(&tokens, Kind::Null), syntax.null);
2224 assert_eq!(value_colour(&tokens, Kind::Object), syntax.punctuation);
2225 assert_eq!(value_colour(&tokens, Kind::Array), syntax.punctuation);
2226 assert_eq!(value_colour(&tokens, Kind::String), syntax.string);
2227 }
2228 }
2229
2230 fn contrast(a: Color, b: Color) -> f32 {
2232 let channel = |c: f32| {
2233 if c <= 0.039_28 {
2234 c / 12.92
2235 } else {
2236 ((c + 0.055) / 1.055).powf(2.4)
2237 }
2238 };
2239 let luminance =
2240 |c: Color| 0.2126 * channel(c.r) + 0.7152 * channel(c.g) + 0.0722 * channel(c.b);
2241 let (x, y) = (luminance(a), luminance(b));
2242 (x.max(y) + 0.05) / (x.min(y) + 0.05)
2243 }
2244
2245 #[test]
2246 fn booleans_are_calm_and_readable_in_both_themes() {
2247 for theme in [light(), dark()] {
2248 let tokens = Tokens::of(&theme);
2249 let boolean = value_colour(&tokens, Kind::Bool);
2250 assert_eq!(boolean, tokens.primary);
2251 assert_ne!(boolean, tokens.destructive, "true is not an error");
2252 assert_ne!(boolean, mix(tokens.destructive, tokens.foreground, 0.25));
2253 let ratio = contrast(boolean, tokens.background);
2254 assert!(ratio >= 4.5, "dark: {}, ratio {ratio}", tokens.is_dark);
2255 for kind in [Kind::String, Kind::Number] {
2256 let ratio = contrast(value_colour(&tokens, kind), tokens.background);
2257 assert!(ratio >= 3.0, "{kind:?} dark: {}, {ratio}", tokens.is_dark);
2258 }
2259 }
2260 assert_eq!(value_font(Kind::Bool), theme::semibold());
2261 assert_eq!(value_font(Kind::String), theme::font());
2262 }
2263
2264 #[test]
2265 fn the_frame_is_a_rounded_border() {
2266 for theme in [light(), dark()] {
2267 let tokens = Tokens::of(&theme);
2268 let style = frame_style(&tokens);
2269 assert_eq!(style.border.color, tokens.border);
2270 assert_eq!(style.border.width, 1.0);
2271 assert!(style.background.is_none());
2272 }
2273 }
2274
2275 #[test]
2276 fn rows_in_every_status_and_theme() {
2277 for theme in [light(), dark()] {
2278 let tokens = Tokens::of(&theme);
2279 let idle = row_style(&tokens, Status::default());
2280 assert_eq!(
2281 idle,
2282 RowStyle {
2283 background: None,
2284 ring: None
2285 }
2286 );
2287
2288 let hovered = row_style(
2289 &tokens,
2290 Status {
2291 hovered: true,
2292 ..Status::default()
2293 },
2294 );
2295 assert!(
2296 hovered
2297 .background
2298 .is_some_and(|colour| colour.a < tokens.accent.a)
2299 );
2300
2301 let matched = row_style(
2302 &tokens,
2303 Status {
2304 matched: true,
2305 ..Status::default()
2306 },
2307 );
2308 let current = row_style(
2309 &tokens,
2310 Status {
2311 matched: true,
2312 current: true,
2313 ..Status::default()
2314 },
2315 );
2316 let alpha = |style: RowStyle| style.background.map_or(0.0, |colour| colour.a);
2317 assert!(alpha(current) > alpha(matched) && alpha(matched) > 0.0);
2318
2319 let highlighted = row_style(
2320 &tokens,
2321 Status {
2322 highlighted: true,
2323 matched: true,
2324 ..Status::default()
2325 },
2326 );
2327 assert_eq!(highlighted.background, matched.background, "no fill");
2328 assert_eq!(highlighted.ring, None, "the ring needs focus");
2329 let focused = row_style(
2330 &tokens,
2331 Status {
2332 highlighted: true,
2333 focused: true,
2334 ..Status::default()
2335 },
2336 );
2337 assert_eq!(focused.background, None);
2338 assert_eq!(focused.ring, Some(tokens.ring));
2339 let current_focused = row_style(
2340 &tokens,
2341 Status {
2342 highlighted: true,
2343 focused: true,
2344 matched: true,
2345 current: true,
2346 ..Status::default()
2347 },
2348 );
2349 assert_eq!(
2350 current_focused.background, current.background,
2351 "the current match keeps its tint under the ring"
2352 );
2353
2354 let disabled = row_style(
2355 &tokens,
2356 Status {
2357 highlighted: true,
2358 focused: true,
2359 hovered: true,
2360 matched: true,
2361 current: true,
2362 disabled: true,
2363 },
2364 );
2365 assert_eq!(
2366 disabled,
2367 RowStyle {
2368 background: None,
2369 ring: None
2370 }
2371 );
2372 }
2373 }
2374
2375 #[test]
2376 fn builder_defaults_and_options() {
2377 let state = sample();
2378 let view: JsonViewer<'_, ()> = json_viewer(&state);
2379 assert!(!view.search_bar && !view.is_enabled());
2380 assert!(view.frame);
2381 assert_eq!(view.placeholder, "Search");
2382 assert_eq!((view.width, view.height), (Length::Fill, Length::Shrink));
2383 assert_eq!(view.key_width, KEY_WIDTH);
2384 assert_eq!(view.keymap, default_keymap());
2385 let view = view
2386 .search_bar(true)
2387 .placeholder("Find")
2388 .frame(false)
2389 .key_width(120.0)
2390 .width(300)
2391 .height(200)
2392 .keymap(Keymap::new())
2393 .context_menu_keymap(Keymap::new())
2394 .on_event(|_| ());
2395 assert!(view.search_bar && view.is_enabled() && !view.frame);
2396 assert_eq!(view.placeholder, "Find");
2397 assert_eq!(state.search_id(), &SEARCH);
2398 let other = sample().with_search_id("other");
2399 assert_eq!(other.search_id(), &widget::Id::new("other"));
2400 assert_eq!(view.key_width, 120.0);
2401 assert_eq!(view.width, Length::Fixed(300.0));
2402 assert!(view.keymap.is_empty() && view.menu_keymap.is_empty());
2403 assert!(format!("{view:?}").starts_with("JsonViewer"));
2404 }
2405}