UI: toolbars that don't fit the window switch to two columns or scroll (#57) (#112)

* UI: toolbars that don't fit the window switch to two columns or scroll (#57)

In a short window the Tools panel lays its tools out in two columns,
without changing the single/double-column preference, which comes back
when the window grows again. While the window forces two columns the
column toggle is disabled. Whatever two columns can't fit scrolls
vertically.

The Control panel and the application bar scroll sideways when their
controls are wider than the window. The Control panel keeps its height.
In the application bar the workspace controls follow the menus instead of
overlapping them. A plain mouse wheel scrolls these one-direction bars,
and a thin floating scroll bar shows only when something overflows, so
nothing changes while everything fits.

Tool buttons, icon buttons and the Fill proxy carry accessibility
labels, which the new window-size tests use to find them.

* UI: captions in a sideways-scrolled bar stay on one line

The last group of a scrolled Control bar row got only the width left
over, so a caption like Columns wrapped one letter per line.
This commit is contained in:
aiongg 2026-10-10 01:09:52 -04:00 committed by GitHub
parent 4564ec6707
commit d730d8fa27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 364 additions and 98 deletions

View File

@ -21,86 +21,105 @@ pub fn app_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
let full = ui.max_rect();
let mut menus_end = full.min.x;
let mut tools_start = full.max.x;
ui.horizontal_centered(|ui| {
if icons::button(ui, "home", 24.0, app.session.active().is_none(), crate::i18n::tr(&app.ui.language, "Home")).clicked() {
app.session_home();
}
ui.add_space(6.0);
if !app.native_menu {
ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE;
ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE;
ui.style_mut().spacing.button_padding = vec2(7.0, 3.0);
crate::menus::menu_bar(app, ui);
}
menus_end = ui.min_rect().max.x;
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(4.0);
// Search field.
let (r, _) = ui.allocate_exact_size(vec2(125.0, 18.0), Sense::click());
ui.painter().rect(r, 1.0, t.input, Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside);
icons::paint(ui.painter(), egui::Rect::from_min_size(r.min + vec2(3.0, 2.0), vec2(14.0, 14.0)), "search", t.icon);
crate::rtl::paint(
ui.painter(),
r.min + vec2(20.0, 9.0),
egui::Align2::LEFT_CENTER,
crate::i18n::tr(&app.ui.language, "Search"),
egui::FontId::proportional(11.0),
t.text_dim,
);
ui.add_space(8.0);
let current = app.ui.workspace.clone();
let shown = crate::i18n::workspace_name(&app.ui.language, &current);
ui.menu_button(crate::rtl::widget(ui, egui::RichText::new(format!("{shown} ▾")).font(semibold(11.5)).color(t.text)), |ui| {
let customs: Vec<String> = app.ui.custom_workspaces.iter().map(|w| w.name.clone()).collect();
for w in
["Essentials", "Advanced", "Book", "Digital Publishing", "Interactive for PDF", "Printing and Proofing", "Typography"]
.into_iter()
.map(str::to_string)
.chain(customs.iter().cloned())
{
if ui.selectable_label(w == current, crate::rtl::widget(ui, crate::i18n::workspace_name(&app.ui.language, &w))).clicked()
// The right-hand controls' width, measured last frame: in a window too narrow for them
// and the menus they follow the menus, and the bar scrolls sideways.
let tools_w_id = ui.id().with("tools_width");
let tools_w: f32 = ui.data(|d| d.get_temp(tools_w_id)).unwrap_or(0.0);
crate::widgets::overflow_scrolling(ui);
egui::ScrollArea::horizontal().id_salt("app_bar_scroll").auto_shrink([false, false]).show(ui, |ui| {
ui.horizontal_centered(|ui| {
if icons::button(ui, "home", 24.0, app.session.active().is_none(), crate::i18n::tr(&app.ui.language, "Home")).clicked() {
app.session_home();
}
ui.add_space(6.0);
if !app.native_menu {
ui.style_mut().visuals.widgets.inactive.weak_bg_fill = Color32::TRANSPARENT;
ui.style_mut().visuals.widgets.inactive.bg_stroke = Stroke::NONE;
ui.style_mut().visuals.widgets.hovered.bg_stroke = Stroke::NONE;
ui.style_mut().spacing.button_padding = vec2(7.0, 3.0);
crate::menus::menu_bar(app, ui);
}
menus_end = ui.min_rect().max.x;
let size = vec2(ui.available_width().max(tools_w), ui.available_height());
let tools = ui.allocate_ui_with_layout(size, egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(4.0);
// Search field.
let (r, _) = ui.allocate_exact_size(vec2(125.0, 18.0), Sense::click());
ui.painter().rect(r, 1.0, t.input, Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside);
icons::paint(ui.painter(), egui::Rect::from_min_size(r.min + vec2(3.0, 2.0), vec2(14.0, 14.0)), "search", t.icon);
crate::rtl::paint(
ui.painter(),
r.min + vec2(20.0, 9.0),
egui::Align2::LEFT_CENTER,
crate::i18n::tr(&app.ui.language, "Search"),
egui::FontId::proportional(11.0),
t.text_dim,
);
ui.add_space(8.0);
let current = app.ui.workspace.clone();
let shown = crate::i18n::workspace_name(&app.ui.language, &current);
ui.menu_button(crate::rtl::widget(ui, egui::RichText::new(format!("{shown} ▾")).font(semibold(11.5)).color(t.text)), |ui| {
let customs: Vec<String> = app.ui.custom_workspaces.iter().map(|w| w.name.clone()).collect();
for w in
["Essentials", "Advanced", "Book", "Digital Publishing", "Interactive for PDF", "Printing and Proofing", "Typography"]
.into_iter()
.map(str::to_string)
.chain(customs.iter().cloned())
{
let _ = app.run("window.workspace", json!({"name": w}));
if ui
.selectable_label(w == current, crate::rtl::widget(ui, crate::i18n::workspace_name(&app.ui.language, &w)))
.clicked()
{
let _ = app.run("window.workspace", json!({"name": w}));
ui.close();
}
}
ui.separator();
if ui
.button(crate::rtl::widget(
ui,
format!(
"{} {}",
crate::i18n::tr(&app.ui.language, "Reset"),
crate::i18n::workspace_name(&app.ui.language, &current)
),
))
.clicked()
{
let _ = app.run("window.resetWorkspace", json!({}));
ui.close();
}
}
ui.separator();
if ui
.button(crate::rtl::widget(
ui,
format!("{} {}", crate::i18n::tr(&app.ui.language, "Reset"), crate::i18n::workspace_name(&app.ui.language, &current)),
))
.clicked()
{
let _ = app.run("window.resetWorkspace", json!({}));
ui.close();
}
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Workspace…"))).clicked() {
let _ = app.run("window.newWorkspace", json!({}));
ui.close();
}
if !customs.is_empty() {
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Workspace")), |ui| {
for w in &customs {
if ui.button(w).clicked() {
let _ = app.run("window.deleteWorkspace", json!({"name": w}));
ui.close();
if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "New Workspace…"))).clicked() {
let _ = app.run("window.newWorkspace", json!({}));
ui.close();
}
if !customs.is_empty() {
ui.menu_button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Delete Workspace")), |ui| {
for w in &customs {
if ui.button(w).clicked() {
let _ = app.run("window.deleteWorkspace", json!({"name": w}));
ui.close();
}
}
}
});
});
}
});
ui.add_space(6.0);
if icons::button(ui, "share", 22.0, false, crate::i18n::tr(&app.ui.language, "Share")).clicked() {
app.status("Export a PDF, IDML or package to share — no cloud account needed.");
}
ui.add_space(8.0);
// Always one click away: the ArtCraft community Discord.
if crate::about::discord_button(ui, "Discord", vec2(78.0, 22.0)) {
let _ = app.run("help.discord", json!({}));
}
tools_start = ui.min_rect().min.x;
ui.min_rect().width()
});
ui.add_space(6.0);
if icons::button(ui, "share", 22.0, false, crate::i18n::tr(&app.ui.language, "Share")).clicked() {
app.status("Export a PDF, IDML or package to share — no cloud account needed.");
if (tools.inner - tools_w).abs() > 0.5 {
ui.data_mut(|d| d.insert_temp(tools_w_id, tools.inner));
ui.ctx().request_discard("app bar: the right-hand controls changed width");
}
ui.add_space(8.0);
// Always one click away: the ArtCraft community Discord.
if crate::about::discord_button(ui, "Discord", vec2(78.0, 22.0)) {
let _ = app.run("help.discord", json!({}));
}
tools_start = ui.min_rect().min.x;
});
});
// Centred title.
@ -142,14 +161,18 @@ pub fn control_bar(app: &mut DesignApp, ui: &mut egui::Ui) {
egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin { left: 8, right: 8, top: 5, bottom: 3 }).stroke(Stroke::new(1.0, t.divider)),
)
.show(ui, |ui| {
ui.horizontal(|ui| {
let text_mode =
matches!(app.session.tool_id(), "type" | "verticalType") || app.session.active().is_some_and(|d| d.selection.text.is_some());
if text_mode {
control_text(app, ui);
} else {
control_object(app, ui);
}
// Wider than the window: the bar scrolls sideways.
crate::widgets::overflow_scrolling(ui);
egui::ScrollArea::horizontal().id_salt("control_bar_scroll").auto_shrink([false, true]).show(ui, |ui| {
ui.horizontal(|ui| {
let text_mode =
matches!(app.session.tool_id(), "type" | "verticalType") || app.session.active().is_some_and(|d| d.selection.text.is_some());
if text_mode {
control_text(app, ui);
} else {
control_object(app, ui);
}
});
});
});
}
@ -716,3 +739,86 @@ fn community_card(app: &mut DesignApp, ui: &mut egui::Ui) {
});
});
}
#[cfg(test)]
mod tests {
use egui::{pos2, vec2};
use egui_kittest::kittest::Queryable;
use crate::test_window::{self, wheel};
/// The Control panel's last control with nothing selected.
const LAST_CONTROL: &str = "Jump to Next Column";
const FIRST_CONTROL: &str = "Rotate 90° Counterclockwise";
fn app() -> crate::DesignApp {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.ui.control_bar = true;
app
}
fn rect(h: &egui_kittest::Harness<'static, test_window::Window>, label: &str) -> egui::Rect {
h.get_by_label(label).rect()
}
#[test]
fn a_narrow_window_scrolls_the_control_bar() {
let w = 700.0;
let mut h = test_window::open(app(), vec2(w, 600.0));
let bar = test_window::panel_rect(&h, "control_bar");
assert!(rect(&h, LAST_CONTROL).max.x > w, "the controls are wider than the window");
// A plain mouse wheel scrolls the bar sideways.
let first = rect(&h, FIRST_CONTROL).center();
wheel(&mut h, first, vec2(0.0, -3000.0));
let last = rect(&h, LAST_CONTROL);
let window = egui::Rect::from_min_size(pos2(0.0, 0.0), vec2(w, 600.0));
assert!(window.contains_rect(last) && bar.contains_rect(last), "{last:?} outside the bar {bar:?}");
assert_eq!(test_window::panel_rect(&h, "control_bar").height(), bar.height(), "the bar keeps its height");
// A popup opened from a scrolled control opens at that control.
let fill = h.get_by_label("Fill").rect();
let chip = pos2(fill.max.x + 15.0, fill.center().y);
h.hover_at(chip);
h.drag_at(chip);
h.drop_at(chip);
h.run_steps(4);
let item = rect(&h, "[Black]");
assert!(item.min.y > fill.max.y && (item.min.x - chip.x).abs() < 120.0, "popup item at {item:?}, control at {chip:?}");
}
#[test]
fn a_narrow_window_scrolls_the_app_bar() {
let w = 640.0;
let mut h = test_window::open(app(), vec2(w, 600.0));
let (help, share) = (rect(&h, "Help"), rect(&h, "Share"));
assert!(help.max.x <= share.min.x, "the menus ({help:?}) and the workspace controls ({share:?}) overlap");
wheel(&mut h, help.center(), vec2(0.0, -3000.0));
let share = rect(&h, "Share");
let window = egui::Rect::from_min_size(pos2(0.0, 0.0), vec2(w, 600.0));
assert!(window.contains_rect(share), "{share:?} outside the window");
}
#[test]
fn a_wide_window_does_not_scroll_the_bars() {
let mut h = test_window::open(app(), vec2(1400.0, 900.0));
let before = [rect(&h, FIRST_CONTROL), rect(&h, LAST_CONTROL), rect(&h, "Help"), rect(&h, "Share")];
assert!(before.iter().all(|r| r.max.x <= 1400.0), "{before:?}");
wheel(&mut h, before[0].center(), vec2(0.0, -3000.0));
wheel(&mut h, before[2].center(), vec2(0.0, -3000.0));
let after = [rect(&h, FIRST_CONTROL), rect(&h, LAST_CONTROL), rect(&h, "Help"), rect(&h, "Share")];
assert_eq!(after, before);
}
#[test]
fn a_scrolled_bar_keeps_its_captions_on_one_line() {
let mut app = app();
app.run("file.new", serde_json::json!({})).unwrap();
let f = app.run("frame.create", serde_json::json!({"rect": [72, 72, 300, 200], "content": "text", "text": "Hello"})).unwrap();
app.run("selection.set", serde_json::json!({"ids": [f["id"]]})).unwrap();
let mut h = test_window::open(app, vec2(700.0, 600.0));
let bar = test_window::panel_rect(&h, "control_bar").center();
wheel(&mut h, bar, vec2(0.0, -3000.0));
// The text frame's last group: its caption gets only what is left of the row.
let columns = h.get_by_label("Columns").rect();
assert!(columns.height() < 20.0 && columns.width() > columns.height(), "Columns wraps: {columns:?}");
}
}

