Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Search Panel

Group: Data

Code

struct SearchMatch {
    line: usize,
    text: &'static str,
}

struct FileResult {
    path: &'static str,
    matches: &'static [SearchMatch],
    expanded: bool,
}

pub struct SearchPanelDemo {
    results: Vec<Vec<FileResult>>,
    selected_path: Vec<Option<Vec<u16>>>,
    last_message: Vec<String>,
}

impl SearchPanelDemo {
    pub fn new() -> Self {
        Self {
            results: vec![fake_results(), Vec::new()],
            selected_path: vec![None, None],
            last_message: vec![
                "Click a result to jump".into(),
                "No matches for this query".into(),
            ],
        }
    }

    fn build_tree_rows(&self, variant: usize) -> Vec<TreeRow> {
        let mut rows = Vec::new();
        for (fi, file) in self.results[variant].iter().enumerate() {
            rows.push(TreeRow {
                path: vec![fi as u16],
                indent: 0,
                icon: None,
                text: StyledText {
                    spans: vec![
                        StyledSpan::plain(file.path),
                        StyledSpan {
                            text: format!(" ({} matches)", file.matches.len()),
                            fg: Some(Color::rgb(120, 120, 120)),
                            bg: None,
                            bold: false,
                            italic: false,
                            underline: false,
                        },
                    ],
                },
                badge: None,
                is_expanded: Some(file.expanded),
                decoration: Decoration::Header,
                edit: None,
            });
            if file.expanded {
                for (mi, m) in file.matches.iter().enumerate() {
                    rows.push(TreeRow {
                        path: vec![fi as u16, mi as u16],
                        indent: 1,
                        icon: None,
                        text: StyledText {
                            spans: vec![
                                StyledSpan {
                                    text: format!("{:>4}: ", m.line),
                                    fg: Some(Color::rgb(100, 100, 100)),
                                    bg: None,
                                    bold: false,
                                    italic: false,
                                    underline: false,
                                },
                                StyledSpan::plain(m.text),
                            ],
                        },
                        badge: None,
                        is_expanded: None,
                        decoration: Decoration::Normal,
                        edit: None,
                    });
                }
            }
        }
        rows
    }

    fn build_view(&self, variant: usize) -> MultiSectionView {
        let rows = self.build_tree_rows(variant);
        let tree = TreeView {
            id: WidgetId::new("gallery:search-panel:results"),
            rows,
            selection_mode: quadraui::SelectionMode::Single,
            selected_path: self.selected_path[variant].clone(),
            scroll_offset: 0,
            style: quadraui::TreeStyle::default(),
            has_focus: true,
        };
        MultiSectionView {
            id: WidgetId::new("gallery:search-panel"),
            sections: vec![Section {
                id: "results".into(),
                header: SectionHeader {
                    icon: None,
                    title: StyledText::plain("SEARCH RESULTS"),
                    badge: None,
                    actions: vec![],
                    show_chevron: false,
                },
                body: SectionBody::Tree(tree),
                aux: Some(SectionAux::Search(InlineInput {
                    id: WidgetId::new("gallery:search-panel:input"),
                    text: String::new(),
                    caret: 0,
                    placeholder: Some("Search".into()),
                    has_focus: false,
                })),
                size: SectionSize::EqualShare,
                collapsed: false,
                min_size: None,
                max_size: None,
            }],
            active_section: Some(0),
            axis: quadraui::MsvAxis::Vertical,
            allow_resize: false,
            allow_collapse: false,
            scroll_mode: quadraui::ScrollMode::PerSection,
            has_focus: true,
            panel_scroll: 0.0,
        }
    }

    fn status(&self, variant: usize) -> StatusBar {
        let match_count: usize = self.results[variant].iter().map(|f| f.matches.len()).sum();
        let file_count = self.results[variant].len();
        StatusBar {
            id: WidgetId::new("gallery:search-panel:status"),
            left_segments: vec![StatusBarSegment {
                text: format!(" {} ", self.last_message[variant]),
                fg: Color::rgb(255, 255, 255),
                bg: Color::rgb(40, 80, 120),
                bold: false,
                action_id: None,
            }],
            right_segments: vec![StatusBarSegment {
                text: format!(" {match_count} matches in {file_count} files "),
                fg: Color::rgb(220, 220, 220),
                bg: Color::rgb(40, 80, 120),
                bold: false,
                action_id: None,
            }],
        }
    }

