GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(widget): migrate MenuBar; delete dead Menu (Phase 5o)
MenuBar rides the container-era adapter with its legacy ButtonStrip
EMBEDDED in the model (owned by value, driven through Element calls;
events reach it via EventCtx::ui; arrange_children parents it back to the
adapter so its dummy-ctx parent-chain styling walks keep working — the new
Layout::tracked_parent hook serves Element::parent from the model's field
for exactly those walks). Dropdowns move onto new Paint popover hooks
(popover / draw_popover); z-ordering onto Layout::z_order; host-pushed
modifier state onto Input::set_modifiers; set_visible side effects onto
Input::visibility_changed; and the legacy CONDITIONAL focus (the bar holds
global focus only while a dropdown is open or a menu selected) onto
Input::is_focused + EventCtx::release_focus, with the adapter's
focus()/unfocus() now carrying the self pointer so focus events can
claim/release the global slot. PageSelector joins the capability hooks
(as_page_selector pairs — live in the designer and test-interface).
Paint::corner_style now receives the laid-out rect (MenuBar's corners are
computed against its parent backplate's edges; nine implementors updated
mechanically).
The standalone Menu widget is DELETED: zero constructors workspace-wide.
Dropped, flagged: the title_buf/curved_title_char_bufs/context_item_bufs
glyphon caches (get_text_items always returned empty — prepare_text built
buffers nothing read) and the vertical-mode dynamic rect() override
(with_vertical has no callers; the vertical geometry itself is kept).
Verification: 168 tests (menu open via strip press+release -> dropdown
popover -> item click -> menu_click roundtrip -> conditional focus
release, all through real adapter dispatch; hidden-bar gating); all four
hosts (designer, cce-graph, data-editor, test-interface) start and run;
live A/B on cce-test-interface shows the menubar strip pixel-equivalent
(zero diffs above the 8% threshold; sub-8% noise is desktop bleed through
the translucent bar) and identical interaction behavior (the File click
not opening a dropdown there is pre-existing app behavior, byte-identical
in the legacy build). Not yet live-verified: the designer's menu layer and
curved circular-pane mode (screen contended by the live session's
terminal) — worth a designer A/B when the screen frees up.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LkfJazPs9bRchkcozmxXCX
src/widget/container/breadcrumb.rs | 2 +-
src/widget/container/menu.rs | 1803 ++++++++++++-----------------------
src/widget/container/mod.rs | 2 +-
src/widget/container/spreadsheet.rs | 2 +-
src/widget/container/switcher.rs | 4 +-
src/widget/display/graph.rs | 2 +-
src/widget/display/progress_bar.rs | 2 +-
src/widget/input/button.rs | 2 +-
src/widget/input/checkbox.rs | 2 +-
src/widget/input/slider.rs | 4 +-
src/widget/input/spinbox.rs | 2 +-
src/widget/mod.rs | 2 +-
src/widget/model.rs | 122 ++-
13 files changed, 752 insertions(+), 1199 deletions(-)
diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index c9633fe..38dd772 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -95,7 +95,7 @@ impl Paint for Breadcrumb {
self.bg_color()
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
Some((crate::layout::breadcrumb_corner_radius(), (true, true, false, false)))
}
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 886ccf1..054002e 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -1,9 +1,33 @@
+//! Narrow-trait `MenuBar` (Phase 5o) — a titled bar of dropdown menus with an optional
+//! context selector on the title. The menu buttons live in an EMBEDDED legacy [`ButtonStrip`]
+//! (owned by value in the model, driven through `Element` calls — events reach it via
+//! `EventCtx::ui`, and [`Layout::arrange_children`] parents it back to the adapter so its
+//! parent-chain styling walks keep working). Dropdowns are painted through the popover hooks
+//! ([`Paint::popover`] / [`Paint::draw_popover`]) and layered by [`Layout::z_order`]. The
+//! title supports the designer's curved circular-pane mode. Focus is conditional, exactly like
+//! legacy: the bar claims the global focus only while a dropdown is open or a menu selected
+//! ([`Input::is_focused`] reports that state, ignoring the base flag).
+//!
+//! The standalone `Menu` widget that used to live here was DELETED in this migration: it had
+//! zero constructors workspace-wide (dead code).
+//!
+//! Dropped with the migration: the `title_buf`/`curved_title_char_bufs`/`context_item_bufs`
+//! glyphon caches — `get_text_items` always returned empty, so `prepare_text` built buffers
+//! nothing ever read (an abandoned optimization). Also gone: the legacy `rect()` override's
+//! vertical-mode dynamic height (`with_vertical` has no callers workspace-wide; the vertical
+//! label/title geometry is kept for the strip's rotated mode, but the widget rect is the
+//! assigned base rect).
+
use crate::colors;
-use crate::widget::*;
-use crate::widget::display::{make_widget_text_buffer, TextLabel};
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
+use crate::widget::display::TextLabel;
+use crate::widget::{
+ Adapted, ButtonStrip, Element, ElementState, Event, EventCtx, Input, Key, Layout,
+ MenuController, MouseButton, NamedKey, PageSelector, Paint, UiContext, DROPDOWN_ITEM_H,
+};
pub struct MenuBar {
- pub base: Widget,
pub visible: bool,
pub network_opacity: f32,
pub curved_circle: Option<(f32, f32, f32)>,
@@ -20,9 +44,6 @@ pub struct MenuBar {
pub z_level: i32,
pub center_items: bool,
pub title_pos: Option<(f32, f32)>,
- pub title_buf: Option<glyphon::Buffer>,
- pub curved_title_char_bufs: Vec<glyphon::Buffer>,
- pub font_family: String,
pub label: Option<String>,
pub context_options: Vec<String>,
pub context_selected: usize,
@@ -30,7 +51,6 @@ pub struct MenuBar {
pub context_just_changed: bool,
pub context_hovered_item: Option<usize>,
pub context_title_hovered: bool,
- pub context_item_bufs: Vec<glyphon::Buffer>,
pub right_align_title: bool,
pub parent: Option<*mut (dyn Element + 'static)>,
pub page_hidden: bool,
@@ -39,30 +59,12 @@ pub struct MenuBar {
pub on_menu_click_cb: Option<Box<dyn Fn(usize, usize) + Send + Sync>>,
pub hovered_dropdown_item: Option<usize>,
pub clicked_dropdown_item: Option<(usize, usize)>,
+ last_arranged: Option<Rect>,
}
impl MenuBar {
- pub fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
- self.curved_circle = circle;
- }
-
- pub fn set_network_opacity(&mut self, opacity: f32) {
- self.network_opacity = opacity;
- }
-
- pub fn with_color(mut self, color: [f32; 4]) -> Self {
- self.color = Some(color);
- self
- }
-
- pub fn with_blur(mut self, blur: bool) -> Self {
- self.blur = blur;
- self
- }
-
- pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
- Self {
- base: Widget::new_rect(x, y, w, h),
+ pub fn new(x: f32, y: f32, w: f32, h: f32) -> Adapted<MenuBar> {
+ let mut bar = Adapted::new(MenuBar {
visible: true,
network_opacity: 1.0,
curved_circle: None,
@@ -79,9 +81,6 @@ impl MenuBar {
z_level: 0,
center_items: false,
title_pos: None,
- title_buf: None,
- curved_title_char_bufs: Vec::new(),
- font_family: crate::layout::menubar_font(),
label: None,
context_options: Vec::new(),
context_selected: 0,
@@ -89,7 +88,7 @@ impl MenuBar {
context_just_changed: false,
context_hovered_item: None,
context_title_hovered: false,
- context_item_bufs: Vec::new(),
+ right_align_title: false,
parent: None,
page_hidden: false,
layout_dirty: true,
@@ -97,48 +96,22 @@ impl MenuBar {
on_menu_click_cb: None,
hovered_dropdown_item: None,
clicked_dropdown_item: None,
- right_align_title: false,
- }
- }
-
- pub fn with_right_aligned_title(mut self, right: bool) -> Self {
- self.right_align_title = right;
- self
- }
-
- pub fn on_context_change<F: Fn(usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
- self.on_context_change_cb = Some(Box::new(cb));
- self
+ last_arranged: None,
+ });
+ Element::set_rect(&mut bar, x, y, w, h);
+ bar
}
- pub fn on_menu_click<F: Fn(usize, usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
- self.on_menu_click_cb = Some(Box::new(cb));
- self
- }
-
- pub fn with_label(mut self, label: &str) -> Self {
- self.label = Some(label.to_string());
- self.base.label = Some(label.to_string());
- self
- }
-
- pub fn set_label(&mut self, label: &str) {
- self.label = Some(label.to_string());
- self.base.label = Some(label.to_string());
- self.layout_dirty = true;
+ pub fn set_curved_circle(&mut self, circle: Option<(f32, f32, f32)>) {
+ self.curved_circle = circle;
}
- pub fn with_context_options(mut self, options: Vec<String>, selected: usize) -> Self {
- self.context_options = options;
- self.context_selected = selected;
- self
+ pub fn set_network_opacity(&mut self, opacity: f32) {
+ self.network_opacity = opacity;
}
pub fn set_context_selected(&mut self, selected: usize) {
- if self.context_selected != selected {
- self.context_selected = selected;
- self.context_item_bufs.clear();
- }
+ self.context_selected = selected;
}
pub fn take_context_change(&mut self) -> Option<usize> {
@@ -150,7 +123,15 @@ impl MenuBar {
}
}
- pub fn title_rect(&self) -> (f32, f32, f32, f32) {
+ fn display_title(&self) -> String {
+ let mut display_title = self.title.clone();
+ if !self.context_options.is_empty() {
+ display_title.push_str(if self.vertical { "▼" } else { " ▼" });
+ }
+ display_title
+ }
+
+ pub fn title_rect(&self, rect: Rect) -> (f32, f32, f32, f32) {
if self.title.is_empty() {
return (0.0, 0.0, 0.0, 0.0);
}
@@ -165,12 +146,12 @@ impl MenuBar {
display_title.push_str(" ▼");
}
- if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
+ if self.curved_circle.is_some() {
if let Some((tx, ty)) = self.title_pos {
let title_w = display_title.len() as f32 * char_w + 24.0;
- (tx, ty, title_w, self.base.h)
+ (tx, ty, title_w, rect.height)
} else {
- (self.base.x, self.base.y, display_title.len() as f32 * char_w + 24.0, self.base.h)
+ (rect.x, rect.y, display_title.len() as f32 * char_w + 24.0, rect.height)
}
} else if self.vertical {
let mut cy = 16.0;
@@ -180,12 +161,8 @@ impl MenuBar {
cy += label_h + 20.0;
}
let line_height = font_size * 1.2;
- let mut display_title_vertical = self.title.clone();
- if !self.context_options.is_empty() {
- display_title_vertical.push_str("▼");
- }
- let title_h = display_title_vertical.chars().count() as f32 * line_height;
- (self.base.x, self.base.y + cy, self.base.w, title_h)
+ let title_h = self.display_title().chars().count() as f32 * line_height;
+ (rect.x, rect.y + cy, rect.width, title_h)
} else {
let mut start_x = 8.0;
if self.center_items {
@@ -194,21 +171,21 @@ impl MenuBar {
for btn_label in &self.menus.buttons {
total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
}
- if self.base.w > total_width {
- start_x = (self.base.w - total_width) / 2.0;
+ if rect.width > total_width {
+ start_x = (rect.width - total_width) / 2.0;
}
}
let title_w = display_title.len() as f32 * char_w + 24.0;
let tx = if self.right_align_title {
- self.base.x + self.base.w - title_w - 20.0
+ rect.x + rect.width - title_w - 20.0
} else {
- self.base.x + start_x
+ rect.x + start_x
};
- (tx, self.base.y, title_w, self.base.h)
+ (tx, rect.y, title_w, rect.height)
}
}
- pub fn context_popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
+ pub fn context_popover_rect(&self, rect: Rect) -> Option<(f32, f32, f32, f32)> {
if self.context_options.is_empty() || !self.context_dropdown_open {
return None;
}
@@ -221,71 +198,12 @@ impl MenuBar {
let dw = (max_len as f32 * char_w + 40.0).max(140.0);
let dh = self.context_options.len() as f32 * DROPDOWN_ITEM_H;
- let tr = self.title_rect();
- let dx = if self.vertical {
- tr.0 + tr.2
- } else {
- tr.0
- };
- let dy = if self.vertical {
- tr.1
- } else {
- tr.1 + tr.3
- };
+ let tr = self.title_rect(rect);
+ let dx = if self.vertical { tr.0 + tr.2 } else { tr.0 };
+ let dy = if self.vertical { tr.1 } else { tr.1 + tr.3 };
Some((dx, dy, dw, dh))
}
- pub fn with_center_items(mut self, center: bool) -> Self {
- self.center_items = center;
- self
- }
-
- pub fn with_title(mut self, title: &str) -> Self {
- self.title = title.to_string();
- self
- }
-
- pub fn with_item(mut self, label: &str, items: &[&str]) -> Self {
- self.menu_items.push(label.to_string());
- self.vertical_items.push(label.to_string());
- self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
- self.menu_dropdown_checked.push(vec![None; items.len()]);
- self.menus.add_button(label);
- self
- }
-
- pub fn with_item_vh(mut self, horizontal_label: &str, vertical_label: &str, items: &[&str]) -> Self {
- self.menu_items.push(horizontal_label.to_string());
- self.vertical_items.push(vertical_label.to_string());
- self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
- self.menu_dropdown_checked.push(vec![None; items.len()]);
- let label = if self.vertical { vertical_label } else { horizontal_label };
- self.menus.add_button(label);
- self
- }
-
- fn update_menu_labels(&mut self) {
- let src = if self.vertical {
- &self.vertical_items
- } else {
- &self.menu_items
- };
- self.menus.buttons = src.clone();
- self.menus.generate_rotated_labels();
- }
-
- pub fn with_vertical(mut self, vertical: bool) -> Self {
- self.vertical = vertical;
- self.menus.vertical = vertical;
- self.update_menu_labels();
- self
- }
-
- pub fn with_z_index(mut self, z: i32) -> Self {
- self.z_level = z;
- self
- }
-
pub fn menu_dropdown_rect(&self) -> Option<(f32, f32, f32, f32)> {
let menu_idx = self.menus.selected?;
let items = self.menu_dropdowns.get(menu_idx)?;
@@ -300,17 +218,9 @@ impl MenuBar {
let font_size = font_size_opt.unwrap_or(12.0);
let char_w = 7.5 * (font_size / 12.0);
let dw = (max_len as f32 * char_w + 40.0).max(120.0);
-
- let dx = if self.vertical {
- hr.0 + hr.2
- } else {
- hr.0
- };
- let dy = if self.vertical {
- hr.1
- } else {
- hr.1 + hr.3
- };
+
+ let dx = if self.vertical { hr.0 + hr.2 } else { hr.0 };
+ let dy = if self.vertical { hr.1 } else { hr.1 + hr.3 };
Some((dx, dy, dw, dh))
}
@@ -331,91 +241,55 @@ impl MenuBar {
}
self.blur
}
-}
-
-impl Element for MenuBar {
- crate::impl_widget_base!(MenuBar);
-
- fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
- self.menus.set_modifiers(ctrl, shift, alt);
- }
-
- fn layout_ignore(&self) -> bool {
- true
- }
-
- fn blocks_backplate_drag(&self) -> bool {
- false
- }
-
- fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
- self.parent
- }
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
- self.parent = parent;
+ fn update_menu_labels(&mut self) {
+ let src = if self.vertical { &self.vertical_items } else { &self.menu_items };
+ self.menus.buttons = src.clone();
+ self.menus.generate_rotated_labels();
}
- fn label(&self) -> Option<String> {
- self.label.clone()
+ fn bg_color(&self) -> [f32; 4] {
+ if let Some(p_ptr) = self.parent {
+ if unsafe { (*p_ptr).is_backplate() } {
+ return crate::colors::backplate_menubar_color();
+ }
+ }
+ self.color.unwrap_or_else(|| colors::sidebar_bg_color())
}
- fn set_text(&mut self, text: &str) {
- self.label = Some(text.to_string());
- self.base.label = Some(text.to_string());
- self.layout_dirty = true;
- }
+ fn corners_against_parent(&self, rect: Rect) -> (bool, bool, bool, bool) {
+ if let Some(p_ptr) = self.parent {
+ let is_bp = unsafe { (*p_ptr).is_backplate() };
+ if is_bp {
+ let (px, py, pw, ph) = unsafe { (*p_ptr).rect() };
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
+ let is_at_top = (y - py).abs() < 0.1;
+ let is_at_bottom = (y + h - (py + ph)).abs() < 0.1;
- fn rect(&self) -> (f32, f32, f32, f32) {
- if !self.visible {
- return (0.0, 0.0, 0.0, 0.0);
- }
- if self.vertical {
- let mut h = 16.0;
- if let Some(ref label) = self.label {
- let font_size = 12.0;
- let line_height = font_size * 1.2;
- let label_h = label.chars().count() as f32 * line_height;
- h += label_h + 20.0;
- }
- if !self.title.is_empty() {
- let font_size = 12.0;
- let line_height = font_size * 1.2;
- let mut display_title = self.title.clone();
- if !self.context_options.is_empty() {
- display_title.push_str("▼");
+ if is_at_top && is_at_bottom {
+ let is_at_left = (x - px).abs() < 0.1;
+ let is_at_right = (x + w - (px + pw)).abs() < 0.1;
+ return (is_at_left, is_at_right, is_at_right, is_at_left);
+ } else if is_at_top {
+ return (true, true, false, false);
+ } else if is_at_bottom {
+ return (false, false, true, true);
}
- let title_h = display_title.chars().count() as f32 * line_height;
- h += title_h + 36.0;
}
- let (_, _, _, menus_h) = self.menus.rect();
- (self.base.x, self.base.y, self.base.w, h + menus_h)
- } else {
- (self.base.x, self.base.y, self.base.w, self.base.h)
}
+ (false, false, false, false)
}
- fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- if self.base.x == x && self.base.y == y && self.base.w == w && self.base.h == h && !self.layout_dirty {
+ /// Position the embedded strip inside `rect` — the legacy `set_rect` body, minus the
+ /// parent clamping (that lives in [`Layout::adjust_rect`]) and the base assignment (the
+ /// adapter's). Early-outs when the rect and content are unchanged, like legacy.
+ fn layout_strip(&mut self, rect: Rect) {
+ if self.last_arranged == Some(rect) && !self.layout_dirty {
return;
}
self.layout_dirty = false;
- let (clamped_x, clamped_y, clamped_w, clamped_h) = if let Some(parent_ptr) = self.parent {
- let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
- let cx = x.clamp(px, px + pw);
- let cy = y.clamp(py, py + ph);
- let cw = w.min(px + pw - cx);
- let ch = h.min(py + ph - cy);
- (cx, cy, cw, ch)
- } else {
- (x, y, w, h)
- };
-
- self.base.x = clamped_x;
- self.base.y = clamped_y;
- self.base.w = clamped_w;
- self.base.h = clamped_h;
- let parent_ptr = self as *mut MenuBar as *mut (dyn Element + 'static);
+ self.last_arranged = Some(rect);
+ let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
let font_setting = crate::layout::menubar_font();
let font_info = crate::layout::parse_font_string(&font_setting);
@@ -433,19 +307,13 @@ impl Element for MenuBar {
if !self.title.is_empty() {
let font_size = 12.0;
let line_height = font_size * 1.2;
- let mut display_title = self.title.clone();
- if !self.context_options.is_empty() {
- display_title.push_str("▼");
- }
- let title_h = display_title.chars().count() as f32 * line_height;
+ let title_h = self.display_title().chars().count() as f32 * line_height;
cy += title_h + 36.0;
}
- let menus_y = (clamped_y + cy).clamp(clamped_y, clamped_y + clamped_h);
- let menus_h = (clamped_h - cy).min(clamped_y + clamped_h - menus_y).max(0.0);
+ let menus_y = (y + cy).clamp(y, y + h);
+ let menus_h = (h - cy).min(y + h - menus_y).max(0.0);
self.menus.vertical = true;
- self.menus.set_rect(clamped_x, menus_y, clamped_w, menus_h);
- let mut dummy = crate::context::UiContext::new();
- self.menus.set_parent(Some(parent_ptr), &mut dummy);
+ self.menus.set_rect(x, menus_y, w, menus_h);
} else {
let padding = crate::layout::button_padding();
let spacing = crate::layout::button_strip_spacing();
@@ -468,8 +336,8 @@ impl Element for MenuBar {
}
}
total_width += btn_strip_w;
- if self.base.w > total_width {
- cx = (self.base.w - total_width) / 2.0;
+ if w > total_width {
+ cx = (w - total_width) / 2.0;
}
}
if !self.title.is_empty() && !self.right_align_title {
@@ -487,477 +355,209 @@ impl Element for MenuBar {
btn_strip_w += spacing;
}
}
- let menus_x = (clamped_x + cx).clamp(clamped_x, clamped_x + clamped_w);
- let menus_w = btn_strip_w.min(clamped_x + clamped_w - menus_x);
+ let menus_x = (x + cx).clamp(x, x + w);
+ let menus_w = btn_strip_w.min(x + w - menus_x);
self.menus.vertical = false;
- self.menus.set_rect(menus_x, clamped_y, menus_w, clamped_h);
- let mut dummy = crate::context::UiContext::new();
- self.menus.set_parent(Some(parent_ptr), &mut dummy);
+ self.menus.set_rect(menus_x, y, menus_w, h);
}
}
- fn color(&self) -> [f32; 4] {
- if let Some(p_ptr) = self.parent {
- if unsafe { (*p_ptr).is_backplate() } {
- return crate::colors::backplate_menubar_color();
- }
- }
- self.color.unwrap_or_else(|| colors::sidebar_bg_color())
+ /// Close every open dropdown and drop internal focus state — the state half of the legacy
+ /// `unfocus` (the global-focus release is the caller's, via `EventCtx::release_focus`).
+ fn close_all(&mut self) {
+ self.focused = false;
+ self.context_dropdown_open = false;
+ self.context_hovered_item = None;
+ self.hovered_dropdown_item = None;
+ self.menus.set_selected(None);
}
-
-
- fn rounded_corners(&self) -> (bool, bool, bool, bool) {
- if let Some(p_ptr) = self.parent {
- let is_bp = unsafe { (*p_ptr).is_backplate() };
- if is_bp {
- let (px, py, pw, ph) = unsafe { (*p_ptr).rect() };
- let (x, y, w, h) = self.rect();
- let is_at_top = (y - py).abs() < 0.1;
- let is_at_bottom = (y + h - (py + ph)).abs() < 0.1;
-
- if is_at_top && is_at_bottom {
- let is_at_left = (x - px).abs() < 0.1;
- let is_at_right = (x + w - (px + pw)).abs() < 0.1;
- return (is_at_left, is_at_right, is_at_right, is_at_left);
- } else if is_at_top {
- return (true, true, false, false);
- } else if is_at_bottom {
- return (false, false, true, true);
- }
- }
+ /// The conditional focus claim of the legacy `focus()`: hold the global focus only while
+ /// something is open.
+ fn sync_focus(&mut self, ectx: &mut EventCtx) {
+ if self.context_dropdown_open || self.menus.selected.is_some() {
+ self.focused = true;
+ ectx.request_focus();
+ } else {
+ self.focused = false;
+ ectx.release_focus();
}
- (false, false, false, false)
}
+}
- fn corner_radius(&self) -> f32 {
- if let Some(p_ptr) = self.parent {
- unsafe { (*p_ptr).corner_radius() }
- } else {
- 0.0
- }
+impl Adapted<MenuBar> {
+ pub fn with_color(mut self, color: [f32; 4]) -> Self {
+ self.color = Some(color);
+ self
}
+ pub fn with_blur(mut self, blur: bool) -> Self {
+ self.blur = blur;
+ self
+ }
- fn set_hovered(&mut self, v: bool) {
- self.base.hovered = v;
+ pub fn with_right_aligned_title(mut self, right: bool) -> Self {
+ self.right_align_title = right;
+ self
}
- fn hovered(&self) -> bool {
- self.base.hovered
+ pub fn on_context_change<F: Fn(usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
+ self.on_context_change_cb = Some(Box::new(cb));
+ self
}
- fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
- if !self.visible {
- return false;
- }
- if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- if let Some((dx, dy, dw, dh)) = self.context_popover_rect() {
- if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- return true;
- }
- }
- if let Some((dx, dy, dw, dh)) = self.menu_dropdown_rect() {
- if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- return true;
- }
- }
- let (rx, ry, rw, rh) = self.rect();
- if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
- return true;
- }
- if self.menus.hit_test(px, py, ctx) {
- return true;
- }
- false
+ pub fn on_menu_click<F: Fn(usize, usize) + Send + Sync + 'static>(mut self, cb: F) -> Self {
+ self.on_menu_click_cb = Some(Box::new(cb));
+ self
}
- fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if !self.visible {
- return false;
- }
- let (rx, ry, rw, rh) = self.rect();
- self.set_rect(rx, ry, rw, rh);
-
- let mut changed = false;
-
- let old_title_hovered = self.context_title_hovered;
- self.context_title_hovered = false;
- if !self.context_options.is_empty() {
- let tr = self.title_rect();
- if px >= tr.0 && px <= tr.0 + tr.2 && py >= tr.1 && py <= tr.1 + tr.3 {
- self.context_title_hovered = true;
- }
- }
- if old_title_hovered != self.context_title_hovered {
- changed = true;
- }
-
- let old_hovered_item = self.context_hovered_item;
- self.context_hovered_item = None;
- if let Some((dx, dy, dw, dh)) = self.context_popover_rect() {
- if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if di < self.context_options.len() {
- self.context_hovered_item = Some(di);
- }
- }
- }
- if old_hovered_item != self.context_hovered_item {
- changed = true;
- }
-
- let old_hovered_dropdown = self.hovered_dropdown_item;
- self.hovered_dropdown_item = None;
- if let Some((dx, dy, dw, dh)) = self.menu_dropdown_rect() {
- if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if let Some(menu_idx) = self.menus.selected {
- if let Some(items) = self.menu_dropdowns.get(menu_idx) {
- if di < items.len() {
- self.hovered_dropdown_item = Some(di);
- }
- }
- }
- }
- }
- if old_hovered_dropdown != self.hovered_dropdown_item {
- changed = true;
- }
-
- if self.menus.cursor_moved(px, py, ctx) {
- changed = true;
- }
- changed
+ pub fn with_context_options(mut self, options: Vec<String>, selected: usize) -> Self {
+ self.context_options = options;
+ self.context_selected = selected;
+ self
}
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if !self.visible {
- return false;
- }
- if button != MouseButton::Left {
- return false;
- }
-
- let (rx, ry, rw, rh) = self.rect();
- self.set_rect(rx, ry, rw, rh);
-
- let mut changed = false;
-
- if let Some((dx, dy, dw, dh)) = self.menu_dropdown_rect() {
- if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- if state == ElementState::Pressed {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if let Some(menu_idx) = self.menus.selected {
- if let Some(items) = self.menu_dropdowns.get(menu_idx) {
- if di < items.len() {
- self.clicked_dropdown_item = Some((menu_idx, di));
- self.menus.set_selected(None);
- self.hovered_dropdown_item = None;
- self.unfocus();
- return true;
- }
- }
- }
- }
- return true;
- }
- }
-
- if let Some((dx, dy, dw, dh)) = self.context_popover_rect() {
- if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- if state == ElementState::Pressed {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if di < self.context_options.len() {
- self.context_selected = di;
- self.context_just_changed = true;
- self.context_dropdown_open = false;
- self.unfocus();
- if let Some(ref cb) = self.on_context_change_cb {
- cb(di);
- }
- return true;
- }
- }
- }
- }
-
- if !self.context_options.is_empty() {
- let tr = self.title_rect();
- if px >= tr.0 && px <= tr.0 + tr.2 && py >= tr.1 && py <= tr.1 + tr.3 {
- if state == ElementState::Pressed {
- if self.context_dropdown_open {
- self.context_dropdown_open = false;
- self.unfocus();
- } else {
- self.menus.unfocus();
- self.context_dropdown_open = true;
- self.focus();
- }
- }
- return true;
- }
- }
+ pub fn with_center_items(mut self, center: bool) -> Self {
+ self.center_items = center;
+ self
+ }
- if self.context_dropdown_open && state == ElementState::Pressed {
- self.context_dropdown_open = false;
- self.unfocus();
- changed = true;
- }
+ pub fn with_title(mut self, title: &str) -> Self {
+ self.title = title.to_string();
+ self
+ }
- let old_menu_selected = self.menus.selected;
- if self.menus.mouse_input(button, state, px, py, ctx) {
- changed = true;
- if self.menus.selected.is_some() && old_menu_selected != self.menus.selected {
- self.focus();
- }
- }
- changed
+ pub fn with_item(mut self, label: &str, items: &[&str]) -> Self {
+ self.menu_items.push(label.to_string());
+ self.vertical_items.push(label.to_string());
+ self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
+ self.menu_dropdown_checked.push(vec![None; items.len()]);
+ self.menus.add_button(label);
+ self
}
- fn focus(&mut self) {
- if self.context_dropdown_open || self.menus.selected.is_some() {
- self.focused = true;
- focus::set_focused(self);
- } else {
- self.focused = false;
- focus::clear_if_matches(self);
- }
+ pub fn with_item_vh(mut self, horizontal_label: &str, vertical_label: &str, items: &[&str]) -> Self {
+ self.menu_items.push(horizontal_label.to_string());
+ self.vertical_items.push(vertical_label.to_string());
+ self.menu_dropdowns.push(items.iter().map(|s| s.to_string()).collect());
+ self.menu_dropdown_checked.push(vec![None; items.len()]);
+ let label = if self.vertical { vertical_label } else { horizontal_label };
+ self.menus.add_button(label);
+ self
}
- fn unfocus(&mut self) {
- self.focused = false;
- self.context_dropdown_open = false;
- self.context_hovered_item = None;
- self.hovered_dropdown_item = None;
- focus::clear_if_matches(self);
- self.menus.set_selected(None);
+ pub fn with_vertical(mut self, vertical: bool) -> Self {
+ self.vertical = vertical;
+ self.menus.vertical = vertical;
+ self.update_menu_labels();
+ self
}
- fn focused(&self, _ctx: &UiContext) -> bool {
- self.focused || self.context_dropdown_open || self.menus.selected.is_some()
+ pub fn with_z_index(mut self, z: i32) -> Self {
+ self.z_level = z;
+ self
}
+}
- fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
- self.context_popover_rect().or_else(|| self.menu_dropdown_rect())
+impl Layout for MenuBar {
+ fn layout_ignore(&self) -> bool {
+ true
}
- fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
- if self.context_dropdown_open {
- if let Some((dx, dy, dw, dh)) = self.context_popover_rect() {
- let theme = colors::active_theme();
- pc.rect(theme.surface_border, dx, dy, dw, dh);
- pc.rect(theme.surface_bg, dx + 1.0, dy + 1.0, dw - 2.0, dh - 2.0);
- if let Some(di) = self.context_hovered_item {
- let iy = dy + di as f32 * DROPDOWN_ITEM_H;
- pc.rect(colors::PANEL_MENU_HOVER, dx + 2.0, iy + 2.0, dw - 4.0, DROPDOWN_ITEM_H - 4.0);
- }
-
- let label_color = self.text_color();
- let srgb = crate::colors::to_srgb(label_color);
- let color_f32 = [srgb[0], srgb[1], srgb[2], 1.0];
- let bounds = Some([dx, dy, dx + dw, dy + dh]);
-
- let font = self.widget_font();
- for (i, option) in self.context_options.iter().enumerate() {
- let is_selected = self.context_selected == i;
- let prefix = if is_selected { "✓ " } else { " " };
- let text = format!("{}{}", prefix, option);
- let iy = crate::layout::align_text_y(dy + i as f32 * DROPDOWN_ITEM_H, DROPDOWN_ITEM_H, 12.0, 0.0);
- if let Some(ref f) = font {
- pc.text_with_font_and_bounds(&text, dx + 8.0, iy, 12.0, color_f32, f, bounds);
- } else {
- pc.text_with_bounds(&text, dx + 8.0, iy, 12.0, color_f32, bounds);
- }
- }
- }
- } else if let Some((dx, dy, dw, dh)) = self.menu_dropdown_rect() {
- let theme = colors::active_theme();
- pc.rect(theme.surface_border, dx, dy, dw, dh);
- pc.rect(theme.surface_bg, dx + 1.0, dy + 1.0, dw - 2.0, dh - 2.0);
- if let Some(di) = self.hovered_dropdown_item {
- let iy = dy + di as f32 * DROPDOWN_ITEM_H;
- pc.rect(colors::PANEL_MENU_HOVER, dx + 2.0, iy + 2.0, dw - 4.0, DROPDOWN_ITEM_H - 4.0);
- }
+ fn z_order(&self) -> i32 {
+ self.z_level
+ }
- let label_color = self.text_color();
- let srgb = crate::colors::to_srgb(label_color);
- let color_f32 = [srgb[0], srgb[1], srgb[2], 1.0];
- let bounds = Some([dx, dy, dx + dw, dy + dh]);
+ fn tracked_parent(&self) -> Option<Option<*mut (dyn Element + 'static)>> {
+ Some(self.parent)
+ }
- let font = self.widget_font();
- if let Some(menu_idx) = self.menus.selected {
- if let Some(items) = self.menu_dropdowns.get(menu_idx) {
- for (i, option) in items.iter().enumerate() {
- let checked = self.menu_dropdown_checked.get(menu_idx)
- .and_then(|menu| menu.get(i))
- .and_then(|&v| v);
- let prefix = match checked {
- Some(true) => "✓ ",
- Some(false) => " ",
- None => "",
- };
- let text = format!("{}{}", prefix, option);
- let iy = crate::layout::align_text_y(dy + i as f32 * DROPDOWN_ITEM_H, DROPDOWN_ITEM_H, 12.0, 0.0);
- if let Some(ref f) = font {
- pc.text_with_font_and_bounds(&text, dx + 8.0, iy, 12.0, color_f32, f, bounds);
- } else {
- pc.text_with_bounds(&text, dx + 8.0, iy, 12.0, color_f32, bounds);
- }
- }
- }
- }
- }
+ fn parent_changed(&mut self, parent: Option<*mut (dyn Element + 'static)>) {
+ self.parent = parent;
}
- fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
- if event.state != ElementState::Pressed { return false; }
- if self.context_dropdown_open {
- match event.logical_key {
- Key::Named(NamedKey::ArrowDown) => {
- let current = self.context_hovered_item.unwrap_or(self.context_selected);
- if current + 1 < self.context_options.len() {
- self.context_hovered_item = Some(current + 1);
- } else {
- self.context_hovered_item = Some(0);
- }
- return true;
- }
- Key::Named(NamedKey::ArrowUp) => {
- let current = self.context_hovered_item.unwrap_or(self.context_selected);
- if current > 0 {
- self.context_hovered_item = Some(current - 1);
- } else {
- self.context_hovered_item = Some(self.context_options.len() - 1);
- }
- return true;
- }
- Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => {
- if let Some(idx) = self.context_hovered_item {
- self.context_selected = idx;
- self.context_just_changed = true;
- }
- self.context_dropdown_open = false;
- self.unfocus();
- return true;
- }
- Key::Named(NamedKey::Escape) => {
- self.context_dropdown_open = false;
- self.unfocus();
- return true;
- }
- _ => {}
- }
- }
- self.menus.keyboard_input(event, ctx)
+ /// Legacy `set_rect` clamped into the parent rect (no zero floor, unlike Switcher).
+ fn adjust_rect(&self, requested: Rect) -> Rect {
+ let Some(parent_ptr) = self.parent else {
+ return requested;
+ };
+ let (px, py, pw, ph) = unsafe { (*parent_ptr).rect() };
+ let cx = requested.x.clamp(px, px + pw);
+ let cy = requested.y.clamp(py, py + ph);
+ let cw = requested.width.min(px + pw - cx);
+ let ch = requested.height.min(py + ph - cy);
+ Rect { x: cx, y: cy, width: cw, height: ch }
}
- fn set_selected(&mut self, selected: bool) {
- self.focused = selected;
- if !selected {
- self.menus.set_selected(None);
- }
+ fn arrange_children(&mut self, rect: Rect, host: *mut (dyn Element + 'static)) {
+ self.layout_strip(rect);
+ // The strip walks its parent chain for backplate-aware styling; through the adapter
+ // the chain is host -> tracked parent (a dummy-ctx-safe walk, as legacy relied on).
+ let mut dummy = crate::context::UiContext::new();
+ self.menus.set_parent(Some(host), &mut dummy);
}
+}
- fn as_page_selector(&self) -> Option<&dyn PageSelector> { Some(self) }
- fn as_page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> { Some(self) }
- fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
- fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
+impl Paint for MenuBar {
+ fn color(&self) -> [f32; 4] {
+ self.bg_color()
+ }
- fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = self.extra_quads();
- if let Some(hq) = self.menus.highlight_quad(ctx) {
- if hq.4 != colors::HIGHLIGHT_SECONDARY {
- quads.push(hq);
- }
- }
- quads
+ fn corner_style(&self, rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
+ let radius = match self.parent {
+ Some(p_ptr) => unsafe { (*p_ptr).corner_radius() },
+ None => 0.0,
+ };
+ Some((radius, self.corners_against_parent(rect)))
}
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- self.menus.extra_arcs()
+ fn widget_font(&self) -> Option<String> {
+ Some(crate::layout::menubar_font())
}
- fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- self.menus.extra_circles()
+ fn sync_label(&mut self, label: &str) {
+ self.label = Some(label.to_string());
+ self.layout_dirty = true;
}
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- if !self.visible {
- return;
- }
- let current_font = crate::layout::menubar_font();
- if self.font_family != current_font {
- self.font_family = current_font;
- self.title_buf = None;
- self.curved_title_char_bufs.clear();
+ fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+ // Background: plain when cornerless (the legacy extra_quads bg), rounded against the
+ // parent's corners otherwise (the legacy Element-default all_rounded_quads bg).
+ let corners = self.corners_against_parent(rect);
+ let bg = self.bg_color();
+ if corners == (false, false, false, false) {
+ ctx.quad(rect, bg);
+ } else if bg[3].abs() > 0.001 {
+ let radius = match self.parent {
+ Some(p_ptr) => unsafe { (*p_ptr).corner_radius() },
+ None => 0.0,
+ };
+ ctx.rounded_rect(rect, radius, corners, bg);
}
- let (font_fam, font_size_opt) = crate::layout::parse_font_string(&self.font_family);
- let font_size = font_size_opt.unwrap_or(12.0);
- if !self.title.is_empty() {
- let mut display_title = self.title.clone();
- if !self.context_options.is_empty() {
- display_title.push_str(" ▼");
- }
- if let Some((_ccx, _ccy, _ccr)) = self.curved_circle {
- if self.curved_title_char_bufs.len() != display_title.chars().count() {
- let font_fam_clone = font_fam.clone();
- self.curved_title_char_bufs = display_title.chars()
- .map(|c| make_widget_text_buffer(fs, &c.to_string(), font_size, &font_fam_clone))
- .collect();
- }
- self.title_buf = None;
- } else if self.vertical {
- self.title_buf = None;
- self.curved_title_char_bufs.clear();
- } else {
- if self.title_buf.is_none() {
- self.title_buf = Some(make_widget_text_buffer(fs, &display_title, font_size, &font_fam));
- }
- self.curved_title_char_bufs.clear();
+ // Title highlight while the context dropdown is open / hovered.
+ if !self.context_options.is_empty() {
+ let tr = self.title_rect(rect);
+ if self.context_dropdown_open {
+ ctx.quad(Rect { x: tr.0, y: tr.1, width: tr.2, height: tr.3 }, colors::highlight_primary_color());
+ } else if self.context_title_hovered {
+ ctx.quad(Rect { x: tr.0, y: tr.1, width: tr.2, height: tr.3 }, colors::HIGHLIGHT_SECONDARY);
}
- } else {
- self.title_buf = None;
- self.curved_title_char_bufs.clear();
}
- if self.context_dropdown_open {
- if self.context_item_bufs.len() != self.context_options.len() {
- let font_fam_clone = font_fam.clone();
- self.context_item_bufs = self.context_options.iter().enumerate().map(|(i, option)| {
- let is_selected = self.context_selected == i;
- let prefix = if is_selected { "✓ " } else { " " };
- let text = format!("{}{}", prefix, option);
- make_widget_text_buffer(fs, &text, font_size, &font_fam_clone)
- }).collect();
- }
- } else {
- self.context_item_bufs.clear();
+ // The embedded strip's geometry (it is not a tree child; its pixels are ours).
+ for (qx, qy, qw, qh, qc) in self.menus.extra_quads() {
+ ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
}
-
- self.menus.prepare_text(fs);
- }
-
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- Vec::new()
- }
-
- fn text_labels(&self) -> Vec<TextLabel> {
- if !self.visible {
- return Vec::new();
+ for (cx, cy, r, t, start, end, c) in self.menus.extra_arcs() {
+ ctx.arc(cx, cy, r, t, start, end, c);
}
+ for (cx, cy, r, c) in self.menus.extra_circles() {
+ ctx.circle(cx, cy, r, c);
+ }
+
+ // Text: the sidebar label (vertical), the title (curved / vertical / horizontal), and
+ // the strip's button labels — the legacy `text_labels` body.
let label_color = self.text_color();
let srgb = crate::colors::to_srgb(label_color);
let text_color = [
@@ -966,25 +566,18 @@ impl Element for MenuBar {
(srgb[2] * 255.0) as u8,
];
let padding_x = crate::layout::paginator_tab_padding_x();
- let mut labels = Vec::new();
if let Some(ref label) = self.label {
if self.vertical {
let font_size = 12.0;
let line_height = font_size * 1.2;
- let start_y = self.base.y + 16.0;
+ let start_y = rect.y + 16.0;
for (i, c) in label.chars().enumerate() {
let char_str = c.to_string();
let char_w = crate::widget::display::measure_text(&char_str, font_size);
- let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
+ let x_pos = rect.x + (rect.width - char_w) / 2.0;
let y_pos = start_y + i as f32 * line_height;
- labels.push(TextLabel {
- text: char_str,
- x: x_pos,
- y: y_pos,
- font_size,
- color: [0x83, 0x83, 0x8a],
- });
+ ctx.text(char_str, x_pos, y_pos, font_size, [0x83, 0x83, 0x8a]);
}
}
}
@@ -1000,7 +593,7 @@ impl Element for MenuBar {
}
if let Some((ccx, ccy, ccr)) = self.curved_circle {
- let r_mid = ccr - self.base.h / 2.0;
+ let r_mid = ccr - rect.height / 2.0;
let mut total_width = 8.0;
if !self.title.is_empty() {
total_width += display_title.len() as f32 * char_w + 24.0;
@@ -1015,17 +608,19 @@ impl Element for MenuBar {
if !self.title.is_empty() {
let title_w = display_title.len() as f32 * char_w + 24.0;
let dtheta_title = title_w / r_mid;
- labels.extend(TextLabel::curved_layout(
+ for l in TextLabel::curved_layout(
&display_title,
ccx, ccy, r_mid,
current_angle, current_angle + dtheta_title,
font_size,
text_color,
- ));
+ ) {
+ ctx.text(l.text, l.x, l.y, l.font_size, l.color);
+ }
}
} else if self.vertical {
if !self.title.is_empty() {
- let mut start_y = self.base.y + 16.0;
+ let mut start_y = rect.y + 16.0;
if let Some(ref label) = self.label {
let font_size = 12.0;
let line_height = font_size * 1.2;
@@ -1034,587 +629,390 @@ impl Element for MenuBar {
}
let line_height = font_size * 1.2;
let char_w = crate::widget::display::measure_text("o", font_size);
- let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
- let mut display_title_vertical = self.title.clone();
- if !self.context_options.is_empty() {
- display_title_vertical.push_str("▼");
- }
- for (i, c) in display_title_vertical.chars().enumerate() {
+ let x_pos = rect.x + (rect.width - char_w) / 2.0;
+ for (i, c) in self.display_title().chars().enumerate() {
let char_str = c.to_string();
let y_pos = start_y + i as f32 * line_height;
- labels.push(TextLabel {
- text: char_str,
- x: x_pos,
- y: y_pos,
- font_size,
- color: text_color,
- });
+ ctx.text(char_str, x_pos, y_pos, font_size, text_color);
}
}
- } else {
+ } else if !self.title.is_empty() {
let mut start_x = 8.0;
if self.center_items {
let mut total_width = 8.0;
- if !self.title.is_empty() && !self.right_align_title {
+ if !self.right_align_title {
total_width += display_title.len() as f32 * char_w + 24.0;
}
for btn_label in &self.menus.buttons {
total_width += btn_label.len() as f32 * char_w + 2.0 * padding_x;
}
- if self.base.w > total_width {
- start_x = (self.base.w - total_width) / 2.0;
+ if rect.width > total_width {
+ start_x = (rect.width - total_width) / 2.0;
}
}
- if !self.title.is_empty() {
- let text_y = crate::layout::align_text_y(self.base.y, self.base.h, font_size, 0.0);
- let x_pos = if self.right_align_title {
- let title_w = crate::widget::display::measure_text_width(&display_title, &font_fam, font_size) + 24.0;
- self.base.x + self.base.w - title_w - 20.0
- } else {
- self.base.x + start_x
- };
- labels.push(TextLabel {
- text: display_title,
- x: x_pos,
- y: text_y,
- font_size,
- color: text_color,
- });
- }
+ let text_y = crate::layout::align_text_y(rect.y, rect.height, font_size, 0.0);
+ let x_pos = if self.right_align_title {
+ let title_w = crate::widget::display::measure_text_width(&display_title, &font_fam, font_size) + 24.0;
+ rect.x + rect.width - title_w - 20.0
+ } else {
+ rect.x + start_x
+ };
+ ctx.text(display_title, x_pos, text_y, font_size, text_color);
}
- labels.extend(self.menus.text_labels());
- labels
- }
-
- fn set_visible(&mut self, visible: bool) {
- if self.visible != visible {
- self.visible = visible;
- self.menus.set_visible(visible);
- self.layout_dirty = true;
+ for l in self.menus.text_labels() {
+ ctx.text(l.text, l.x, l.y, l.font_size, l.color);
}
}
- fn visible(&self) -> bool {
- self.visible
- }
-
- fn children(&self, _ctx: &UiContext) -> Vec<*mut (dyn Element + 'static)> {
- let ptr: *const dyn Element = &self.menus as &dyn Element;
- vec![ptr as *mut (dyn Element + 'static)]
+ fn popover(&self, rect: Rect) -> Option<(f32, f32, f32, f32)> {
+ self.context_popover_rect(rect).or_else(|| self.menu_dropdown_rect())
}
- fn z_index(&self) -> i32 {
- self.z_level
- }
-
-
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- if !self.visible {
- return Vec::new();
- }
- let mut quads = Vec::new();
+ fn draw_popover(&self, rect: Rect, pc: &mut dyn crate::layout::RenderTarget) {
+ let label_color = self.text_color();
+ let srgb = crate::colors::to_srgb(label_color);
+ let color_f32 = [srgb[0], srgb[1], srgb[2], 1.0];
+ let font = Paint::widget_font(self);
- // Draw main background
- let (r1, r2, r3, r4) = self.rounded_corners();
- if !r1 && !r2 && !r3 && !r4 {
- quads.push((self.base.x, self.base.y, self.base.w, self.base.h, self.color()));
- }
+ if self.context_dropdown_open {
+ if let Some((dx, dy, dw, dh)) = self.context_popover_rect(rect) {
+ let theme = colors::active_theme();
+ pc.rect(theme.surface_border, dx, dy, dw, dh);
+ pc.rect(theme.surface_bg, dx + 1.0, dy + 1.0, dw - 2.0, dh - 2.0);
+ if let Some(di) = self.context_hovered_item {
+ let iy = dy + di as f32 * DROPDOWN_ITEM_H;
+ pc.rect(colors::PANEL_MENU_HOVER, dx + 2.0, iy + 2.0, dw - 4.0, DROPDOWN_ITEM_H - 4.0);
+ }
- // 1. Highlight the title on hover or open
- if !self.context_options.is_empty() {
- let tr = self.title_rect();
- if self.context_dropdown_open {
- quads.push((tr.0, tr.1, tr.2, tr.3, colors::highlight_primary_color()));
- } else if self.context_title_hovered {
- quads.push((tr.0, tr.1, tr.2, tr.3, colors::HIGHLIGHT_SECONDARY));
+ let bounds = Some([dx, dy, dx + dw, dy + dh]);
+ for (i, option) in self.context_options.iter().enumerate() {
+ let is_selected = self.context_selected == i;
+ let prefix = if is_selected { "✓ " } else { " " };
+ let text = format!("{}{}", prefix, option);
+ let iy = crate::layout::align_text_y(dy + i as f32 * DROPDOWN_ITEM_H, DROPDOWN_ITEM_H, 12.0, 0.0);
+ if let Some(ref f) = font {
+ pc.text_with_font_and_bounds(&text, dx + 8.0, iy, 12.0, color_f32, f, bounds);
+ } else {
+ pc.text_with_bounds(&text, dx + 8.0, iy, 12.0, color_f32, bounds);
+ }
+ }
+ }
+ } else if let Some((dx, dy, dw, dh)) = self.menu_dropdown_rect() {
+ let theme = colors::active_theme();
+ pc.rect(theme.surface_border, dx, dy, dw, dh);
+ pc.rect(theme.surface_bg, dx + 1.0, dy + 1.0, dw - 2.0, dh - 2.0);
+ if let Some(di) = self.hovered_dropdown_item {
+ let iy = dy + di as f32 * DROPDOWN_ITEM_H;
+ pc.rect(colors::PANEL_MENU_HOVER, dx + 2.0, iy + 2.0, dw - 4.0, DROPDOWN_ITEM_H - 4.0);
}
- }
-
- quads.extend(self.menus.extra_quads());
- quads
- }
-
- fn widget_font(&self) -> Option<String> {
- Some(crate::layout::menubar_font())
- }
-
-}
-impl Drop for MenuBar {
- fn drop(&mut self) {
- clear_widget_references(self);
- }
-}
-
-#[derive(Debug, Clone)]
-pub struct Menu {
- pub base: Widget,
- pub title: String,
- pub vertical_title: String,
- pub items: Vec<String>,
- pub item_checked: Vec<Option<bool>>,
- pub open: bool,
- pub vertical: bool,
- hovered_item: Option<usize>,
- clicked_item: Option<usize>,
- was_open: Option<usize>,
- pub parent: Option<*mut (dyn Element + 'static)>,
- pub children: Vec<*mut (dyn Element + 'static)>,
- pub curved_arc: Option<(f32, f32, f32, f32, f32, f32)>,
- pub title_buf: Option<glyphon::Buffer>,
- pub item_bufs: Vec<glyphon::Buffer>,
- pub check_buf: Option<glyphon::Buffer>,
- pub curved_char_bufs: Vec<glyphon::Buffer>,
- pub font_family: String,
-}
-
-impl Menu {
- pub fn new(title: &str, vertical_title: &str, items: &[String]) -> Self {
- Self {
- base: Widget::new(),
- title: title.to_string(),
- vertical_title: vertical_title.to_string(),
- items: items.to_vec(),
- item_checked: vec![None; items.len()],
- open: false,
- vertical: false,
- hovered_item: None,
- clicked_item: None,
- was_open: None,
- parent: None,
- children: Vec::new(),
- curved_arc: None,
- title_buf: None,
- item_bufs: Vec::new(),
- check_buf: None,
- curved_char_bufs: Vec::new(),
- font_family: String::new(),
- }
- }
-
- pub fn active_title(&self) -> &str {
- if self.vertical {
- &self.vertical_title
- } else {
- &self.title
- }
- }
-
- fn dropdown_rect(&self) -> (f32, f32, f32, f32) {
- let dh = self.items.len() as f32 * DROPDOWN_ITEM_H;
- let mut max_len = 0;
- for item in &self.items {
- max_len = max_len.max(item.len());
- }
- let font_setting = crate::layout::menubar_font();
- let (_, font_size_opt) = crate::layout::parse_font_string(&font_setting);
- let font_size = font_size_opt.unwrap_or(12.0);
- let char_w = 7.5 * (font_size / 12.0);
-
- let dw = (max_len as f32 * char_w + 40.0).max(120.0);
- let dx = if self.vertical {
- self.base.x + self.base.w
- } else {
- self.base.x
- };
- let dy = if self.vertical {
- self.base.y
- } else {
- self.base.y + self.base.h
- };
- (dx, dy, dw, dh)
- }
- pub fn text_color(&self) -> [f32; 4] {
- if let Some(p_ptr) = self.parent {
- let mut curr = p_ptr;
- loop {
- if unsafe { (*curr).is_backplate() } {
- return crate::colors::backplate_menubar_text_color();
- }
- let dummy = crate::context::UiContext::new();
- if let Some(next_p) = unsafe { (*curr).parent(&dummy) } {
- curr = next_p;
- } else {
- break;
+ let bounds = Some([dx, dy, dx + dw, dy + dh]);
+ if let Some(menu_idx) = self.menus.selected {
+ if let Some(items) = self.menu_dropdowns.get(menu_idx) {
+ for (i, option) in items.iter().enumerate() {
+ let checked = self.menu_dropdown_checked.get(menu_idx)
+ .and_then(|menu| menu.get(i))
+ .and_then(|&v| v);
+ let prefix = match checked {
+ Some(true) => "✓ ",
+ Some(false) => " ",
+ None => "",
+ };
+ let text = format!("{}{}", prefix, option);
+ let iy = crate::layout::align_text_y(dy + i as f32 * DROPDOWN_ITEM_H, DROPDOWN_ITEM_H, 12.0, 0.0);
+ if let Some(ref f) = font {
+ pc.text_with_font_and_bounds(&text, dx + 8.0, iy, 12.0, color_f32, f, bounds);
+ } else {
+ pc.text_with_bounds(&text, dx + 8.0, iy, 12.0, color_f32, bounds);
+ }
+ }
}
}
}
- crate::colors::menubar_tab_label_color()
}
}
-impl Element for Menu {
- crate::impl_widget_base!(Menu);
- fn label(&self) -> Option<String> {
- Some(self.active_title().to_string())
- }
-
- fn color(&self) -> [f32; 4] {
- [0.0, 0.0, 0.0, 0.0]
+impl Input for MenuBar {
+ fn blocks_backplate_drag(&self) -> bool {
+ false
}
- fn is_active(&self) -> bool {
- self.base.focused || self.open
+ /// Legacy `mouse_input` saw every press: any press closes an open context dropdown, even
+ /// outside the bar.
+ fn gates_presses(&self) -> bool {
+ false
}
- fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
- if ctx.is_coordinate_covered(self as *const Self as *const () as usize, px, py) {
- return false;
- }
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- let dx = px - cx;
- let dy = py - cy;
- let dist = (dx * dx + dy * dy).sqrt();
- if dist >= r - thickness && dist <= r {
- let angle = dy.atan2(dx);
- let mut norm_angle = angle;
- if norm_angle < 0.0 {
- norm_angle += 2.0 * std::f32::consts::PI;
- }
- if norm_angle >= start_angle && norm_angle <= end_angle {
- return true;
- }
- }
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- return true;
- }
- }
+ fn hit(&self, rect: Rect, px: f32, py: f32) -> bool {
+ if !self.visible {
return false;
}
- let (rx, ry, rw, rh) = self.rect();
- if px >= rx && px <= rx + rw && py >= ry && py <= ry + rh {
- return true;
- }
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+ if let Some((dx, dy, dw, dh)) = self.context_popover_rect(rect) {
+ if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
return true;
}
}
- false
- }
-
- fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- self.was_open = None;
- let was_hovering = self.base.hovered;
- self.base.hovered = self.hit_test(px, py, ctx);
- let old_item = self.hovered_item;
- self.hovered_item = None;
-
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
+ if let Some((dx, dy, dw, dh)) = self.menu_dropdown_rect() {
if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if di < self.items.len() {
- self.hovered_item = Some(di);
- }
+ return true;
}
}
-
- was_hovering != self.base.hovered || old_item != self.hovered_item
- }
-
- fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
- if button != MouseButton::Left || state != ElementState::Pressed {
- return false;
- }
- if !self.hit_test(px, py, ctx) {
- return false;
- }
-
- // Check dropdown click if open
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 && px >= dx && px < dx + dw && py >= dy && py < dy + dh {
- let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
- if di < self.items.len() {
- self.clicked_item = Some(di);
- self.open = false;
- return true;
- }
- }
+ if px >= rect.x && px <= rect.x + rect.width && py >= rect.y && py <= rect.y + rect.height {
+ return true;
}
+ // The strip may extend past the assigned rect (clamped layouts).
+ let (sx, sy, sw, sh) = self.menus.rect();
+ px >= sx && px <= sx + sw && py >= sy && py <= sy + sh
+ }
- // Since we passed hit_test and didn't click dropdown, it's a click on the header title
- if self.was_open == Some(0) || self.open {
- self.open = false;
- self.was_open = None;
- } else {
- self.open = true;
- self.was_open = None;
- focus::set_focused(self);
- }
- true
+ fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+ self.menus.set_modifiers(ctrl, shift, alt);
}
- fn focus(&mut self) {
- self.open = true;
- self.base.focused = true;
- focus::set_focused(self);
+ fn visibility_changed(&mut self, visible: bool) {
+ self.visible = visible;
+ self.menus.set_visible(visible);
+ self.layout_dirty = true;
}
- fn unfocus(&mut self) {
- if self.open {
- self.was_open = Some(0);
- }
- self.open = false;
- self.base.focused = false;
- focus::clear_if_matches(self);
- self.hovered_item = None;
+ fn is_focused(&self, _base_focused: bool) -> bool {
+ self.focused || self.context_dropdown_open || self.menus.selected.is_some()
}
fn set_selected(&mut self, selected: bool) {
- self.base.focused = selected;
+ self.focused = selected;
if !selected {
- self.open = false;
- self.was_open = None;
- self.hovered_item = None;
- focus::clear_if_matches(self);
+ self.menus.set_selected(None);
}
}
- fn as_menu_controller(&self) -> Option<&dyn MenuController> { Some(self) }
- fn as_menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> { Some(self) }
-
- fn highlight_quad(&self, _ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])>{
- None
- }
-
- fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
- let mut quads = Vec::new();
- if self.curved_arc.is_none() {
- if self.base.focused || self.open {
- quads.push((self.base.x, self.base.y, self.base.w, self.base.h, colors::highlight_primary_color()));
- } else if self.base.hovered {
- quads.push((self.base.x, self.base.y, self.base.w, self.base.h, colors::HIGHLIGHT_SECONDARY));
- }
- }
- if self.open {
- let (dx, dy, dw, dh) = self.dropdown_rect();
- if dh > 0.0 {
- quads.push((dx, dy, dw, dh, colors::popover_bg_color()));
- if let Some(di) = self.hovered_item {
- quads.push((dx, dy + di as f32 * DROPDOWN_ITEM_H, dw, DROPDOWN_ITEM_H, colors::PANEL_MENU_HOVER));
+ fn on_event(&mut self, event: &Event, ectx: &mut EventCtx) -> bool {
+ match event {
+ Event::PointerMove { x: px, y: py, .. } => {
+ if !self.visible {
+ return false;
}
- }
- }
- quads
- }
+ let (px, py) = (*px, *py);
+ let rect = ectx.rect;
+ self.layout_strip(rect);
- fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
- let mut arcs = Vec::new();
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- if self.base.hovered && !self.open {
- arcs.push((cx, cy, r, thickness, start_angle, end_angle, colors::PANEL_MENU_HOVER));
- }
- }
- arcs
- }
+ let mut changed = false;
- fn text_labels(&self) -> Vec<TextLabel> {
- let label_color = self.text_color();
- let srgb = crate::colors::to_srgb(label_color);
- let text_color = [
- (srgb[0] * 255.0) as u8,
- (srgb[1] * 255.0) as u8,
- (srgb[2] * 255.0) as u8,
- ];
- let mut labels = Vec::new();
- if let Some((cx, cy, r, thickness, start_angle, end_angle)) = self.curved_arc {
- let r_mid = r - thickness / 2.0;
- labels.extend(TextLabel::curved_layout(
- &self.active_title(),
- cx, cy, r_mid,
- start_angle, end_angle,
- 12.0,
- text_color,
- ));
- } else if self.vertical {
- let font_size = 12.0;
- let line_height = font_size * 1.2;
- let title = self.active_title();
- let label_len = title.chars().count() as f32;
- let total_h = label_len * line_height;
- let start_y = self.base.y + (self.base.h - total_h) / 2.0;
- let char_w = crate::widget::display::measure_text("o", font_size);
- let x_pos = self.base.x + (self.base.w - char_w) / 2.0;
- for (i, c) in title.chars().enumerate() {
- let char_str = c.to_string();
- let y_pos = start_y + i as f32 * line_height;
- labels.push(TextLabel {
- text: char_str,
- x: x_pos,
- y: y_pos,
- font_size,
- color: text_color,
- });
- }
- } else {
- labels.push(TextLabel {
- text: self.active_title().to_string(),
- x: self.base.x + crate::layout::paginator_tab_padding_x(),
- y: self.base.y + 7.0,
- font_size: 12.0,
- color: text_color,
- });
- }
- if self.open {
- let (dx, dy, _, _) = self.dropdown_rect();
- for (i, item) in self.items.iter().enumerate() {
- let checked = self.item_checked.get(i).and_then(|&v| v);
- let prefix = match checked {
- Some(true) => "\u{2713} ",
- Some(false) => " ",
- None => "",
- };
- labels.push(TextLabel {
- text: format!("{}{}", prefix, item),
- x: dx + 8.0,
- y: dy + i as f32 * DROPDOWN_ITEM_H + 5.0,
- font_size: 12.0,
- color: text_color,
- });
- }
- }
- labels
- }
+ let old_title_hovered = self.context_title_hovered;
+ self.context_title_hovered = false;
+ if !self.context_options.is_empty() {
+ let tr = self.title_rect(rect);
+ if px >= tr.0 && px <= tr.0 + tr.2 && py >= tr.1 && py <= tr.1 + tr.3 {
+ self.context_title_hovered = true;
+ }
+ }
+ if old_title_hovered != self.context_title_hovered {
+ changed = true;
+ }
- fn set_visible(&mut self, visible: bool) {
- self.base.hovered = false;
- if !visible {
- self.open = false;
- self.was_open = None;
- self.hovered_item = None;
- focus::clear_if_matches(self);
- }
- }
+ let old_hovered_item = self.context_hovered_item;
+ self.context_hovered_item = None;
+ if let Some((dx, dy, dw, dh)) = self.context_popover_rect(rect) {
+ if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+ let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
+ if di < self.context_options.len() {
+ self.context_hovered_item = Some(di);
+ }
+ }
+ }
+ if old_hovered_item != self.context_hovered_item {
+ changed = true;
+ }
- fn parent(&self, _ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
- self.parent
- }
+ let old_hovered_dropdown = self.hovered_dropdown_item;
+ self.hovered_dropdown_item = None;
+ if let Some((dx, dy, dw, dh)) = self.menu_dropdown_rect() {
+ if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+ let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
+ if let Some(menu_idx) = self.menus.selected {
+ if let Some(items) = self.menu_dropdowns.get(menu_idx) {
+ if di < items.len() {
+ self.hovered_dropdown_item = Some(di);
+ }
+ }
+ }
+ }
+ }
+ if old_hovered_dropdown != self.hovered_dropdown_item {
+ changed = true;
+ }
- fn set_parent(&mut self, parent: Option<*mut (dyn Element + 'static)>, _ctx: &mut UiContext) {
- self.parent = parent;
- }
+ if let Some(ui) = ectx.ui.as_deref_mut() {
+ if self.menus.cursor_moved(px, py, ui) {
+ changed = true;
+ }
+ }
+ changed
+ }
+ Event::MouseButton { button, state, x: px, y: py, .. } => {
+ if !self.visible {
+ return false;
+ }
+ if *button != MouseButton::Left {
+ return false;
+ }
+ let (px, py, state) = (*px, *py, *state);
+ let rect = ectx.rect;
+ self.layout_strip(rect);
+
+ let mut changed = false;
+
+ if let Some((dx, dy, dw, dh)) = self.menu_dropdown_rect() {
+ if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+ if state == ElementState::Pressed {
+ let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
+ if let Some(menu_idx) = self.menus.selected {
+ if let Some(items) = self.menu_dropdowns.get(menu_idx) {
+ if di < items.len() {
+ self.clicked_dropdown_item = Some((menu_idx, di));
+ self.close_all();
+ ectx.release_focus();
+ return true;
+ }
+ }
+ }
+ }
+ return true;
+ }
+ }
- fn z_index(&self) -> i32 {
- 100
- }
+ if let Some((dx, dy, dw, dh)) = self.context_popover_rect(rect) {
+ if px >= dx && px < dx + dw && py >= dy && py < dy + dh {
+ if state == ElementState::Pressed {
+ let di = ((py - dy) / DROPDOWN_ITEM_H) as usize;
+ if di < self.context_options.len() {
+ self.context_selected = di;
+ self.context_just_changed = true;
+ self.close_all();
+ ectx.release_focus();
+ if let Some(ref cb) = self.on_context_change_cb {
+ cb(di);
+ }
+ return true;
+ }
+ }
+ }
+ }
- fn focused(&self, _ctx: &UiContext) -> bool {
- self.base.focused
- }
+ if !self.context_options.is_empty() {
+ let tr = self.title_rect(rect);
+ if px >= tr.0 && px <= tr.0 + tr.2 && py >= tr.1 && py <= tr.1 + tr.3 {
+ if state == ElementState::Pressed {
+ if self.context_dropdown_open {
+ self.close_all();
+ ectx.release_focus();
+ } else {
+ self.menus.unfocus();
+ self.context_dropdown_open = true;
+ self.sync_focus(ectx);
+ }
+ }
+ return true;
+ }
+ }
- fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
- let font_setting = crate::layout::menubar_font();
- if self.font_family != font_setting {
- self.font_family = font_setting.clone();
- self.title_buf = None;
- self.curved_char_bufs.clear();
- self.item_bufs.clear();
- }
- let title_text = self.active_title();
- let (font_fam, font_size_opt) = crate::layout::parse_font_string(&font_setting);
- let font_size = font_size_opt.unwrap_or(12.0);
+ if self.context_dropdown_open && state == ElementState::Pressed {
+ self.close_all();
+ ectx.release_focus();
+ changed = true;
+ }
- if let Some((_cx, _cy, _r, _thickness, _start_angle, _end_angle)) = self.curved_arc {
- if self.curved_char_bufs.len() != title_text.chars().count() {
- let font_fam_clone = font_fam.clone();
- self.curved_char_bufs = title_text.chars()
- .map(|c| make_widget_text_buffer(fs, &c.to_string(), font_size, &font_fam_clone))
- .collect();
+ let old_menu_selected = self.menus.selected;
+ if let Some(ui) = ectx.ui.as_deref_mut() {
+ if self.menus.mouse_input(MouseButton::Left, state, px, py, ui) {
+ changed = true;
+ if self.menus.selected.is_some() && old_menu_selected != self.menus.selected {
+ self.sync_focus(ectx);
+ }
+ }
+ }
+ changed
}
- self.title_buf = None;
- } else if self.vertical {
- self.title_buf = None;
- self.curved_char_bufs.clear();
- } else {
- if self.title_buf.is_none() {
- self.title_buf = Some(make_widget_text_buffer(fs, title_text, font_size, &font_fam));
+ Event::KeyInput(key_event) => {
+ if key_event.state != ElementState::Pressed {
+ return false;
+ }
+ if self.context_dropdown_open {
+ match key_event.logical_key {
+ Key::Named(NamedKey::ArrowDown) => {
+ let current = self.context_hovered_item.unwrap_or(self.context_selected);
+ if current + 1 < self.context_options.len() {
+ self.context_hovered_item = Some(current + 1);
+ } else {
+ self.context_hovered_item = Some(0);
+ }
+ return true;
+ }
+ Key::Named(NamedKey::ArrowUp) => {
+ let current = self.context_hovered_item.unwrap_or(self.context_selected);
+ if current > 0 {
+ self.context_hovered_item = Some(current - 1);
+ } else {
+ self.context_hovered_item = Some(self.context_options.len() - 1);
+ }
+ return true;
+ }
+ Key::Named(NamedKey::Enter) | Key::Named(NamedKey::Space) => {
+ if let Some(idx) = self.context_hovered_item {
+ self.context_selected = idx;
+ self.context_just_changed = true;
+ }
+ self.close_all();
+ ectx.release_focus();
+ return true;
+ }
+ Key::Named(NamedKey::Escape) => {
+ self.close_all();
+ ectx.release_focus();
+ return true;
+ }
+ _ => {}
+ }
+ }
+ match ectx.ui.as_deref_mut() {
+ Some(ui) => self.menus.keyboard_input(key_event, ui),
+ None => false,
+ }
}
- self.curved_char_bufs.clear();
- }
-
- if self.open {
- if self.item_bufs.len() != self.items.len() {
- let font_fam_clone = font_fam.clone();
- self.item_bufs = self.items.iter().enumerate().map(|(i, item)| {
- let checked = self.item_checked.get(i).and_then(|&v| v);
- let prefix = match checked {
- Some(true) => "\u{2713} ",
- Some(false) => " ",
- None => "",
- };
- let text = format!("{}{}", prefix, item);
- make_widget_text_buffer(fs, &text, font_size, &font_fam_clone)
- }).collect();
+ // Hosts call `focus()`/`unfocus()` directly; legacy semantics: focus is claimed
+ // conditionally (only while something is open), unfocus closes everything.
+ Event::FocusIn => {
+ self.sync_focus(ectx);
+ true
}
- } else {
- self.item_bufs.clear();
+ Event::FocusOut => {
+ self.close_all();
+ ectx.release_focus();
+ true
+ }
+ _ => false,
}
}
- fn get_text_items(&self) -> Vec<(&glyphon::Buffer, f32, f32, glyphon::Color)> {
- Vec::new()
+ fn menu_controller(&self) -> Option<&dyn MenuController> {
+ Some(self)
}
-
- fn widget_font(&self) -> Option<String> {
- Some(crate::layout::menubar_font())
+ fn menu_controller_mut(&mut self) -> Option<&mut dyn MenuController> {
+ Some(self)
}
-}
-
-unsafe impl Send for Menu {}
-unsafe impl Sync for Menu {}
-
-impl Drop for Menu {
- fn drop(&mut self) {
- clear_widget_references(self);
- }
-}
-
-impl MenuController for Menu {
- fn menu_click(&mut self) -> Option<(usize, usize)> {
- self.clicked_item.take().map(|i| (0, i))
+ fn page_selector(&self) -> Option<&dyn PageSelector> {
+ Some(self)
}
- fn trigger_menu_click(&mut self, _menu_idx: usize, item_idx: usize) {
- if item_idx < self.items.len() {
- self.clicked_item = Some(item_idx);
- }
+ fn page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> {
+ Some(self)
}
- fn set_item_checked(&mut self, _menu_idx: usize, item_idx: usize, checked: bool) {
- if item_idx < self.item_checked.len() {
- self.item_checked[item_idx] = Some(checked);
- self.item_bufs.clear();
- }
- }
- fn set_menu_items(&mut self, _menu_idx: usize, items: &[String]) {
- self.items = items.to_vec();
- self.item_checked = vec![None; items.len()];
- self.item_bufs.clear();
- }
- fn is_menu_bar(&self) -> bool { false }
- fn is_menu_open(&self) -> bool { self.open }
- fn menu_items(&self) -> Vec<String> { self.items.clone() }
- fn menu_item_checked(&self) -> Vec<Option<bool>> { self.item_checked.clone() }
- fn is_vertical(&self) -> bool { self.vertical }
- fn menu_names(&self) -> Vec<String> { vec![self.active_title().to_string()] }
- fn menu_items_list(&self) -> Vec<Vec<String>> { vec![self.items.clone()] }
- fn menu_checked_list(&self) -> Vec<Vec<Option<bool>>> { vec![self.item_checked.clone()] }
- fn take_context_change(&mut self) -> Option<usize> { None }
- fn set_context_selected(&mut self, _selected: usize) {}
- fn set_center_items(&mut self, _center: bool) {}
- fn get_menu_items_at(&self, _px: f32, _py: f32) -> Option<(usize, String, Vec<String>, f32, f32, f32, f32)> { None }
}
-
impl MenuController for MenuBar {
fn menu_click(&mut self) -> Option<(usize, usize)> {
let _ = self.menus.take_click();
-
+
if let Some((menu_idx, item_idx)) = self.clicked_dropdown_item.take() {
if let Some(ref cb) = self.on_menu_click_cb {
cb(menu_idx, item_idx);
@@ -1769,7 +1167,6 @@ impl PageSelector for MenuBar {
fn set_sidebar_label(&mut self, label: Option<String>) {
self.label = label.clone();
- self.base.label = label.clone();
if self.vertical {
self.title = label.unwrap_or_default();
self.label = None;
@@ -1779,3 +1176,67 @@ impl PageSelector for MenuBar {
fn add_widget_to_page(&mut self, _page_idx: usize, _widget: *mut (dyn Element + 'static), _ctx: &mut UiContext) {}
fn clear_page_widgets(&mut self, _page_idx: usize, _ctx: &mut UiContext) {}
}
+
+unsafe impl Send for MenuBar {}
+unsafe impl Sync for MenuBar {}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::context::UiContext;
+
+ fn bar() -> Adapted<MenuBar> {
+ MenuBar::new(0.0, 0.0, 400.0, 24.0)
+ .with_title("Test")
+ .with_item("File", &["New", "Save"])
+ .with_item("Edit", &["Undo"])
+ }
+
+ #[test]
+ fn menu_open_click_and_controller_roundtrip() {
+ let mut ctx = UiContext::new();
+ let mut mb = bar();
+ let (id, ptr) = (mb.id(), mb.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ Element::set_rect(&mut mb, 0.0, 0.0, 400.0, 24.0);
+
+ // Click the "File" strip button (the strip commits selection on release): the dropdown
+ // opens, the bar reports focused (conditional focus), and a popover rect exists.
+ let (bx, by, bw, bh) = mb.menus.item_rect(0);
+ assert!(bw > 0.0, "strip laid out");
+ assert!(mb.mouse_input(MouseButton::Left, ElementState::Pressed, bx + bw / 2.0, by + bh / 2.0, &mut ctx));
+ assert!(mb.mouse_input(MouseButton::Left, ElementState::Released, bx + bw / 2.0, by + bh / 2.0, &mut ctx));
+ let elem: &dyn Element = &mb;
+ assert!(elem.as_menu_controller().unwrap().is_menu_open(), "dropdown open");
+ assert!(Element::focused(&mb, &ctx), "bar holds focus while open");
+ let (dx, dy, _, _) = Element::popover_rect(&mb).expect("dropdown popover");
+
+ // Click the second item ("Save"): menu_click reports (0, 1) and everything closes.
+ assert!(mb.mouse_input(MouseButton::Left, ElementState::Pressed, dx + 10.0, dy + DROPDOWN_ITEM_H * 1.5, &mut ctx));
+ {
+ let elem: &mut dyn Element = &mut mb;
+ assert_eq!(elem.as_menu_controller_mut().unwrap().menu_click(), Some((0, 1)));
+ assert!(!elem.as_menu_controller().unwrap().is_menu_open());
+ }
+ assert!(!Element::focused(&mb, &ctx), "focus released after the click");
+
+ // The PageSelector capability rides the same hooks.
+ let elem: &dyn Element = &mb;
+ assert!(elem.as_page_selector().unwrap().sidebar_w() > 0.0);
+ }
+
+ #[test]
+ fn hidden_menubar_reports_no_menu_and_rejects_hits() {
+ let mut ctx = UiContext::new();
+ let mut mb = bar();
+ let (id, ptr) = (mb.id(), mb.as_ptr_mut());
+ ctx.register_widget(id, ptr);
+ Element::set_rect(&mut mb, 0.0, 0.0, 400.0, 24.0);
+
+ Element::set_visible(&mut mb, false);
+ let elem: &dyn Element = &mb;
+ assert!(!elem.as_menu_controller().unwrap().is_menu_bar(), "hidden bar is not a menu bar");
+ assert!(!Element::hit_test(&mb, 10.0, 10.0, &ctx));
+ assert!(elem.as_menu_controller().unwrap().get_menu_items_at(10.0, 10.0).is_none());
+ }
+}
diff --git a/src/widget/container/mod.rs b/src/widget/container/mod.rs
index d8e1b0e..43a385b 100644
--- a/src/widget/container/mod.rs
+++ b/src/widget/container/mod.rs
@@ -29,7 +29,7 @@ pub use section_container::SectionContainer;
pub use header::Header;
pub use content_bg::ContentBg;
pub use parameters_bg::ParametersBg;
-pub use menu::{Menu, MenuBar};
+pub use menu::MenuBar;
pub use breadcrumb::Breadcrumb;
pub use spreadsheet::Spreadsheet;
pub use scroll_box::ScrollBox;
diff --git a/src/widget/container/spreadsheet.rs b/src/widget/container/spreadsheet.rs
index 60b65b8..c1cb14d 100644
--- a/src/widget/container/spreadsheet.rs
+++ b/src/widget/container/spreadsheet.rs
@@ -108,7 +108,7 @@ impl Paint for Spreadsheet {
colors::PARAM_BG
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
// Legacy: rounded_corners override (all corners) with the Element-default 12.0 radius.
Some((12.0, (true, true, true, true)))
}
diff --git a/src/widget/container/switcher.rs b/src/widget/container/switcher.rs
index 9759fa6..b1e44b0 100644
--- a/src/widget/container/switcher.rs
+++ b/src/widget/container/switcher.rs
@@ -89,7 +89,7 @@ impl Layout for Switcher {
}
/// The active child fills the switcher's rect.
- fn arrange_children(&mut self, rect: Rect) {
+ fn arrange_children(&mut self, rect: Rect, _host: *mut (dyn Element + 'static)) {
if let Some(child) = self.active_child() {
unsafe {
(*child).set_rect(rect.x, rect.y, rect.width, rect.height);
@@ -120,7 +120,7 @@ impl Paint for Switcher {
}
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
// Legacy kept the Element-default 12.0 radius and proxied the corner flags.
let corners = match self.active_child() {
Some(child) => unsafe { (*child).rounded_corners() },
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index 7e33a66..4b86ffd 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -588,7 +588,7 @@ impl Paint for Graph {
self.bg_color()
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
Some((WIDGET_RADIUS, WIDGET_CORNERS))
}
diff --git a/src/widget/display/progress_bar.rs b/src/widget/display/progress_bar.rs
index 0907fdf..c3c10aa 100644
--- a/src/widget/display/progress_bar.rs
+++ b/src/widget/display/progress_bar.rs
@@ -30,7 +30,7 @@ impl Paint for ProgressBar {
colors::progress_bg()
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
Some((crate::layout::slider_corner_radius(), (true, true, true, true)))
}
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index e96a23c..7bb92c0 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -239,7 +239,7 @@ impl Paint for Button {
}
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
let r = crate::layout::button_corner_radius();
if r > 0.0 {
Some((r, (true, true, true, true)))
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 421983d..52ac0f2 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -271,7 +271,7 @@ impl Paint for Toggle {
colors::toggle_bg_color()
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
let r = crate::layout::toggle_corner_radius();
if r > 0.0 {
Some((r, (true, true, true, true)))
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 63b9205..c379c26 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -197,7 +197,7 @@ impl Paint for Slider {
[0.0, 0.0, 0.0, 0.0]
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
let r = crate::layout::slider_corner_radius();
if r > 0.0 {
Some((r, (true, true, true, true)))
@@ -496,7 +496,7 @@ impl Paint for RangeSlider {
[0.0, 0.0, 0.0, 0.0]
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
let r = crate::layout::rangeslider_corner_radius();
if r > 0.0 {
Some((r, (true, true, true, true)))
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index 6fdfaeb..7777327 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -177,7 +177,7 @@ impl Paint for Spinbox {
[0.0, 0.0, 0.0, 0.0]
}
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
let r = crate::layout::spinbox_corner_radius();
if r > 0.0 {
Some((r, (true, true, true, true)))
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 6de7ce2..eddfa22 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -834,7 +834,7 @@ pub use self::container::{
Container, ContainerLayout, OverlayLayout, ManualLayout, VerticalLayout, GridLayout, AdaptiveGridLayout,
ColumnsLayout, MosaicLayout, ReverseMosaicLayout,
SectionContainer, Header, ContentBg, ParametersBg, List, ListColumn, ListRow, ColumnWidth,
- ScrollBox, Menu, MenuBar, Spreadsheet, Breadcrumb, Plate,
+ ScrollBox, MenuBar, Spreadsheet, Breadcrumb, Plate,
Switcher, Layer, Page, Backplate, Paginator, ScrollBar, TreeList, TreeElement, ControlPanel,
SplitBox, SplitDirection
};
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 0bbb22a..3d199a0 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -29,8 +29,9 @@
use crate::scene::layout::{Rect, Size, Style};
use crate::scene::paint::{PaintCtx, Prim};
use crate::widget::{
- Element, Event, GeomController, GraphController, MenuController, ParamController,
- PathController, SpreadsheetController, TextLabel, UiContext, Widget, WidgetId,
+ Element, Event, GeomController, GraphController, MenuController, PageSelector,
+ ParamController, PathController, SpreadsheetController, TextLabel, UiContext, Widget,
+ WidgetId,
};
/// Layout inputs for the scene layout engine — the RFC's `Widget` concern, named `Layout` here to
@@ -122,8 +123,10 @@ pub trait Layout {
}
/// Position children after a `set_rect` (no ctx available — use the owned pointers).
- /// Called only while the widget is visible, matching the legacy overrides.
- fn arrange_children(&mut self, _rect: Rect) {}
+ /// Called only while the widget is visible, matching the legacy overrides. `host` is the
+ /// adapter's `*mut dyn Element` — widgets that embed a legacy child (MenuBar's
+ /// ButtonStrip) parent it back to the host so legacy parent-chain styling walks work.
+ fn arrange_children(&mut self, _rect: Rect, _host: *mut (dyn Element + 'static)) {}
/// Recursive child layout for the `Element::layout` pass (this one has ctx). Called after
/// the adapter has measured and placed the container itself, only while visible.
@@ -134,6 +137,20 @@ pub trait Layout {
fn child_visible(&self, _child: *mut (dyn Element + 'static)) -> bool {
true
}
+
+ /// Legacy `Element::z_index` (host render ordering; MenuBar's dropdowns layer at 100+).
+ fn z_order(&self) -> i32 {
+ 0
+ }
+
+ /// `Some(parent)` when the model tracks its parent pointer itself (via
+ /// [`parent_changed`](Layout::parent_changed)) — the adapter then serves `Element::parent`
+ /// from it instead of the tree. Legacy widgets with parent-dependent styling walk the
+ /// chain with a DUMMY ctx (MenuBar/ButtonStrip backplate checks), which a tree lookup
+ /// cannot answer. `None` (default): use the tree.
+ fn tracked_parent(&self) -> Option<Option<*mut (dyn Element + 'static)>> {
+ None
+ }
}
/// The paint concern — a widget's fill color, its own (non-recursive) geometry emission, and
@@ -161,15 +178,26 @@ pub trait Paint {
false
}
- /// Corner rounding `(radius, per-corner flags)` of the widget's background. **Transitional:**
- /// this exists only for legacy render paths that draw widget backgrounds themselves from
- /// style properties (`widget_vertices` / `push_widget_vertices` readers of
+ /// Corner rounding `(radius, per-corner flags)` of the widget's background, given its
+ /// laid-out rect (MenuBar's corners depend on where it sits against its parent's edges).
+ /// **Transitional:** this exists only for legacy render paths that draw widget backgrounds
+ /// themselves from style properties (`widget_vertices` / `push_widget_vertices` readers of
/// `Element::corner_radius` + `rounded_corners`) — the widget's real geometry is whatever
/// [`paint`](Paint::paint) emits. Dies with those paths. Default: sharp corners.
- fn corner_style(&self) -> Option<(f32, (bool, bool, bool, bool))> {
+ fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
None
}
+ /// This widget's OWN popover (dropdown) rect, if one is open — hosts float it above
+ /// z-ordered siblings (`register_popover` + `render_popovers`). Containers combine this
+ /// with their children's popovers in the adapter. Default: none.
+ fn popover(&self, _rect: Rect) -> Option<(f32, f32, f32, f32)> {
+ None
+ }
+
+ /// Draw this widget's own popover (legacy `Element::render_popover`).
+ fn draw_popover(&self, _rect: Rect, _pc: &mut dyn crate::layout::RenderTarget) {}
+
/// Solid border `(color, thickness)` of the widget's background quad. **Transitional**, like
/// [`corner_style`](Paint::corner_style): `render_widget` gives a widget's background quad a
/// border+inset treatment when this is `Some` — `Toggle`'s square mode depends on it.
@@ -242,6 +270,14 @@ impl EventCtx<'_> {
}
}
+ /// Drop this widget's claim on the global focus if it holds it (legacy
+ /// `focus::clear_if_matches(self)` — MenuBar releases focus when its dropdowns close).
+ pub fn release_focus(&mut self) {
+ if let Some(ptr) = self.self_ptr {
+ unsafe { crate::widget::focus::clear_if_matches(&mut *ptr) };
+ }
+ }
+
/// Open the shared context menu on this widget (legacy `ctx.handle_right_click(self, …)`),
/// for widgets that must do work *before* the menu opens — Breadcrumb records which segment
/// was right-clicked first, so the menu header can show that segment's path.
@@ -432,6 +468,28 @@ pub trait Input {
/// Copy this widget's path/content to the clipboard — the context menu's "Copy Path" action
/// calls `Element::copy_path` on its target (Breadcrumb is the only implementor).
fn copy_path(&self) {}
+
+ /// Keyboard modifier state pushed in by hosts before dispatch (legacy
+ /// `Element::set_modifiers`).
+ fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
+
+ /// The widget's visibility flag changed through `Element::set_visible` (the adapter owns
+ /// the flag) — legacy hideable widgets used the setter for side effects (MenuBar closes
+ /// its dropdowns and invalidates layout).
+ fn visibility_changed(&mut self, _visible: bool) {}
+
+ /// The widget's `Element::focused` answer, given the base flag — MenuBar reports focused
+ /// while any of its dropdowns is open, beyond the flag itself. Default: the flag.
+ fn is_focused(&self, base_focused: bool) -> bool {
+ base_focused
+ }
+
+ fn page_selector(&self) -> Option<&dyn PageSelector> {
+ None
+ }
+ fn page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> {
+ None
+ }
}
/// Wraps a narrow-trait widget `W` so it lives in the legacy `*mut dyn Element` tree. Carries the
@@ -681,7 +739,10 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
}
fn set_visible(&mut self, visible: bool) {
- self.visible = visible;
+ if self.visible != visible {
+ self.visible = visible;
+ Input::visibility_changed(&mut self.inner, visible);
+ }
}
fn visible(&self) -> bool {
self.visible
@@ -754,6 +815,32 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
false
}
+ fn z_index(&self) -> i32 {
+ Layout::z_order(&self.inner)
+ }
+
+ fn parent(&self, ctx: &UiContext) -> Option<*mut (dyn Element + 'static)> {
+ if let Some(tracked) = Layout::tracked_parent(&self.inner) {
+ return tracked;
+ }
+ ctx.tree.parent_ptr(self.base.id())
+ }
+
+ fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
+ Input::set_modifiers(&mut self.inner, ctrl, shift, alt)
+ }
+
+ fn focused(&self, _ctx: &UiContext) -> bool {
+ Input::is_focused(&self.inner, self.base.focused)
+ }
+
+ fn as_page_selector(&self) -> Option<&dyn PageSelector> {
+ Input::page_selector(&self.inner)
+ }
+ fn as_page_selector_mut(&mut self) -> Option<&mut dyn PageSelector> {
+ Input::page_selector_mut(&mut self.inner)
+ }
+
fn layout(&mut self, origin: crate::widget::Point, constraints: crate::widget::LayoutConstraints, ctx: &mut UiContext) {
// The Element default (measure + set_rect), plus recursive child layout for visible
// containers — the ctx-carrying half of the arrangement the model can't do in
@@ -788,13 +875,15 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
if !self.visible() {
return None;
}
- self.visible_children().into_iter().find_map(|c| unsafe { &*c }.popover_rect())
+ Paint::popover(&self.inner, self.content_rect())
+ .or_else(|| self.visible_children().into_iter().find_map(|c| unsafe { &*c }.popover_rect()))
}
fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
if !self.visible() {
return;
}
+ Paint::draw_popover(&self.inner, self.content_rect(), pc);
for child in self.visible_children() {
unsafe { &*child }.render_popover(pc);
}
@@ -835,7 +924,8 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
// overrides); hidden containers skip it, like the legacy impls.
if self.visible {
let content = self.content_rect();
- Layout::arrange_children(&mut self.inner, content);
+ let host = self.as_ptr_mut();
+ Layout::arrange_children(&mut self.inner, content, host);
}
}
@@ -872,10 +962,10 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
}
fn corner_radius(&self) -> f32 {
// 12.0 mirrors the `Element` default for widgets without a corner style.
- Paint::corner_style(&self.inner).map_or(12.0, |(r, _)| r)
+ Paint::corner_style(&self.inner, self.content_rect()).map_or(12.0, |(r, _)| r)
}
fn rounded_corners(&self) -> (bool, bool, bool, bool) {
- Paint::corner_style(&self.inner).map_or((false, false, false, false), |(_, c)| c)
+ Paint::corner_style(&self.inner, self.content_rect()).map_or((false, false, false, false), |(_, c)| c)
}
fn solid_border(&self) -> Option<([f32; 4], f32)> {
Paint::solid_border(&self.inner)
@@ -1208,12 +1298,14 @@ impl<W: Layout + Paint + Input + 'static> Element for Adapted<W> {
/// tell the widget via the same `FocusIn`/`FocusOut` events the router would send.
fn focus(&mut self) {
self.base.focused = true;
- let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: None };
+ let self_ptr = self.as_ptr_mut();
+ let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: Some(self_ptr) };
Input::on_event(&mut self.inner, &Event::FocusIn, &mut ectx);
}
fn unfocus(&mut self) {
self.base.focused = false;
- let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: None };
+ let self_ptr = self.as_ptr_mut();
+ let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: Some(self_ptr) };
Input::on_event(&mut self.inner, &Event::FocusOut, &mut ectx);
}