View File

@ -805,6 +805,8 @@ pub fn button(ui: &mut egui::Ui, name: &str, size: f32, selected: bool, tip: &st
}
let pad = size * 0.16;
paint(ui.painter(), r.shrink(pad), name, if selected { t.text_strong } else { t.icon });
let enabled = ui.is_enabled();
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, enabled, selected, tip));
if tip.is_empty() {
resp
} else {

View File

@ -984,3 +984,54 @@ mod tests {
assert!(app.ui.dialog.is_none(), "a control-channel app.open doesn't raise an alert");
}
}
/// The whole window in a headless test harness, at a chosen window size.
#[cfg(test)]
pub(crate) mod test_window {
use egui_kittest::Harness;
pub struct Window {
pub app: crate::DesignApp,
ready: bool,
}
/// The window at `size` with a new document open.
pub fn open(mut app: crate::DesignApp, size: egui::Vec2) -> Harness<'static, Window> {
if app.session.active().is_none() {
app.run("file.new", serde_json::json!({})).unwrap();
}
let mut h = Harness::builder().with_size(size).with_max_steps(1000).build_ui_state(
|ui, w: &mut Window| {
// The builder runs a frame before the texture size can be raised: skip it.
if !w.ready {
return;
}
let ctx = ui.ctx().clone();
w.app.logic(&ctx);
w.app.ui(ui);
},
Window { app, ready: false },
);
h.input_mut().max_texture_side = Some(8192);
h.state_mut().ready = true;
h.run_steps(6);
h
}
/// The mouse wheel turned over `pos`: positive `delta` moves the content right and down.
pub fn wheel(h: &mut Harness<'static, Window>, pos: egui::Pos2, delta: egui::Vec2) {
h.hover_at(pos);
h.event(egui::Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta,
phase: egui::TouchPhase::Move,
modifiers: egui::Modifiers::NONE,
});
h.run_steps(10);
}
/// The outer rect of a side or top panel, from egui's memory.
pub fn panel_rect(h: &Harness<'static, Window>, id: &str) -> egui::Rect {
egui::containers::panel::PanelState::load(&h.ctx, egui::Id::new(id)).map(|s| s.outer_rect).unwrap()
}
}

