use quadraui::compose::app_shell::{AppShellEvent, AppShellLayout, PanelDefinition};
use quadraui::{
filter_help_actions, help_actions_to_palette_items, Backend, Color, DualModePaletteController,
DualModePaletteEvent, HelpAction, HelpNote, HelpOverlayController, HelpOverlayEvent,
HelpRegistry, InteractionState, Key, NamedKey, Reaction, Rect, ShellApp, ShellConfig,
ShellContext, StatusBar, StatusBarSegment, UiEvent, ViewHelp, WidgetId,
};
const EXPLORER_PANEL: &str = "panel:explorer";
const GIT_PANEL: &str = "panel:git";
const SETTINGS_PANEL: &str = "panel:settings";
pub struct HelpLayerDemo {
registry: HelpRegistry,
help_overlay: HelpOverlayController,
palette: Option<DualModePaletteController>,
active_panel: WidgetId,
last_message: String,
}
impl HelpLayerDemo {
pub fn new() -> Self {
let mut registry = HelpRegistry::new();
registry.register(
EXPLORER_PANEL,
ViewHelp::new("Explorer")
.with_notes(vec![
HelpNote::new("●", "File has unsaved changes"),
HelpNote::new("M", "File modified since last commit"),
])
.with_actions(vec![
HelpAction::new("explorer.new_file", "New File", "Create a new file")
.with_accelerator("Ctrl+N"),
HelpAction::new(
"explorer.reveal",
"Reveal in Finder",
"Show the selected file on disk",
)
.with_accelerator("Ctrl+Shift+R"),
]),
);
registry.register(
GIT_PANEL,
ViewHelp::new("Source Control")
.with_notes(vec![
HelpNote::new("M", "Modified"),
HelpNote::new("A", "Added"),
HelpNote::new("U", "Untracked"),
])
.with_actions(vec![
HelpAction::new("git.commit", "Commit", "Commit staged changes")
.with_accelerator("Ctrl+Enter"),
HelpAction::new(
"git.discard",
"Discard Changes",
"Revert the selected file to HEAD",
),
]),
);
Self {
registry,
help_overlay: HelpOverlayController::new(),
palette: None,
active_panel: WidgetId::new(EXPLORER_PANEL),
last_message: "?=help p=commands Tab=focus bar q=quit".into(),
}
}
pub fn config() -> ShellConfig {
ShellConfig::new(
"Help Layer Demo",
vec![
PanelDefinition {
id: WidgetId::new(EXPLORER_PANEL),
icon: "E".into(),
tooltip: "Explorer".into(),
title: "EXPLORER".into(),
},
PanelDefinition {
id: WidgetId::new(GIT_PANEL),
icon: "G".into(),
tooltip: "Source Control".into(),
title: "SOURCE CONTROL".into(),
},
PanelDefinition {
id: WidgetId::new(SETTINGS_PANEL),
icon: "S".into(),
tooltip: "Settings (no help registered)".into(),
title: "SETTINGS".into(),
},
],
)
}
fn active_view_help(&self) -> Option<&ViewHelp> {
self.registry.get(self.active_panel.as_str())
}
fn filtered_actions(&self, query: &str) -> Vec<&HelpAction> {
filter_help_actions(self.registry.actions_for(self.active_panel.as_str()), query)
}
fn open_palette(&mut self) {
let items = help_actions_to_palette_items(self.filtered_actions(""), "");
self.palette = Some(DualModePaletteController::new("Commands", None, items));
self.last_message =
"Command palette open — type to search, Enter to run, Esc to cancel".into();
}
}
impl Default for HelpLayerDemo {
fn default() -> Self {
Self::new()
}
}
impl ShellApp for HelpLayerDemo {
fn render_content(&self, backend: &mut dyn Backend, layout: &AppShellLayout) {
let lh = backend.line_height();
let hint = StatusBar {
id: WidgetId::new("main-label"),
left_segments: vec![StatusBarSegment {
text: format!(" {} ", self.last_message),
fg: Color::rgb(200, 200, 200),
bg: Color::rgb(30, 30, 30),
bold: false,
action_id: None,
}],
right_segments: vec![StatusBarSegment {
text: format!(" {} ", self.active_panel.as_str()),
fg: Color::rgb(140, 200, 255),
bg: Color::rgb(20, 40, 70),
bold: true,
action_id: None,
}],
};
let rect = Rect::new(
layout.main_content_bounds.x,
layout.main_content_bounds.y,
layout.main_content_bounds.width,
lh,
);
backend.draw_status_bar_interactive(rect, &hint, &InteractionState::new());
if let Some(palette) = &self.palette {
let popup = popup_rect(layout.window_bounds, backend);
palette.render(popup, backend);
} else {
self.help_overlay
.render(layout.window_bounds, backend, self.active_view_help());
}
}
fn handle(
&mut self,
event: UiEvent,
backend: &mut dyn Backend,
ctx: &ShellContext,
) -> Reaction {
if let Some(mut palette) = self.palette.take() {
let popup = popup_rect(ctx.window_bounds(), backend);
let lh = backend.line_height();
let visible_rows = if lh > 0.0 {
((popup.height / lh) as usize).saturating_sub(quadraui::PALETTE_CHROME_ROWS)
} else {
10
};
let reaction = match palette.handle(&event, visible_rows) {
DualModePaletteEvent::ItemConfirmed { idx } => {
let query = palette.query().to_string();
let matched = self.filtered_actions(&query);
if let Some(action) = matched.get(idx) {
self.last_message = format!("Ran: {}", action.label);
}
Reaction::Redraw
}
DualModePaletteEvent::QueryChanged { value } => {
let matched = self.filtered_actions(&value);
palette.set_items(help_actions_to_palette_items(matched, &value));
self.palette = Some(palette);
return Reaction::Redraw;
}
DualModePaletteEvent::Cancelled => {
self.last_message = "Command palette closed".into();
Reaction::Redraw
}
DualModePaletteEvent::TextConfirmed { .. }
| DualModePaletteEvent::ModeToggled { .. }
| DualModePaletteEvent::Consumed => {
self.palette = Some(palette);
return Reaction::Redraw;
}
DualModePaletteEvent::Ignored => {
self.palette = Some(palette);
return Reaction::Continue;
}
};
return reaction;
}
match self.help_overlay.handle(&event) {
HelpOverlayEvent::Opened => {
self.last_message = format!("Help — {}", self.active_panel.as_str());
return Reaction::Redraw;
}
HelpOverlayEvent::Closed => {
self.last_message = "Help closed".into();
return Reaction::Redraw;
}
HelpOverlayEvent::Consumed => return Reaction::Redraw,
HelpOverlayEvent::Ignored => {}
}
match event {
UiEvent::KeyPressed {
key: Key::Char('p'),
..
} => {
self.open_palette();
Reaction::Redraw
}
UiEvent::KeyPressed {
key: Key::Named(NamedKey::Tab),
..
} => {
ctx.request_activity_keyboard_focus();
self.last_message = "Activity bar focused (j/k, Enter, Esc)".into();
Reaction::Redraw
}
UiEvent::KeyPressed {
key: Key::Char('q') | Key::Named(NamedKey::Escape),
..
} => Reaction::Exit,
_ => Reaction::Continue,
}
}
fn on_shell_event_ctx(&mut self, event: &AppShellEvent, _ctx: &ShellContext) {
if let AppShellEvent::PanelChanged { panel_id } = event {
self.active_panel = *panel_id;
self.last_message = format!("Panel: {}", panel_id.as_str());
}
}
}
fn popup_rect(container: Rect, backend: &dyn Backend) -> Rect {
let lh = backend.line_height();
let cw = backend.char_width();
let w = (container.width * 0.6).max(cw * 40.0).min(container.width);
let h = (container.height * 0.6)
.max(lh * 10.0)
.min(container.height);
let x = container.x + (container.width - w) * 0.5;
let y = container.y + (container.height - h) * 0.5;
Rect::new(x, y, w, h)
}