Touch-friendly egui widgets on mobile; egui-based inspector header

Add mobile::apply_touch_style(ctx) — applied every frame when mobile is active —
enlarging egui's global spacing/sizing (interact_size, button padding, slider
width/rail, combo width/height, icon sizes, scrollbar, body/button text) so the
standard widgets in panes and dialogs are finger-sized. Desktop is untouched.

Rework the mobile inspector header into a single row using real egui widgets:
title (left, truncating) + Timeline/Nodes jump buttons + ✕ close, laid out with
egui layouts so they inherit the touch sizing and theme visuals instead of being
hand-painted.
This commit is contained in:
Skyler Lehmkuhl 2026-07-01 07:28:08 -04:00
parent 943ff7c15f
commit 4b2802076b
3 changed files with 70 additions and 44 deletions

View File

@ -5346,6 +5346,10 @@ impl eframe::App for EditorApp {
// Apply the theme's palette to egui's global visuals so the standard widgets (dialogs, pane // Apply the theme's palette to egui's global visuals so the standard widgets (dialogs, pane
// chrome, text) share the same colors as the mobile UI and respond to light/dark/user themes. // chrome, text) share the same colors as the mobile UI and respond to light/dark/user themes.
self.theme.apply_to_egui(ctx); self.theme.apply_to_egui(ctx);
// On mobile, enlarge egui spacing/sizing so widgets in panes/dialogs are touch-friendly.
if self.mobile_active() {
mobile::apply_touch_style(ctx);
}
// === Raster fault-in (Phase 3 paging) === // === Raster fault-in (Phase 3 paging) ===
// The canvas records raster keyframe ids whose `raw_pixels` weren't resident // The canvas records raster keyframe ids whose `raw_pixels` weren't resident

View File

@ -11,8 +11,7 @@ use crate::panes::{NodePath, SharedPaneState};
use crate::RenderContext; use crate::RenderContext;
const GRAB_H: f32 = 16.0; const GRAB_H: f32 = 16.0;
const HEAD_H: f32 = 30.0; const HEAD_H: f32 = 44.0;
const CHIP_H: f32 = 30.0;
fn inspector_path() -> NodePath { fn inspector_path() -> NodePath {
vec![MOBILE_NS, 200] vec![MOBILE_NS, 200]
@ -123,61 +122,54 @@ pub fn render(
ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical); ui.ctx().set_cursor_icon(egui::CursorIcon::ResizeVertical);
} }
// Header row: title + close. // Single header row (egui widgets — they inherit the mobile touch sizing + theme visuals):
// title on the left, [Timeline] [Nodes] jump buttons + ✕ close on the right.
let head_y = rect.top() + GRAB_H; let head_y = rect.top() + GRAB_H;
let head = egui::Rect::from_min_max( let head = egui::Rect::from_min_max(
egui::pos2(rect.left(), head_y), egui::pos2(rect.left(), head_y),
egui::pos2(rect.right(), head_y + HEAD_H), egui::pos2(rect.right(), head_y + HEAD_H),
); );
ui.painter().text( let title_text = title(&rc.shared);
egui::pos2(head.left() + 14.0, head.center().y), let mut jump: Option<(usize, usize)> = None;
egui::Align2::LEFT_CENTER, let mut do_close = false;
title(&rc.shared), let mut hui = ui.new_child(
egui::FontId::proportional(13.0), egui::UiBuilder::new()
pal.text, .max_rect(head.shrink2(egui::vec2(12.0, 5.0)))
.layout(egui::Layout::left_to_right(egui::Align::Center)),
); );
let close = egui::Rect::from_min_size(egui::pos2(head.right() - 36.0, head.top()), egui::vec2(36.0, HEAD_H)); hui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let cresp = ui.interact(close, ui.id().with("mobile_inspector_close"), egui::Sense::click()); if ui
ui.painter().text( .add(egui::Button::new(egui::RichText::new(super::icons::X).font(super::icons::font(18.0))).frame(false))
close.center(), .clicked()
egui::Align2::CENTER_CENTER, {
super::icons::X, do_close = true;
super::icons::font(16.0), }
if cresp.hovered() { pal.text } else { pal.text_dim }, ui.add_space(4.0);
); // Reversed so the visual order stays Timeline, then Nodes.
if cresp.clicked() { for chip in CHIPS.iter().rev() {
if ui.button(chip.label).clicked() {
jump = Some(chip.window);
}
}
// Title fills the remaining space on the left.
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.add(egui::Label::new(egui::RichText::new(title_text).color(pal.text)).truncate());
});
});
if do_close {
rc.shared.selection.clear(); rc.shared.selection.clear();
*rc.shared.focus = FocusSelection::None; *rc.shared.focus = FocusSelection::None;
} }
if let Some((top, count)) = jump {
// Jump-to chips (reframe to a related surface, keeping the selection).
let chip_y = head.bottom();
let mut cx = rect.left() + 12.0;
for (i, chip) in CHIPS.iter().enumerate() {
let galley = ui.painter().layout_no_wrap(
chip.label.to_string(),
egui::FontId::proportional(11.0),
pal.text,
);
let w = galley.size().x + 18.0;
let chip_rect = egui::Rect::from_min_size(egui::pos2(cx, chip_y + 4.0), egui::vec2(w, CHIP_H - 8.0));
let resp = ui.interact(chip_rect, ui.id().with(("mobile_inspector_chip", i)), egui::Sense::click());
ui.painter().rect_filled(chip_rect, 11.0, if resp.hovered() { pal.line } else { pal.surface_alt });
ui.painter().rect_stroke(chip_rect, 11.0, egui::Stroke::new(1.0, pal.line), egui::StrokeKind::Inside);
ui.painter().galley(chip_rect.center() - galley.size() * 0.5, galley, pal.text);
if resp.clicked() {
let (top, count) = chip.window;
state.window_top = top; state.window_top = top;
state.window_count = count; state.window_count = count;
state.weights = [1.0, 1.0, 1.0]; state.weights = [1.0, 1.0, 1.0];
state.anim = None; state.anim = None;
} }
cx += w + 6.0;
}
// Properties content — reuse the Infopanel full-bleed. // Properties content — reuse the Infopanel full-bleed.
let content = egui::Rect::from_min_max( let content = egui::Rect::from_min_max(
egui::pos2(rect.left(), chip_y + CHIP_H), egui::pos2(rect.left(), head.bottom()),
rect.max, rect.max,
); );
if content.height() > 1.0 { if content.height() > 1.0 {

View File

@ -44,6 +44,36 @@ pub fn dialog_width(ctx: &egui::Context, desired: f32) -> f32 {
desired.min(avail.max(200.0)) desired.min(avail.max(200.0))
} }
/// Enlarge egui's spacing/sizing so the standard widgets (buttons, dropdowns, sliders, text fields)
/// in panes and dialogs are touch-friendly. Applied every frame after the theme visuals when the
/// mobile shell is active. The mobile chrome (transport, omnibutton, headers) is hand-sized already;
/// this targets the egui-widget content inside panes.
pub fn apply_touch_style(ctx: &egui::Context) {
use egui::{FontId, TextStyle};
ctx.style_mut(|s| {
let sp = &mut s.spacing;
// Minimum touch target height for buttons/sliders/checkboxes.
sp.interact_size = egui::vec2(sp.interact_size.x.max(44.0), 38.0);
sp.button_padding = egui::vec2(12.0, 9.0);
sp.item_spacing = egui::vec2(10.0, 10.0);
sp.slider_width = 200.0;
sp.slider_rail_height = 10.0;
sp.combo_width = 200.0;
sp.combo_height = 360.0;
sp.text_edit_width = 220.0;
sp.icon_width = 24.0;
sp.icon_width_inner = 14.0;
sp.icon_spacing = 8.0;
sp.scroll.bar_width = 16.0;
sp.menu_margin = egui::Margin::same(8);
// Larger text for touch legibility.
s.text_styles.insert(TextStyle::Body, FontId::proportional(15.0));
s.text_styles.insert(TextStyle::Button, FontId::proportional(15.0));
s.text_styles.insert(TextStyle::Monospace, FontId::monospace(13.0));
s.text_styles.insert(TextStyle::Small, FontId::proportional(12.0));
});
}
/// Returns true if the mobile UI is requested via the `LB_MOBILE_UI` env var. /// Returns true if the mobile UI is requested via the `LB_MOBILE_UI` env var.
/// Any non-empty value other than "0" enables it. /// Any non-empty value other than "0" enables it.
pub fn is_mobile_env() -> bool { pub fn is_mobile_env() -> bool {