Skip to main content

iced_cube/data/
json_viewer.rs

1//! A JSON document shown as rows that expand and collapse.
2//!
3//! [`State`] flattens a [`serde_json::Value`] into nodes, or parses JSON
4//! text with [`State::parse`] and keeps the error when it is not valid. It
5//! holds which objects and arrays are expanded, the highlighted node and
6//! the search. Its [`update`](State::update) handles pointer and keyboard
7//! navigation, expanding everything or to a depth, searching and copying.
8//!
9//! Objects and arrays show how many members they hold, `{3}` or `[12]`.
10//! Values are coloured by type from the theme's [`Tokens`], see [`syntax`],
11//! and a long value ends in an ellipsis on one line. Copy it to read it in
12//! full. The view only builds the rows in view, so a document with
13//! thousands of nodes stays quick.
14//!
15//! Copying never touches the clipboard itself: [`Output::CopyValue`] holds
16//! the value as pretty-printed JSON and [`Output::CopyPath`] its path, such
17//! as `$.users[2].name`, for the app to write. Paths start at `$`, the
18//! document, and use `["key"]` for keys that are not plain identifiers.
19//!
20//! The viewer resolves its keys through a [`Keymap`] of [`Action`]s while
21//! it has focus, which it takes when pressed; see [`default_keymap`].
22
23use 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
50/// Height of every row, as in a menu.
51pub const ROW_HEIGHT: f32 = 32.0;
52/// Width of one level of indentation, which is also the chevron's slot.
53pub const INDENT: f32 = 20.0;
54/// The widest a key gets before it ends in an ellipsis, by default.
55pub const KEY_WIDTH: f32 = 180.0;
56/// The id of the search box, unless [`State::with_search_id`] sets another.
57pub const SEARCH: widget::Id = widget::Id::new("json-viewer-search");
58
59/// A node in the document, numbered in document order.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
61pub struct NodeId(usize);
62
63impl NodeId {
64    /// The node's position in document order, starting at the document
65    /// itself.
66    pub fn index(self) -> usize {
67        self.0
68    }
69}
70
71/// The type of a JSON value.
72#[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    /// Whether values of this kind hold other values.
104    pub fn is_container(self) -> bool {
105        matches!(self, Kind::Object | Kind::Array)
106    }
107}
108
109/// How a node is reached from its parent.
110#[derive(Debug, Clone, PartialEq, Eq, Hash)]
111pub enum Segment {
112    /// A member of an object.
113    Key(String),
114    /// An element of an array.
115    Index(usize),
116}
117
118/// Why JSON text could not be read.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct ParseError {
121    pub message: String,
122    /// Where the problem is, counted from 1.
123    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    /// A scalar as JSON, such as `"text"` or `12`. Empty for containers.
160    text: String,
161    /// The lower-case key and value the search looks in.
162    key_lower: Option<String>,
163    text_lower: String,
164    children: Vec<NodeId>,
165}
166
167/// Everything that changes a viewer.
168#[derive(Debug, Clone, PartialEq)]
169pub enum Event {
170    /// Expands or collapses a node, as its chevron does.
171    Toggle(NodeId),
172    Expand(NodeId),
173    Collapse(NodeId),
174    /// Expands a node and everything under it.
175    ExpandSubtree(NodeId),
176    /// Collapses a node and everything under it.
177    CollapseSubtree(NodeId),
178    ExpandAll,
179    CollapseAll,
180    /// Expands every object and array fewer than this many levels deep and
181    /// collapses the rest, so `0` collapses everything.
182    ExpandToDepth(usize),
183    /// The pointer pressed a row.
184    Press(NodeId),
185    Next,
186    Previous,
187    First,
188    Last,
189    /// Expands the highlighted node, or moves to its first child when it
190    /// is already expanded.
191    ExpandOrEnter,
192    /// Collapses the highlighted node, or moves to its parent when it is
193    /// already collapsed.
194    CollapseOrLeave,
195    ToggleHighlighted,
196    /// Expands the highlighted node and every sibling that has children.
197    ExpandSiblings,
198    /// Searches keys and values for this text, ignoring case, and expands
199    /// the parents of every match.
200    Search(String),
201    NextMatch,
202    PreviousMatch,
203    /// Shows only matches, their parents and what is inside them.
204    OnlyMatches(bool),
205    CopyValue(NodeId),
206    CopyPath(NodeId),
207    /// Asks the app to focus the search box.
208    FocusSearch,
209    /// The viewer gained or lost focus. The widget sends this itself.
210    Focus(bool),
211}
212
213/// What the app may need to act on after an [`Event`].
214#[derive(Debug, Clone, PartialEq, Eq)]
215pub enum Output {
216    /// A value as pretty-printed JSON, to write to the clipboard.
217    CopyValue(String),
218    /// A path such as `$.users[2].name`, to write to the clipboard.
219    CopyPath(String),
220    /// Focus the search box, with [`focus_search`].
221    FocusSearch,
222}
223
224/// The document, what is expanded and highlighted, and the search.
225#[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    /// The ancestors of every match.
237    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    /// A viewer of `value` with everything collapsed. An object or array
252    /// shows its members as the top rows; anything else shows as one row.
253    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    /// Parses JSON text. Text that is not valid JSON gives an empty viewer
274    /// that shows the [`error`](Self::error) in place of rows.
275    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    /// Starts with every object and array fewer than `depth` levels deep
293    /// expanded.
294    pub fn with_depth(mut self, depth: usize) -> Self {
295        self.expand_to_depth(depth);
296        self
297    }
298
299    /// Starts with only the matches shown while searching.
300    pub fn with_only_matches(mut self, only_matches: bool) -> Self {
301        self.only_matches = only_matches;
302        self
303    }
304
305    /// The search box's widget id. Defaults to [`SEARCH`]; give each viewer
306    /// its own when a window has several.
307    pub fn with_search_id(mut self, id: impl Into<widget::Id>) -> Self {
308        self.search_id = id.into();
309        self
310    }
311
312    /// The search box's widget id, which [`focus_search`] focuses.
313    pub fn search_id(&self) -> &widget::Id {
314        &self.search_id
315    }
316
317    /// The whole document.
318    pub fn value(&self) -> &Value {
319        &self.value
320    }
321
322    /// Why the text given to [`parse`](Self::parse) is not valid JSON.
323    pub fn error(&self) -> Option<&ParseError> {
324        self.error.as_ref()
325    }
326
327    /// How many nodes the document holds, the document itself included.
328    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    /// The top rows.
337    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    /// The key or index the node has in its parent, `None` for the document.
346    pub fn segment(&self, id: NodeId) -> Option<&Segment> {
347        self.item(id).and_then(|item| item.segment.as_ref())
348    }
349
350    /// The node's parent row, `None` for a top row.
351    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    /// How far the row is indented: 0 for a top row.
360    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    /// The node the keyboard acts on.
369    pub fn highlighted(&self) -> Option<NodeId> {
370        self.highlighted
371    }
372
373    pub fn is_focused(&self) -> bool {
374        self.focused
375    }
376
377    /// The search text.
378    pub fn query(&self) -> &str {
379        &self.query
380    }
381
382    /// The nodes whose key or value contains the search text, in document
383    /// order.
384    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    /// The match [`Event::NextMatch`] last moved to.
393    pub fn current_match(&self) -> Option<NodeId> {
394        self.current
395            .and_then(|index| self.matches.get(index))
396            .copied()
397    }
398
399    /// Whether only matches are shown: [`only_matches`](Self::only_matches)
400    /// is on and there is a search.
401    pub fn is_filtering(&self) -> bool {
402        self.only_matches && !self.query.trim().is_empty()
403    }
404
405    /// Whether a search shows only its matches, searching or not.
406    pub fn only_matches(&self) -> bool {
407        self.only_matches
408    }
409
410    /// The rows on screen when scrolled through, in document order.
411    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    /// The node's path from the document, such as `$.users[2].name` or
432    /// `$["first name"]`.
433    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    /// The node's value.
452    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    /// The node's value as pretty-printed JSON.
464    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    /// Applies an event and returns what the app may need to act on.
470    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    /// Turns a key press into an event through `keymap`. A symbol typed
539    /// with Shift, such as `*`, also matches its chord without Shift.
540    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    /// The segments from the node up to the document.
565    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    /// The node and everything under it.
579    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    /// Moves a highlight hidden in a collapsed branch up to the outermost
613    /// collapsed node above it.
614    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        // The nearest match: the first at or after the highlight, or else
722        // the last before it.
723        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
757/// Whether a key can follow a dot in a path.
758fn 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
767/// The nodes of `value` in document order, and the top rows. The members
768/// of an object or array that has any are the top rows; otherwise the
769/// document is the only row.
770fn 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/// What a viewer keyboard shortcut does.
854#[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    /// The [`Event`] this action sends to `state`, or `None` when it does
876    /// nothing, so the key is left for others.
877    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
976/// The default viewer shortcuts:
977///
978/// | Keys | Action |
979/// | --- | --- |
980/// | `ArrowDown` | [`Action::Next`] |
981/// | `ArrowUp` | [`Action::Previous`] |
982/// | `Home` | [`Action::First`] |
983/// | `End` | [`Action::Last`] |
984/// | `ArrowRight` | [`Action::Expand`] |
985/// | `ArrowLeft` | [`Action::Collapse`] |
986/// | `Enter` | [`Action::Toggle`] |
987/// | `*` | [`Action::ExpandSiblings`] |
988/// | `Mod+Shift+ArrowRight` | [`Action::ExpandAll`] |
989/// | `Mod+Shift+ArrowLeft` | [`Action::CollapseAll`] |
990/// | `Mod+C` (Ctrl, or Cmd on macOS) | [`Action::CopyValue`] |
991/// | `Mod+Shift+C` | [`Action::CopyPath`] |
992/// | `Mod+F` | [`Action::FocusSearch`] |
993/// | `Escape` | [`Action::ClearSearch`] |
994/// | `F3` | [`Action::NextMatch`] |
995/// | `Shift+F3` | [`Action::PreviousMatch`] |
996///
997/// Enter does what a double click does, as in the tree, where it activates
998/// a node: here it opens or closes an object or array. Space is left free,
999/// since the viewer has nothing to select, and Shift+Arrow is left alone,
1000/// as other views extend a selection with it. While the search box has
1001/// focus, Enter and F3 step to the next match and Shift+Enter and Shift+F3
1002/// to the previous one, Mod+F selects its text, and Escape clears it, then
1003/// a second Escape leaves it. The viewer resolves these itself,
1004/// only while it has focus, so they never clash with other components.
1005/// Pass a changed keymap with [`JsonViewer::keymap`].
1006pub 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/// An entry of the context menu from [`menu`].
1033#[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    /// The event the entry sends for the node the menu opened on.
1050    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
1060/// A context menu to pass to [`JsonViewer::context_menu`]: copy the value
1061/// or its path, and expand or collapse everything inside it. Turn the
1062/// chosen entry into an event with [`MenuItem::event`].
1063pub 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/// The colours of keys, values and punctuation.
1074#[derive(Debug, Clone, Copy, PartialEq)]
1075pub struct Syntax {
1076    pub key: Color,
1077    /// Array indices and the `{3}` and `[12]` summaries.
1078    pub punctuation: Color,
1079    pub string: Color,
1080    pub number: Color,
1081    pub boolean: Color,
1082    pub null: Color,
1083}
1084
1085/// Resolves the syntax colours from the theme: strings take the success
1086/// colour, numbers the warning colour and booleans the primary colour,
1087/// drawn semibold so they stand apart from keys. In light themes strings
1088/// and numbers are darkened towards the text so they stay readable on a
1089/// white page.
1090pub 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
1108/// The colour of a value of this kind.
1109pub 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
1120/// The font of a value of this kind: semibold for booleans, so they stand
1121/// apart from keys when the primary colour is close to the text colour.
1122pub fn value_font(kind: Kind) -> Font {
1123    match kind {
1124        Kind::Bool => theme::semibold(),
1125        _ => theme::font(),
1126    }
1127}
1128
1129/// How a row is shown.
1130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1131pub struct Status {
1132    /// The row the keyboard acts on.
1133    pub highlighted: bool,
1134    /// Draws the focus ring on the highlighted row.
1135    pub focused: bool,
1136    pub hovered: bool,
1137    /// The key or value contains the search text.
1138    pub matched: bool,
1139    /// The match the search moved to last.
1140    pub current: bool,
1141    pub disabled: bool,
1142}
1143
1144/// The colours of a row.
1145#[derive(Debug, Clone, Copy, PartialEq)]
1146pub struct RowStyle {
1147    pub background: Option<Color>,
1148    pub ring: Option<Color>,
1149}
1150
1151/// The border around the rows, as the property grid and data table have.
1152pub 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
1163/// Resolves a row's colours. The viewer selects nothing, so the highlighted
1164/// row only carries a ring, and only while the viewer has focus. Search
1165/// matches take a tint of the warning colour, stronger on the current one.
1166pub 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
1192/// A JSON viewer builder. Convert it into an [`Element`] to render.
1193pub 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
1207/// Renders `state`. Without [`on_event`](JsonViewer::on_event) it renders
1208/// disabled.
1209pub 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    /// Shows a search box above the rows, with the match count, buttons
1242    /// for the previous and next match and a toggle that shows only
1243    /// matches.
1244    pub fn search_bar(mut self, search_bar: bool) -> Self {
1245        self.search_bar = search_bar;
1246        self
1247    }
1248
1249    /// The search box placeholder. Defaults to "Search".
1250    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1251        self.placeholder = placeholder.into();
1252        self
1253    }
1254
1255    /// Draws a 1px rounded border around the rows, as the property grid
1256    /// and data table have. On by default; turn it off when the viewer
1257    /// already sits in a card or panel.
1258    pub fn frame(mut self, frame: bool) -> Self {
1259        self.frame = frame;
1260        self
1261    }
1262
1263    /// The widest a key gets before it ends in an ellipsis. Defaults to
1264    /// [`KEY_WIDTH`].
1265    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    /// The height of the rows. A fixed or filling height scrolls them.
1276    /// Defaults to shrinking to fit them.
1277    pub fn height(mut self, height: impl Into<Length>) -> Self {
1278        self.height = height.into();
1279        self
1280    }
1281
1282    /// Replaces the [`default_keymap`] the focused viewer resolves keys with.
1283    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    /// Gives every row the context menu of `menu`, keyed by node, such as
1294    /// the one from [`menu()`]. It opens on a right-click or a
1295    /// long press, and on the highlighted row with its opening chord while
1296    /// the viewer has focus.
1297    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    /// Replaces the context menu's default keymap.
1313    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
1323/// Everything a row needs from the builder.
1324struct Context<'a, Message> {
1325    state: &'a State,
1326    key_width: f32,
1327    /// The row a context menu is open on.
1328    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
1445/// Focuses the search box of `state`'s viewer, in answer to
1446/// [`Output::FocusSearch`].
1447pub fn focus_search<Message>(state: &State) -> Task<Message> {
1448    widget::operation::focus(state.search_id().clone())
1449}
1450
1451/// The text beside the search box: "3 of 12" for the current match of all
1452/// matches, "12 found" before one is current, "None" when nothing matches,
1453/// and empty without a search.
1454pub 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
1505/// The indentation for `depth` levels.
1506fn 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    /// ```text
1684    /// name: "Ada"
1685    /// tags: [2]
1686    ///   0: "admin"
1687    ///   1: "ops"
1688    /// address: {2}
1689    ///   city: "London"
1690    ///   first line: null
1691    /// active: true
1692    /// age: 36
1693    /// ```
1694    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    /// The WCAG contrast ratio of two opaque colours.
2231    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}