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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT
//! Menu bar widget.
use crate::core::{Color, HorizontalAlignment, Point, Rect};
use crate::event::key_codes;
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::Signal1;
use crate::widget::capability::properties_trait::{base_property_get, base_property_set};
use crate::widget::capability::types::{CapabilityAccessError, CapabilityValue};
use crate::widget::capability::WidgetProperties;
use crate::widget::metrics::{dimensions, effective_font, estimate_text_width, ControlMetrics};
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
use crate::{impl_widget_property_hooks, property_names_of};
/// Horizontal gap on each side of a menu-bar title.
///
/// Ten pixels is the macOS menu-bar spacing at body size: enough that adjacent titles read as
/// separate targets, and included in `entry_width` so the hit box and the painted label agree.
const MENU_ENTRY_TEXT_PADDING: u32 = 10;
/// A top-level menu entry in the menu bar.
///
/// Entries carry only a title and an enabled flag; the menu contents they open
/// live in a separate menu model, not here.
#[derive(Debug, Clone)]
pub struct MenuBarEntry {
title: String,
enabled: bool,
}
impl MenuBarEntry {
/// Creates an enabled entry with the given title.
pub fn new(title: impl Into<String>) -> Self {
Self { title: title.into(), enabled: true }
}
// --- Accessors ---
/// Returns the label drawn in the bar.
pub fn title(&self) -> &str {
&self.title
}
/// Replaces the label. The bar's layout is derived from title width, so
/// changing this shifts the entries that follow it.
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = title.into();
}
/// Returns whether the entry accepts mouse input.
pub fn is_enabled(&self) -> bool {
self.enabled
}
/// Enables or disables the entry.
///
/// A disabled entry is still drawn, in a greyed colour, but click and hover
/// do not activate it — in particular no `triggered` or `hovered_entry`
/// signal is emitted for it.
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
/// Menu bar widget.
///
/// A horizontal strip of top-level entries, exactly one of which may be active
/// (i.e. its drop-down is considered open). The bar manages only the entries,
/// the pointer cursor, and the signals; it does not own or display drop-down
/// menus, so a consumer is expected to open the matching menu in response to
/// [`MenuBar::triggered`].
///
/// Entry hit areas are estimated from title length rather than measured against
/// real glyph advances, so very wide or narrow fonts can make the clickable
/// regions disagree with the drawn text.
///
pub struct MenuBar {
base: BaseWidget,
entries: Vec<MenuBarEntry>,
active_index: Option<usize>,
/// The entry under the pointer, or `None` when the bar is not hovered at all.
///
/// # Why this is still a field, and what changed (BLUE23 §93, M1)
///
/// M1's shape is "delete the private state and read `BaseWidget`" — but `BaseWidget` knows
/// *whether* the pointer is over the control, not **which entry** it is over, and a menu bar is
/// a row of targets. What was wrong was not the field's existence but its having a second,
/// independent source: every `MouseMove` wrote it directly, so the bar's idea of "hovered"
/// could disagree with `BaseWidget`'s — and the theme's `"menu_bar:hover"` key, which the base
/// flag drives, could be true while no entry was highlighted.
///
/// It is now **derived** from the base flag plus the same `hit_entry` the press path uses:
/// one pointer fact, one hit test. A move that arrives while the base says "not hovered"
/// (or a `MouseLeave`) clears it, so the two cannot diverge.
hovered_index: Option<usize>,
/// Emitted with the clicked entry's title when an enabled entry is pressed
/// with the primary mouse button. The payload is the title text, not the
/// index — duplicate titles are therefore indistinguishable, and renaming
/// an entry after the fact makes previously received payloads stale.
pub triggered: Signal1<String>,
/// Emitted with the title of the entry the pointer newly entered. Not
/// emitted when the pointer moves to a different disabled entry, and not
/// re-emitted while the pointer stays on the same entry.
pub hovered_entry: Signal1<String>,
/// Emitted with the clicked entry's title **and** the rectangle it occupies.
///
/// # Why the rectangle is part of the event
///
/// A host that opens the matching drop-down needs to place it under the title that was
/// clicked. Asking the bar for `entry_rect` from inside a `triggered` handler cannot work:
/// the runtime lends the bar out exclusively for the duration of `handle_event`, so a nested
/// read answers `None` and the popup would silently open nowhere. Carrying the rectangle in
/// the event is the same rule as everywhere else — the emitter states the fact while it still
/// has it, rather than making the receiver guess it afterwards.
pub triggered_entry: Signal1<(String, Rect)>,
}
impl MenuBar {
/// Creates an empty bar with no active or hovered entry.
///
/// `geometry` is in parent-relative logical pixels; the size hint is
/// 400x28.
pub fn new(geometry: Rect) -> Self {
Self {
base: BaseWidget::new(WidgetKind::MenuBar, geometry, "MenuBar"),
entries: Vec::new(),
active_index: None,
hovered_index: None,
triggered: Signal1::new(),
hovered_entry: Signal1::new(),
triggered_entry: Signal1::new(),
}
}
/// Returns the entries in visual order.
pub fn entries(&self) -> &[MenuBarEntry] {
&self.entries
}
/// Returns the index of the active entry, or `None` when no entry is
/// active.
///
/// "Active" means the entry was last clicked. It is cleared by pressing
/// Escape and by [`MenuBar::clear`], but **not** by clicking a different
/// entry (that entry simply becomes the new active one) nor by
/// [`MenuBar::remove_menu`].
pub fn active_index(&self) -> Option<usize> {
self.active_index
}
/// Returns the index currently under the pointer, or `None` when the
/// pointer is outside every entry.
pub fn hovered_index(&self) -> Option<usize> {
self.hovered_index
}
/// Appends an entry and returns its index.
///
/// Indices are positions in the list and therefore shift when an earlier
/// entry is removed.
pub fn add_menu(&mut self, title: impl Into<String>) -> usize {
let idx = self.entries.len();
self.entries.push(MenuBarEntry::new(title));
idx
}
/// Removes the entry at `index`; out-of-range indices are ignored.
///
/// The active and hovered cursors are adjusted with the list: a cursor pointing at the
/// removed entry is cleared, and one pointing past it moves down by one. Leaving them
/// untouched made `active_index` name a different title after a removal — or an index
/// that no longer existed at all — while the bar still reported it as the open menu.
pub fn remove_menu(&mut self, index: usize) {
if index >= self.entries.len() {
return;
}
self.entries.remove(index);
self.active_index = Self::shift_cursor(self.active_index, index);
self.hovered_index = Self::shift_cursor(self.hovered_index, index);
self.base.request_redraw();
}
/// Shifts one cursor after the entry at `removed` left the list.
fn shift_cursor(cursor: Option<usize>, removed: usize) -> Option<usize> {
match cursor {
Some(at) if at == removed => None,
Some(at) if at > removed => Some(at - 1),
other => other,
}
}
/// Enables or disables the entry at `index`. Out-of-range indices are
/// ignored, but a redraw is still requested in that case.
pub fn set_menu_enabled(&mut self, index: usize, enabled: bool) {
if let Some(e) = self.entries.get_mut(index) {
e.set_enabled(enabled);
}
self.base.request_redraw();
}
/// Returns whether the entry at `index` is enabled, or `None` when the
/// index is out of range.
pub fn menu_enabled(&self, index: usize) -> Option<bool> {
self.entries.get(index).map(|entry| entry.is_enabled())
}
/// Removes every entry and clears both the active and hovered cursors.
pub fn clear(&mut self) {
self.entries.clear();
self.active_index = None;
self.hovered_index = None;
}
/// The strip the bar actually paints: a full-width band
/// [`dimensions::MENU_BAR_HEIGHT`] tall, pinned to the **top** of the area the control was
/// given.
///
/// # Why the bar is a top band and not a centred one
///
/// A menu bar is not centred chrome: the menu contents it opens begin *below* it, so
/// whatever the layout handed the bar, the strip belongs on the control's top edge.
/// `draw` painted `geometry()` while `size_hint` reported 28 — the two disagreed about how
/// thick a menu bar is, and the 240x120 census cell drew a 120 px band, four times the
/// strip a layout was told to expect (`menu_bar.svg` was a full-canvas fill). The two ends
/// now share one derivation. This is also what `entry_width` and `hit_entry` read, so the
/// clickable strip is the painted strip.
fn band_rect(&self) -> Rect {
ControlMetrics::top_band(self.geometry(), dimensions::MENU_BAR_HEIGHT)
}
fn entry_width(&self, title: &str) -> f32 {
// Measured with the **effective font** through the shared ruler rather than the old
// "8 px per byte + 16" guess (D09-STYLE-01 / D09-STYLE-02): the byte count over-measured
// non-ASCII titles and ignored the font entirely, so a scaled theme font did not widen an
// entry.
//
// The padding must cover what `draw_text_fitted` insets: it reserves
// `TEXT_FIT_MARGIN` at **each** end, so an entry that only added
// `TOOLBAR_ITEM_INSET * 2` (4 px) gave the painter 2 px less than the measured title
// and truncated every label. Measured on the GTK demo before this: "File" painted as
// "…", "Edit" as "E…". The entry now reserves both the fit margin and a real gap.
let font = effective_font(self.style());
(estimate_text_width(title, font, 1.0)
+ 2 * (crate::render::TEXT_FIT_MARGIN + MENU_ENTRY_TEXT_PADDING)) as f32
}
/// The rectangle of the entry at `index`, in the coordinate space the bar's own geometry
/// uses — client coordinates for a bar placed in a window.
///
/// This is the box `draw` paints and `hit_entry` tests, so a consumer that opens the
/// matching menu from a `triggered` title can place the popup flush under the entry it
/// came from instead of guessing an offset. Returns `None` for an out-of-range index.
pub fn entry_rect(&self, index: usize) -> Option<Rect> {
let rect = self.band_rect();
let mut x = rect.x;
for (i, entry) in self.entries.iter().enumerate() {
let w = self.entry_width(entry.title()) as i32;
if i == index {
return Some(Rect { x, y: rect.y, width: w as u32, height: rect.height });
}
x += w;
}
None
}
fn hit_entry(&self, pos: Point) -> Option<usize> {
// The **painted band**, so a click below a 28 px strip is not a click on the bar.
let rect = self.band_rect();
if pos.y < rect.y || pos.y > rect.y + rect.height as f32 as i32 {
return None;
}
let mut x = rect.x;
for (i, entry) in self.entries.iter().enumerate() {
let w = self.entry_width(entry.title()) as i32;
if pos.x >= x && pos.x < x + w {
return Some(i);
}
x += w;
}
None
}
}
impl Widget for MenuBar {
fn base(&self) -> &BaseWidget {
&self.base
}
fn set_state_theme_hook(&mut self) {
crate::style::reapply_active_theme_state(self);
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> crate::core::Size {
crate::core::Size::new(400, dimensions::MENU_BAR_HEIGHT)
}
impl_draw_bridge!();
impl_widget_property_hooks!();
/// Resolves a published event name to the control's own signal.
///
/// # Why this is explicit per control
///
/// `connect_event` validates a name against the capability table and registers a hub slot; only
/// `event_signal_dyn` joins that name to the signal the control actually emits. Without it a name
/// is valid and inert, which is the silent failure `tools/check_event_signal_dyn.sh` exists to
/// make impossible. The arm set is checked against the capability's published names, so this
/// list cannot drift from what the control advertises.
fn event_signal_dyn(&self, name: &str) -> Option<crate::signal::EventSignalRef> {
use crate::signal::EventSignalRef;
#[allow(unused_imports)]
use crate::widget::capability::CapabilityValue;
match name {
"triggered" => Some(EventSignalRef::mapped("triggered", &self.triggered, |v| {
CapabilityValue::String(v.clone())
})),
"triggered_entry" => {
Some(EventSignalRef::mapped("triggered_entry", &self.triggered_entry, |v| {
// The published shape is `Mixed`, so it must arrive as a structured carrier:
// a subscriber reads the title and the rectangle as their own typed values
// rather than re-parsing a debug spelling.
CapabilityValue::Tuple(crate::compat::Vec::from([
CapabilityValue::String(v.0.clone()),
CapabilityValue::Rect(v.1),
]))
}))
}
"hovered_entry" => {
Some(EventSignalRef::mapped("hovered_entry", &self.hovered_entry, |v| {
CapabilityValue::String(v.clone())
}))
}
_ => None,
}
}
}
/// `MenuBar`'s property contract.
///
/// Every published property is a read-only projection of the bar's current
/// contents and pointer state; the schema advertises none of them as writable,
/// so the write path forwards straight to the base helpers.
impl WidgetProperties for MenuBar {
fn get(&self, name: &str) -> Result<CapabilityValue, CapabilityAccessError> {
match name {
"entry_count" => Ok(CapabilityValue::UInt(self.entries().len() as u64)),
"active_index" => match self.active_index() {
Some(idx) => Ok(CapabilityValue::UInt(idx as u64)),
None => Ok(CapabilityValue::Null),
},
"hovered_index" => match self.hovered_index() {
Some(idx) => Ok(CapabilityValue::UInt(idx as u64)),
None => Ok(CapabilityValue::Null),
},
_ => base_property_get(self, name),
}
}
fn set(&mut self, name: &str, value: CapabilityValue) -> Result<(), CapabilityAccessError> {
match name {
// The entry vector and the hover/active cursors are owned by the
// control's own input handling, so they exist but refuse writes.
"entry_count" | "active_index" | "hovered_index" => {
Err(CapabilityAccessError::ReadOnlyProperty)
}
_ => base_property_set(self, name, value),
}
}
fn property_names(&self) -> &'static [&'static str] {
property_names_of!["entry_count", "active_index", "hovered_index", BASE_PROPERTY_NAMES]
}
/// Runs one of the commands `menu_bar` publishes.
///
/// `clear` is the one genuine zero-argument action here: it drops every entry
/// and resets both cursors, which is exactly what [`MenuBar::clear`] does.
/// `add_menu` takes the entry's title and `remove_menu` takes the index to
/// remove, so a payload-less invocation of either is refused as
/// [`CapabilityAccessError::OutOfRange`] — the names are right and the argument
/// is what is missing, which is not `UnknownCommand`.
fn command(&mut self, name: &str) -> Result<(), CapabilityAccessError> {
match name {
"clear" => {
self.clear();
Ok(())
}
"add_menu" | "remove_menu" => Err(CapabilityAccessError::OutOfRange),
// Any other `set_foo` name carries its value through the property route,
// so the shared default reports that a payload is needed rather than
// claiming the control has never heard of it.
_ if name.starts_with("set_") => Err(CapabilityAccessError::OutOfRange),
_ => Err(CapabilityAccessError::UnknownCommand),
}
}
}
impl EventHandler for MenuBar {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::MouseMove { pos } => {
// Derived rather than assigned: the entry under the pointer is only meaningful while
// `BaseWidget` agrees the pointer is on the bar, and re-testing on every move is
// what keeps this from going stale when the pointer leaves without a `MouseLeave`
// (a host that stopped forwarding moves, a window that lost focus).
let next = if self.base.is_hovered() { self.hit_entry(*pos) } else { None };
let prev = self.hovered_index;
self.hovered_index = next;
if self.hovered_index != prev {
if let Some(idx) = self.hovered_index {
if self.entries[idx].is_enabled() {
let title = self.entries[idx].title().to_string();
self.hovered_entry.emit(title);
}
}
}
}
// The bar's own pointer fact is the source, so leaving it must clear the entry too —
// otherwise the last entry stays highlighted on a bar the pointer has left.
Event::MouseLeave { .. } => {
self.hovered_index = None;
}
Event::MousePress { pos, button: 1, .. } => {
if let Some(idx) = self.hit_entry(*pos) {
if self.entries[idx].is_enabled() {
self.active_index = Some(idx);
let title = self.entries[idx].title().to_string();
let entry = self.entry_rect(idx);
self.triggered.emit(title.clone());
if let Some(entry) = entry {
self.triggered_entry.emit((title, entry));
}
}
}
}
Event::KeyPress { key, .. } if *key == key_codes::ESCAPE => {
self.active_index = None;
}
_ => { /* Other events are not relevant */ }
}
}
}
impl Draw for MenuBar {
fn draw(&mut self, context: &mut RenderContext) {
// The **band**, not the control's rectangle: see `band_rect`. The fill, the bottom
// rule, the entry highlights and the labels are all placed from this one box, so a
// 240x120 census cell draws a 28 px strip rather than a 120 px slab.
let rect = self.band_rect();
// A menu bar is very nearly all chrome — its fill, its separators, its entry
// highlights and its labels. Every one of those was a literal, so a light/dark
// switch repainted nothing and the rendering census reported the control as
// theme-blind.
//
// Chrome colours resolve the explicit style first, then the theme's resolved style
// for this control, and only then the original literal. The literal stays as the
// fallback so an inactive theme still has a defined appearance. The theme read is a
// separate manager lock, taken and released inside `resolved_theme_style`, so no
// guard is held across the draw (the mutex is not re-entrant).
let style = self.base.style();
let themed = crate::style::resolved_theme_style_shared("menu_bar");
let themed_bg = themed.as_ref().and_then(|r| r.background_color);
let themed_border = themed.as_ref().and_then(|r| r.border_color);
let themed_text = themed.as_ref().and_then(|r| r.text_color);
// `menu_bar` is absent from `WidgetRole::for_kind_name`'s table, so it classifies as
// `Surface` and resolves to `theme.colors.background` — the window's own fill. A bar
// painted in that colour is byte-identical to the frame behind it, so the resolved
// surface is re-derived a visible step towards the theme's ink; a colour the caller
// set still wins.
let base = style.background_color.or(themed_bg).unwrap_or(Color::rgb(240, 240, 240));
let ink = style.text_color.or(themed_text).unwrap_or(Color::rgb(0, 0, 0));
let background = base.blend(&ink, 0.06);
// The bottom rule is chrome, and its literal was darker than the bar; deriving it
// from the resolved pair keeps that relationship in either appearance.
let separator =
style.border_color.or(themed_border).unwrap_or_else(|| background.blend(&ink, 0.32));
// The active entry is filled with the accent token (the "open menu" indicator),
// the hovered one with a lighter step of the same family, and the label over each
// is picked for contrast rather than hardcoded white-on-accent.
let active_fill = crate::style::theme_manager()
.current_theme()
.map(|theme| theme.colors.primary)
.unwrap_or(Color::rgb(0, 120, 215));
let hover_fill = background.blend(&active_fill, 0.25);
let disabled_ink = ink.blend(&background, 0.5);
// Menu bar background
context.face_with_gradient(
rect,
background,
self.style().background_gradient.as_ref(),
self.style().surface.unwrap_or_default(),
self.style().border_radius.unwrap_or(0),
Color::BLACK,
);
context.draw_line(
Point::new(rect.x, rect.y + rect.height as i32 - 1),
Point::new(rect.x + rect.width as i32, rect.y + rect.height as i32 - 1),
separator,
);
let mut x = rect.x;
for (i, entry) in self.entries.iter().enumerate() {
let w = self.entry_width(entry.title()) as i32;
let is_hovered = self.hovered_index == Some(i);
let is_active = self.active_index == Some(i);
let entry_rect = Rect { x, y: rect.y, width: w as u32, height: rect.height };
if is_active {
context.fill_rect(entry_rect, active_fill);
} else if is_hovered {
context.fill_rect(entry_rect, hover_fill);
}
let fg = if !entry.is_enabled() {
disabled_ink
} else if is_active {
active_fill.contrast_color()
} else {
ink
};
// Centre the label inside its own entry **on both axes**.
//
// The previous form was `Point::new(x + w / 2, rect.y + rect.height / 2)` with
// `HorizontalAlignment::Left`. Two separate errors came out of that one line: the
// horizontal term computed the entry's *midpoint* and then used it as a left-hand
// origin, and the vertical term put the glyph box's top edge on the bar's middle
// line. `entry_width` is `len * 8 + 16`, so a four-character title in a 48 px entry
// advanced 32 px from `x + 24` — past the end of its own entry and 9.6 px over the
// next title. Passing the entry rectangle and asking for `Center` states the
// intent, and there is no midpoint left to misuse.
let font = effective_font(self.style());
let line = context.text_line(entry_rect, font);
context.draw_text_fitted_cow(
line,
entry.title(),
font,
fg,
HorizontalAlignment::Center,
);
x += w;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn menubar_menu_enabled_accessor_handles_valid_and_oob_indices() {
let mut menu_bar = MenuBar::new(Rect::new(0, 0, 300, 24));
let idx = menu_bar.add_menu("File");
assert_eq!(menu_bar.menu_enabled(idx), Some(true));
menu_bar.set_menu_enabled(idx, false);
assert_eq!(menu_bar.menu_enabled(idx), Some(false));
assert_eq!(menu_bar.menu_enabled(99), None);
}
#[test]
fn removing_an_entry_keeps_the_cursors_pointing_at_the_same_titles() {
let mut menu_bar = MenuBar::new(Rect::new(0, 0, 300, 24));
menu_bar.add_menu("File");
menu_bar.add_menu("Edit");
menu_bar.add_menu("View");
menu_bar.active_index = Some(2);
menu_bar.hovered_index = Some(1);
// Removing "File" shifts both titles down by one...
menu_bar.remove_menu(0);
assert_eq!(menu_bar.entries()[menu_bar.active_index().unwrap()].title(), "View");
assert_eq!(menu_bar.entries()[menu_bar.hovered_index().unwrap()].title(), "Edit");
// ...and removing the entry a cursor was on clears that cursor rather than
// leaving it naming the title that took its place.
menu_bar.hovered_index = Some(1);
menu_bar.remove_menu(1);
assert_eq!(menu_bar.active_index(), None);
assert_eq!(menu_bar.hovered_index(), None);
}
#[test]
fn entry_rect_is_none_past_the_last_entry() {
let mut menu_bar = MenuBar::new(Rect::new(0, 0, 300, 24));
menu_bar.add_menu("File");
assert!(menu_bar.entry_rect(0).is_some());
assert!(menu_bar.entry_rect(1).is_none());
}
}