    fn panel_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0))
    }

    fn status_rect(area: Rect, backend: &dyn Backend) -> Rect {
        let lh = backend.line_height();
        Rect::new(area.x, area.y + area.height - lh, area.width, lh)
    }
}

impl Default for SearchPanelDemo {
    fn default() -> Self {
        Self::new()
    }
}

impl Demo for SearchPanelDemo {
    fn name(&self) -> &'static str {
        "Search Panel"
    }

    fn group(&self) -> &'static str {
        "Data"
    }

    fn variants(&self) -> &'static [&'static str] {
        &["Results found", "No matches"]
    }

    fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
        let panel_rect = Self::panel_rect(area, backend);
        backend.draw_multi_section_view(panel_rect, &self.build_view(variant));

        let status_rect = Self::status_rect(area, backend);
        let _ = backend.draw_status_bar_interactive(
            status_rect,
            &self.status(variant),
            &InteractionState::new(),
        );
    }

    fn handle(
        &mut self,
        variant: usize,
        event: &UiEvent,
        backend: &mut dyn Backend,
        area: Rect,
    ) -> Reaction {
        if let UiEvent::MouseDown { position, .. } = event {
            let panel_rect = Self::panel_rect(area, backend);
            let view = self.build_view(variant);
            let layout = backend.msv_layout(panel_rect, &view);
            match layout.hit_test(position.x, position.y) {
                MultiSectionViewHit::Aux {
                    kind: AuxHit::Input,
                    ..
                } => {
                    self.last_message[variant] = "Input focused".into();
                }
                MultiSectionViewHit::Body { section, .. } => {
                    if let Some(sl) = layout.sections.get(section) {
                        if let SectionBody::Tree(ref tree) = view.sections[section].body {
                            let tree_layout = backend.tree_layout(sl.body_bounds, tree);
                            let local_x = position.x - sl.body_bounds.x;
                            let local_y = position.y - sl.body_bounds.y;
                            match tree_layout.hit_test(local_x, local_y) {
                                TreeViewHit::Chevron(row_idx) | TreeViewHit::Row(row_idx)
                                    if tree.rows[row_idx].is_expanded.is_some() =>
                                {
                                    let fi = tree.rows[row_idx].path[0] as usize;
                                    if let Some(file) = self.results[variant].get_mut(fi) {
                                        file.expanded = !file.expanded;
                                        self.last_message[variant] = format!(
                                            "{} {}",
                                            if file.expanded {
                                                "Expanded"
                                            } else {
                                                "Collapsed"
                                            },
                                            file.path
                                        );
                                    }
                                }
                                TreeViewHit::Row(row_idx) => {
                                    let row = &tree.rows[row_idx];
                                    self.selected_path[variant] = Some(row.path.clone());
                                    if row.path.len() >= 2 {
                                        let fi = row.path[0] as usize;
                                        let mi = row.path[1] as usize;
                                        if let Some(file) = self.results[variant].get(fi) {
                                            if let Some(m) = file.matches.get(mi) {
                                                self.last_message[variant] =
                                                    format!("Jump: {}:{}", file.path, m.line);
                                            }
                                        }
                                    }
                                }
                                TreeViewHit::Chevron(_) | TreeViewHit::Empty => {
                                    self.last_message[variant] = "Empty area".into();
                                }
                            }
                        }
                    }
                }
                _ => {}
            }
            return Reaction::Redraw;
        }
        Reaction::Continue
    }

    fn source(&self) -> &'static str {
        extract_region(SOURCE)
    }

    fn data(&self, variant: usize) -> serde_json::Value {
        serde_json::json!({
            "file_count": self.results[variant].len(),
            "match_count": self.results[variant].iter().map(|f| f.matches.len()).sum::<usize>(),
            "last_message": self.last_message[variant],
        })
    }

    fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
        None
    }
}

Backends

VariantTUIGTKmacOSWindows
Results found
No matches