View File

@ -12,27 +12,33 @@ const BTN: f32 = 24.0;
pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
let t = Tokens::get(ui.ctx());
let cols = if app.ui.tools_double_column { 2 } else { 1 };
let frame = egui::Frame::NONE.fill(t.panel).stroke(Stroke::new(1.0, t.divider));
// A single column that doesn't fit the window's height becomes two, without changing the
// preference; whatever two columns still can't fit scrolls.
let single_h_id = egui::Id::new("tools_single_column_height");
let single_h: Option<f32> = ui.data(|d| d.get_temp(single_h_id));
let viewport_h = ui.available_height() - frame.total_margin().sum().y;
let crowded = !app.ui.tools_double_column && single_h.is_some_and(|h| h > viewport_h);
let cols = if app.ui.tools_double_column || crowded { 2 } else { 1 };
let width = 8.0 + BTN * cols as f32 + 4.0 * (cols as f32 - 1.0) + 8.0;
let r = egui::Panel::left("tools")
.exact_size(width)
.resizable(false)
.frame(egui::Frame::NONE.fill(t.panel).stroke(Stroke::new(1.0, t.divider)))
.show(ui, |ui| {
let r = egui::Panel::left("tools").exact_size(width).resizable(false).frame(frame).show(ui, |ui| {
crate::widgets::overflow_scrolling(ui);
let scroll = egui::ScrollArea::vertical().id_salt("tools_scroll").auto_shrink([false, false]).show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 1.0;
ui.add_space(3.0);
// Collapse chevrons.
// Collapse chevrons (nothing to toggle while the window forces two columns).
ui.horizontal(|ui| {
ui.add_space(width / 2.0 - 8.0);
if icons::button(
ui,
if cols == 1 { "double-chevron-right" } else { "double-chevron-left" },
16.0,
false,
crate::i18n::tr(&app.ui.language, "Toggle double column"),
)
.clicked()
{
let toggle = ui.add_enabled_ui(!crowded, |ui| {
icons::button(
ui,
if cols == 1 { "double-chevron-right" } else { "double-chevron-left" },
16.0,
false,
crate::i18n::tr(&app.ui.language, "Toggle double column"),
)
});
if toggle.inner.clicked() {
app.ui.tools_double_column = !app.ui.tools_double_column;
}
});
@ -169,6 +175,15 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
ui.add_space(8.0);
fill_stroke_proxy(app, ui, width);
});
if cols == 1 {
let h = scroll.content_size.y;
ui.data_mut(|d| d.insert_temp(single_h_id, h));
// Laid out in one column that turned out too tall: lay out again in two.
if h > viewport_h {
ui.ctx().request_discard("tools panel: one column does not fit");
}
}
});
if std::env::var_os("DESIGNCRAFT_DEBUG_LAYOUT").is_some() {
eprintln!("tools panel rect {:?} (wanted width {width}); remaining {:?}", r.response.rect, ui.available_rect_before_wrap());
}
@ -252,9 +267,11 @@ fn fill_stroke_proxy(app: &mut DesignApp, ui: &mut egui::Ui, width: f32) {
paint_stroke(p);
paint_fill(p);
}
let fill_tip = crate::i18n::tr(&app.ui.language, "Fill (X)");
let fresp = ui.interact(fill_r, ui.id().with("proxy_fill"), Sense::click()).on_hover_ui(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Fill (X)"));
crate::rtl::label(ui, fill_tip);
});
fresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, fill_tip));
let sresp = ui.interact(stroke_r.translate(vec2(0.0, 0.0)), ui.id().with("proxy_stroke"), Sense::click()).on_hover_ui(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Stroke (X)"));
});
@ -387,7 +404,82 @@ fn tool_button(ui: &mut egui::Ui, icon: &str, active: bool, tip: &str) -> egui::
ui.painter().rect_filled(egui::Rect::from_center_size(r.center(), egui::vec2(28.0, 20.0)), 2.0, t.hover);
}
icons::paint(ui.painter(), egui::Rect::from_center_size(r.center(), egui::vec2(17.0, 17.0)), icon, if active { t.text_strong } else { t.icon });
let enabled = ui.is_enabled();
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled, tip));
resp.on_hover_ui(|ui| {
crate::rtl::label(ui, tip);
})
}
#[cfg(test)]
mod tests {
use egui::{pos2, vec2};
use egui_kittest::kittest::Queryable;
use crate::test_window::{self, wheel};
/// The last tool group's button and the Fill proxy, near the bottom of the panel.
const LAST_TOOL: &str = "Zoom Tool (Z)";
const FILL: &str = "Fill (X)";
/// Status bar height: the Tools panel ends above it.
const STATUS_H: f32 = 17.0;
fn app(double: bool) -> crate::DesignApp {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.ui.tools_double_column = double;
app
}
fn column_width(cols: f32) -> f32 {
8.0 + super::BTN * cols + 4.0 * (cols - 1.0) + 8.0
}
fn rect(h: &egui_kittest::Harness<'static, test_window::Window>, label: &str) -> egui::Rect {
h.get_by_label(label).rect()
}
#[test]
fn a_short_window_reaches_every_tool() {
for double in [false, true] {
let (w, ht) = (1000.0, 500.0);
let mut h = test_window::open(app(double), vec2(w, ht));
let panel = test_window::panel_rect(&h, "tools");
wheel(&mut h, panel.center(), vec2(0.0, -2000.0));
let visible = egui::Rect::from_min_max(pos2(0.0, panel.min.y), pos2(w, ht - STATUS_H));
for label in [LAST_TOOL, FILL] {
let r = rect(&h, label);
assert!(visible.contains_rect(r), "double column {double}: {label} at {r:?} is outside {visible:?}");
}
assert_eq!(h.state().app.ui.tools_double_column, double, "the preference is the user's");
}
}
#[test]
fn tools_take_two_columns_while_one_does_not_fit() {
let (w, ht) = (1086.0, 612.0);
let mut h = test_window::open(app(false), vec2(w, ht));
assert_eq!(test_window::panel_rect(&h, "tools").width(), column_width(2.0));
let visible = egui::Rect::from_min_max(pos2(0.0, 0.0), pos2(w, ht - STATUS_H));
for label in [LAST_TOOL, FILL] {
let r = rect(&h, label);
assert!(visible.contains_rect(r), "{label} at {r:?} is outside {visible:?} without scrolling");
}
assert!(!h.state().app.ui.tools_double_column);
// A taller window brings the single column back.
h.set_size(vec2(1400.0, 900.0));
h.run_steps(4);
assert_eq!(test_window::panel_rect(&h, "tools").width(), column_width(1.0));
assert!(!h.state().app.ui.tools_double_column);
}
#[test]
fn a_tall_window_keeps_one_column_and_does_not_scroll() {
let mut h = test_window::open(app(false), vec2(1400.0, 900.0));
let panel = test_window::panel_rect(&h, "tools");
assert_eq!(panel.width(), column_width(1.0));
let before = (rect(&h, LAST_TOOL), rect(&h, FILL));
assert!(panel.contains_rect(before.0) && panel.contains_rect(before.1), "{before:?} outside {panel:?}");
wheel(&mut h, panel.center(), vec2(0.0, -2000.0));
assert_eq!((rect(&h, LAST_TOOL), rect(&h, FILL)), before);
}
}

View File

@ -572,6 +572,21 @@ pub fn segmented(ui: &mut Ui, labels: &[&str], active: usize, width: f32) -> Opt
out
}
/// Scrolling for a bar that a small window cuts off (Tools panel, Control panel, application
/// bar): the mouse wheel scrolls the bar's only direction, and a thin scroll bar floats over the
/// contents, so nothing moves and nothing shows while everything fits.
pub fn overflow_scrolling(ui: &mut Ui) {
let style = ui.style_mut();
style.always_scroll_the_only_direction = true;
// A caption at the end of a scrolled row gets only what is left of it: extend, don't wrap.
style.wrap_mode = Some(egui::TextWrapMode::Extend);
let s = &mut style.spacing.scroll;
*s = egui::style::ScrollStyle::floating();
s.bar_width = 6.0;
// A faint handle while the pointer is elsewhere: the bar shows there is more.
s.dormant_handle_opacity = 0.5;
}
#[cfg(test)]
mod tests {
use super::*;