1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! Backend-agnostic app code for the shell + `MenuSystem` regression demo
//! ([`tui_shell_menu`] / [`gtk_shell_menu`]).
//!
//! When an app-level modal visually overlaps `AppShell` chrome (activity bar,
//! sidebar, dividers, bottom-panel grip), it must still receive its `MouseDown`:
//! the chrome must not swallow it via pure position-based hit-testing just
//! because the click's on-screen coordinates land inside a chrome region.
//!
//! The menu bar lives in the shell's title-bar band (`ShellConfig::
//! with_title_bar`) — a plain click on "File" reaches the app normally,
//! since `AppShell::handle` doesn't intercept title-bar clicks. But the
//! "File" menu's *dropdown* opens directly below the bar, at the same x
//! as the leftmost bar item — which puts it squarely over the activity
//! bar strip underneath. Clicking a dropdown item whose position falls
//! inside the activity bar's bounds must still activate it, not be consumed
//! as an activity-bar click.
//!
//! Controls:
//! - click "File" (or Alt+F) opens the dropdown over the activity bar
//! - click a dropdown item activates it, even items drawn over the
//! activity bar strip
//! - click an activity bar icon switches panels (when no dropdown is open)
//! - q / Esc quit (when no dropdown is open)
use quadraui::compose::app_shell::{AppShellEvent, AppShellLayout, PanelDefinition};
use quadraui::{
Backend, Color, ContextMenuItem, InteractionState, Key, MenuDef, MenuEvent, MenuSystem,
NamedKey, Reaction, Rect, ShellApp as ShellAppTrait, ShellConfig, ShellContext, StatusBar,
StatusBarSegment, StyledText, UiEvent, WidgetId,
};
pub struct ShellMenuDemo {
menu_system: MenuSystem,
last_action: String,
}
impl ShellMenuDemo {
pub fn new() -> Self {
Self {
menu_system: MenuSystem::new(vec![
MenuDef {
id: WidgetId::new("file"),
label: "&File".into(),
disabled: false,
items: vec![
action("new", "New File"),
action("open", "Open File"),
separator(),
action("quit", "Quit"),
],
},
MenuDef {
id: WidgetId::new("edit"),
label: "&Edit".into(),
disabled: false,
items: vec![action("undo", "Undo"), action("redo", "Redo")],
},
]),
last_action: "click File, then click an item over the activity bar".into(),
}
}
pub fn config() -> ShellConfig {
ShellConfig::new(
"Shell Menu Demo",
vec![
PanelDefinition {
id: WidgetId::new("panel:explorer"),
icon: "E".into(),
tooltip: "Explorer".into(),
title: "EXPLORER".into(),
},
PanelDefinition {
id: WidgetId::new("panel:search"),
icon: "S".into(),
tooltip: "Search".into(),
title: "SEARCH".into(),
},
],
)
// A title-bar band to host the menu bar. `AppShell::handle` doesn't
// intercept title-bar clicks, so opening the menu always reaches
// the app — it's the dropdown *below* it, anchored at the same x
// as the "File" bar item, that overlaps the activity bar strip
// (still 3 line-heights wide, `ShellConfig`'s default) beneath it.
.with_title_bar(1.0)
}
}
impl Default for ShellMenuDemo {
fn default() -> Self {
Self::new()
}
}
impl ShellAppTrait for ShellMenuDemo {
fn render_content(&self, backend: &mut dyn Backend, layout: &AppShellLayout) {
if let Some(bar_rect) = layout.title_bar_bounds {
self.menu_system.render(backend, bar_rect);
}
let lh = backend.line_height();
let status = StatusBar {
id: WidgetId::new("shell-menu-status"),
left_segments: vec![StatusBarSegment {
text: format!(" {} ", self.last_action),
fg: Color::rgb(200, 200, 200),
bg: Color::rgb(30, 30, 30),
bold: false,
action_id: None,
}],
right_segments: vec![],
};
let status_rect = Rect::new(
layout.main_content_bounds.x,
layout.main_content_bounds.y,
layout.main_content_bounds.width,
lh.min(layout.main_content_bounds.height),
);
let _ = backend.draw_status_bar_interactive(status_rect, &status, &InteractionState::new());
}
fn handle(
&mut self,
event: UiEvent,
backend: &mut dyn Backend,
ctx: &ShellContext,
) -> Reaction {
let Some(bar_rect) = ctx.title_bar_bounds() else {
return Reaction::Continue;
};
match self.menu_system.handle(&event, backend, bar_rect) {
MenuEvent::Activated(id) => {
self.last_action = format!("activated: {}", id.as_str());
if id.as_str() == "quit" {
return Reaction::Exit;
}
Reaction::Redraw
}
MenuEvent::StateChanged | MenuEvent::Consumed => Reaction::Redraw,
MenuEvent::Ignored => match event {
UiEvent::KeyPressed {
key: Key::Char('q'),
..
}
| UiEvent::KeyPressed {
key: Key::Named(NamedKey::Escape),
..
} => Reaction::Exit,
UiEvent::WindowResized { .. } => Reaction::Redraw,
_ => Reaction::Continue,
},
}
}
fn on_shell_event_ctx(&mut self, event: &AppShellEvent, _ctx: &ShellContext) {
match event {
AppShellEvent::PanelChanged { panel_id } => {
self.last_action = format!("Panel: {}", panel_id.as_str());
}
AppShellEvent::SidebarHidden => {
self.last_action = "Sidebar hidden".into();
}
_ => {}
}
}
}
fn action(id: &str, label: &str) -> ContextMenuItem {
ContextMenuItem {
id: Some(WidgetId::new(id)),
label: StyledText::plain(label),
..Default::default()
}
}
fn separator() -> ContextMenuItem {
ContextMenuItem::default()
}