Sidebar
Group: Chrome
Code
const REVEAL_ROW_COUNT: usize = 30;
const BODY_ROW_COUNT: usize = 40;
fn flat_rows(n: usize) -> Vec<TreeRow> {
(0..n)
.map(|i| TreeRow {
path: vec![i as u16],
indent: 0,
icon: None,
text: StyledText::plain(format!("item{i}")),
badge: None,
is_expanded: None,
decoration: Decoration::Normal,
edit: None,
})
.collect()
}
/// Rows for the "Header groups" variant: two file-header rows, each
/// followed by two match rows — `Decoration::Header` vs. `Normal`.
fn grouped_rows() -> Vec<TreeRow> {
let mut rows = Vec::new();
for (file_idx, file) in ["src/main.rs", "src/lib.rs"].iter().enumerate() {
rows.push(TreeRow {
path: vec![file_idx as u16],
indent: 0,
icon: None,
text: StyledText::plain(*file),
badge: Some(quadraui::Badge::plain("2")),
is_expanded: None,
decoration: Decoration::Header,
edit: None,
});
for match_idx in 0..2 {
rows.push(TreeRow {
path: vec![file_idx as u16, match_idx as u16],
indent: 1,
icon: None,
text: StyledText::plain(format!(" match {match_idx} in {file}")),
badge: None,
is_expanded: None,
decoration: Decoration::Normal,
edit: None,
});
}
}
rows
}
struct TreeBody(TreeView);
impl BackendWidget for TreeBody {
fn render(&self, backend: &mut dyn Backend, rect: Rect) {
backend.draw_tree(rect, &self.0);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ChromeMode {
None,
Header,
HeaderAndSearch,
}
impl ChromeMode {
fn next(self) -> Self {
match self {
ChromeMode::None => ChromeMode::Header,
ChromeMode::Header => ChromeMode::HeaderAndSearch,
ChromeMode::HeaderAndSearch => ChromeMode::None,
}
}
}
pub struct SidebarDemo {
// Variant 0: tree + reveal.
reveal_sidebar: SidebarSystem,
reveal_last: String,
// Variant 1: SidebarPanel + toolbar + tasks.
tasks: Vec<String>,
tasks_selected: Option<usize>,
filter_on: bool,
tasks_last: String,
hover: ToolbarHoverTracker,
pressed: Option<WidgetId>,
next_task_id: usize,
// Variant 2: header groups.
groups_sidebar: SidebarSystem,
groups_last: String,
// Variant 3: SidebarPanelBody + chrome cycle.
body_rows: Vec<TreeRow>,
body_scroll: usize,
chrome_mode: ChromeMode,
body_last: String,
}
impl SidebarDemo {
pub fn new() -> Self {
let mut reveal_sidebar = SidebarSystem::new(vec![SidebarSectionDef::new("items", "ITEMS")]);
reveal_sidebar.set_rows(0, flat_rows(REVEAL_ROW_COUNT));
reveal_sidebar.set_navigation_mode(NavigationMode::Selection);
reveal_sidebar.set_active_section(Some(0));
let mut groups_sidebar =
SidebarSystem::new(vec![SidebarSectionDef::new("results", "RESULTS")]);
groups_sidebar.set_rows(0, grouped_rows());
groups_sidebar.set_navigation_mode(NavigationMode::Selection);
groups_sidebar.set_active_section(Some(0));
Self {
reveal_sidebar,
reveal_last: "—".into(),
tasks: vec![
"Review PR #257".into(),
"Write toolbar tests".into(),
"Land sidebar panel".into(),
],
tasks_selected: Some(0),
filter_on: false,
tasks_last: "click toolbar buttons, then list rows".into(),
hover: ToolbarHoverTracker::new(),
pressed: None,
next_task_id: 4,
groups_sidebar,
groups_last: "—".into(),
body_rows: flat_rows(BODY_ROW_COUNT),
body_scroll: 0,
chrome_mode: ChromeMode::HeaderAndSearch,
body_last: "—".into(),
}
}
// ── Variant 1 helpers ────────────────────────────────────────────
fn toolbar(&self) -> Toolbar {
Toolbar::new(WidgetId::new("gallery:sidebar:toolbar")).with_buttons(vec![
ToolbarButton::Action {
id: WidgetId::new("gallery:sidebar:add"),
label: "".into(),
icon: Some("+".into()),
key_hint: None,
enabled: true,
is_active: false,
tooltip: "Add task".into(),
},
ToolbarButton::Separator,
ToolbarButton::Action {
id: WidgetId::new("gallery:sidebar:filter"),
label: "Filter".into(),
icon: Some("⚙".into()),
key_hint: None,
enabled: true,
is_active: self.filter_on,
tooltip: "Toggle filter".into(),
},
ToolbarButton::Action {
id: WidgetId::new("gallery:sidebar:clear"),
label: "Clear".into(),
icon: None,
key_hint: None,
enabled: !self.tasks.is_empty(),
is_active: false,
tooltip: "Remove all tasks".into(),
},
])
}
fn panel(&self) -> SidebarPanel {
SidebarPanel {
id: WidgetId::new("gallery:sidebar:panel"),
toolbar: Some(self.toolbar()),
toolbar_height: Some(2.0),
}
}
fn dispatch_toolbar(&mut self, id: &WidgetId) {
match id.as_str() {
"gallery:sidebar:add" => {
self.tasks.push(format!("Task #{}", self.next_task_id));
self.next_task_id += 1;
self.tasks_last = "Added a task".into();
}
"gallery:sidebar:filter" => {
self.filter_on = !self.filter_on;
self.tasks_last = format!("Filter {}", if self.filter_on { "on" } else { "off" });
}
"gallery:sidebar:clear" => {
self.tasks.clear();
self.tasks_selected = None;
self.tasks_last = "Cleared all tasks".into();
}
_ => {}
}
}
// ── Variant 3 helpers ────────────────────────────────────────────
fn body_chrome(&self) -> SidebarPanelChrome {
match self.chrome_mode {
ChromeMode::None => SidebarPanelChrome::None,
ChromeMode::Header => SidebarPanelChrome::Header("ITEMS".into()),
ChromeMode::HeaderAndSearch => SidebarPanelChrome::HeaderAndSearch {
header: "ITEMS".into(),
query: String::new(),
placeholder: "Filter items".into(),
active: false,
},
}
}
}
impl Default for SidebarDemo {
fn default() -> Self {
Self::new()
}
}
impl Demo for SidebarDemo {
fn name(&self) -> &'static str {
"Sidebar"
}
fn group(&self) -> &'static str {
"Chrome"
}
fn variants(&self) -> &'static [&'static str] {
&[
"Tree + reveal",
"Toolbar header",
"Header groups",
"Body chrome",
]
}
fn render(&self, variant: usize, backend: &mut dyn Backend, area: Rect) {
let lh = backend.line_height();
match variant {
0 => {
let sidebar_rect =
Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0));
self.reveal_sidebar.render(backend, sidebar_rect);
let hint = Rect::new(area.x, area.y + sidebar_rect.height, area.width, lh);
let bar = StatusBar {
id: WidgetId::new("gallery:sidebar:reveal-hint"),
left_segments: vec![StatusBarSegment {
text: format!(" last: {} ", self.reveal_last),
fg: Color::rgb(220, 220, 220),
bg: Color::rgb(40, 40, 60),
bold: false,
action_id: None,
}],
right_segments: vec![StatusBarSegment {
text: " ↑↓ select · z collapse · g reveal ".into(),
fg: Color::rgb(220, 220, 220),
bg: Color::rgb(40, 40, 60),
bold: false,
action_id: None,
}],
};
let _ = backend.draw_status_bar_interactive(hint, &bar, &InteractionState::new());
}
1 => {
let panel_rect = Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0));
let layout = backend.draw_sidebar_panel_interactive(
panel_rect,
&self.panel(),
&InteractionState::from_parts(self.hover.current(), self.pressed),
);
let content = layout.content_bounds;
let header_text = if self.filter_on {
" Tasks (filtered) "
} else {
" Tasks "
};
let _ = backend.draw_status_bar_interactive(
Rect::new(content.x, content.y, content.width, lh),
&StatusBar {
id: WidgetId::new("gallery:sidebar:tasks-header"),
left_segments: vec![StatusBarSegment {
text: header_text.into(),
fg: Color::rgb(200, 200, 200),
bg: Color::rgb(20, 20, 20),
bold: true,
action_id: None,
}],
right_segments: vec![],
},
&InteractionState::new(),
);
for (i, task) in self.tasks.iter().enumerate() {
let row_y = content.y + lh * (1.0 + i as f32);
if row_y + lh > content.y + content.height {
break;
}
let (fg, bg) = if Some(i) == self.tasks_selected {
(Color::rgb(255, 255, 255), Color::rgb(60, 100, 160))
} else {
(Color::rgb(220, 220, 220), Color::rgb(20, 20, 20))
};
let _ = backend.draw_status_bar_interactive(
Rect::new(content.x, row_y, content.width, lh),
&StatusBar {
id: WidgetId::new(format!("gallery:sidebar:task-row:{i}")),
left_segments: vec![StatusBarSegment {
text: format!(" {task} "),
fg,
bg,
bold: false,
action_id: None,
}],
right_segments: vec![],
},
&InteractionState::new(),
);
}
let hint = Rect::new(area.x, area.y + panel_rect.height, area.width, lh);
let bar = StatusBar {
id: WidgetId::new("gallery:sidebar:tasks-hint"),
left_segments: vec![StatusBarSegment {
text: format!(" {} ", self.tasks_last),
fg: Color::rgb(220, 220, 220),
bg: Color::rgb(40, 40, 60),
bold: false,
action_id: None,
}],
right_segments: vec![],
};
let _ = backend.draw_status_bar_interactive(hint, &bar, &InteractionState::new());
}
2 => {
let sidebar_rect =
Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0));
self.groups_sidebar.render(backend, sidebar_rect);
let hint = Rect::new(area.x, area.y + sidebar_rect.height, area.width, lh);
let bar = StatusBar {
id: WidgetId::new("gallery:sidebar:groups-hint"),
left_segments: vec![StatusBarSegment {
text: format!(" last: {} ", self.groups_last),
fg: Color::rgb(220, 220, 220),
bg: Color::rgb(40, 40, 60),
bold: false,
action_id: None,
}],
right_segments: vec![],
};
let _ = backend.draw_status_bar_interactive(hint, &bar, &InteractionState::new());
}
_ => {
let panel_rect = Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0));
let panel = SidebarPanelBody {
background: Some(Color::rgb(24, 24, 28)),
chrome: self.body_chrome(),
scrollbar_gutter: Some(backend.char_width().max(1.0)),
};
let tree = TreeView {
id: WidgetId::new("gallery:sidebar:body-tree"),
rows: self.body_rows.clone(),
selection_mode: quadraui::SelectionMode::Single,
selected_path: None,
scroll_offset: self.body_scroll,
style: TreeStyle::default(),
has_focus: true,
};
let layout = panel.render(backend, panel_rect, &TreeBody(tree));
if let Some(sb_rect) = layout.scrollbar_rect {
let line_h = lh.max(1.0);
let visible = (sb_rect.height / line_h).max(1.0);
let sb = Scrollbar::vertical(
WidgetId::new("gallery:sidebar:body-scrollbar"),
sb_rect,
self.body_scroll as f32,
self.body_rows.len() as f32,
visible,
line_h,
);
backend.draw_scrollbar(sb_rect, &sb);
}
let hint = Rect::new(area.x, area.y + panel_rect.height, area.width, lh);
let bar = StatusBar {
id: WidgetId::new("gallery:sidebar:body-hint"),
left_segments: vec![StatusBarSegment {
text: format!(" chrome={:?} last: {} ", self.chrome_mode, self.body_last),
fg: Color::rgb(220, 220, 220),
bg: Color::rgb(40, 40, 60),
bold: false,
action_id: None,
}],
right_segments: vec![StatusBarSegment {
text: " ↑↓ scroll · c cycle chrome ".into(),
fg: Color::rgb(220, 220, 220),
bg: Color::rgb(40, 40, 60),
bold: false,
action_id: None,
}],
};
let _ = backend.draw_status_bar_interactive(hint, &bar, &InteractionState::new());
}
}
}
fn handle(
&mut self,
variant: usize,
event: &UiEvent,
backend: &mut dyn Backend,
area: Rect,
) -> Reaction {
let lh = backend.line_height();
match variant {
0 => {
self.reveal_sidebar
.set_backend_info(backend.line_height(), backend.msv_metrics());
let rect = Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0));
match self.reveal_sidebar.handle(event, backend, rect) {
SidebarEvent::RowSelected { section, path } => {
self.reveal_last = format!("sel→{section} {path:?}");
Reaction::Redraw
}
SidebarEvent::StateChanged
| SidebarEvent::Consumed
| SidebarEvent::ScrollChanged { .. } => Reaction::Redraw,
SidebarEvent::Ignored => match event {
UiEvent::KeyPressed {
key: quadraui::Key::Char('z'),
..
} => {
let collapsed = !self.reveal_sidebar.is_collapsed(0);
self.reveal_sidebar.set_collapsed(0, collapsed);
self.reveal_last =
format!("{}→0", if collapsed { "collapsed" } else { "expanded" });
Reaction::Redraw
}
UiEvent::KeyPressed {
key: quadraui::Key::Char('g'),
..
} => {
let target = vec![(REVEAL_ROW_COUNT - 1) as u16];
self.reveal_sidebar.reveal(0, &target, rect);
self.reveal_last = format!("reveal→0 {target:?}");
Reaction::Redraw
}
_ => Reaction::Continue,
},
_ => Reaction::Redraw,
}
}
1 => match event {
UiEvent::MouseMoved { position, .. } => {
let panel_rect =
Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0));
let layout = backend.sidebar_panel_layout(panel_rect, &self.panel());
let mut new_hover = self.hover.clone();
if let Some(tlayout) = &layout.toolbar_layout {
new_hover.update(tlayout, position.x, position.y);
} else {
new_hover.clear();
}
if new_hover.current() != self.hover.current() {
self.hover = new_hover;
Reaction::Redraw
} else {
Reaction::Continue
}
}
UiEvent::MouseDown { position, .. } => {
let panel_rect =
Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0));
let layout = backend.sidebar_panel_layout(panel_rect, &self.panel());
match layout.hit_test(position.x, position.y) {
SidebarPanelHit::ToolbarButton(id) => {
self.pressed = Some(id);
Reaction::Redraw
}
SidebarPanelHit::Content { y, .. } => {
if y >= lh {
let idx = ((y - lh) / lh).floor() as usize;
if idx < self.tasks.len() {
self.tasks_selected = Some(idx);
self.tasks_last = format!("Selected: {}", self.tasks[idx]);
return Reaction::Redraw;
}
}
Reaction::Continue
}
_ => Reaction::Continue,
}
}
UiEvent::MouseUp { position, .. } => {
let panel_rect =
Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0));
let layout = backend.sidebar_panel_layout(panel_rect, &self.panel());
let pressed = self.pressed.take();
if let (Some(pressed_id), SidebarPanelHit::ToolbarButton(release_id)) =
(pressed, layout.hit_test(position.x, position.y))
{
if pressed_id == release_id {
self.dispatch_toolbar(&release_id);
}
return Reaction::Redraw;
}
Reaction::Redraw
}
_ => Reaction::Continue,
},
2 => {
let rect = Rect::new(area.x, area.y, area.width, (area.height - lh).max(0.0));
match self.groups_sidebar.handle(event, backend, rect) {
SidebarEvent::RowSelected { section, path } => {
self.groups_last = format!("sel→{section} {path:?}");
Reaction::Redraw
}
SidebarEvent::HeaderActivated { section } => {
self.groups_last = format!("header→{section}");
Reaction::Redraw
}
SidebarEvent::StateChanged
| SidebarEvent::Consumed
| SidebarEvent::ScrollChanged { .. } => Reaction::Redraw,
SidebarEvent::Ignored => Reaction::Continue,
_ => Reaction::Redraw,
}
}
_ => match event {
UiEvent::KeyPressed {
key: quadraui::Key::Char('c'),
..
} => {
self.chrome_mode = self.chrome_mode.next();
self.body_last = format!("chrome→{:?}", self.chrome_mode);
Reaction::Redraw
}
UiEvent::KeyPressed {
key: quadraui::Key::Named(quadraui::NamedKey::Up),
..
} => {
self.body_scroll = self.body_scroll.saturating_sub(1);
Reaction::Redraw
}
UiEvent::KeyPressed {
key: quadraui::Key::Named(quadraui::NamedKey::Down),
..
} => {
let max = self.body_rows.len().saturating_sub(1);
self.body_scroll = (self.body_scroll + 1).min(max);
Reaction::Redraw
}
_ => Reaction::Continue,
},
}
}
fn source(&self) -> &'static str {
extract_region(SOURCE)
}
fn data(&self, variant: usize) -> serde_json::Value {
match variant {
0 => serde_json::json!({ "last": self.reveal_last, "row_count": REVEAL_ROW_COUNT }),
1 => serde_json::json!({ "tasks": self.tasks, "selected": self.tasks_selected }),
2 => serde_json::json!({ "last": self.groups_last }),
_ => serde_json::json!({
"chrome_mode": format!("{:?}", self.chrome_mode),
"row_count": self.body_rows.len(),
}),
}
}
fn caps_note(&self, _variant: usize, _caps: &BackendCaps) -> Option<String> {
None
}
}
Backends
| Variant | TUI | GTK | macOS | Windows |
|---|---|---|---|---|
| Tree + reveal | ||||
| Toolbar header | ||||
| Header groups | ||||
| Body chrome |