Keep the profile dropdown and its submenus inside the window (#2059)

This commit is contained in:
MarekSpodBiedry
2026-10-17 00:27:11 +02:00
parent f34c7a8a89
commit 86147e1eeb
3 changed files with 82 additions and 11 deletions
+2
View File
@@ -140,6 +140,8 @@ mod tests_preview_limit;
#[cfg(test)]
mod tests_print;
#[cfg(test)]
mod tests_profile_menu;
#[cfg(test)]
mod tests_profiles_layout;
#[cfg(test)]
mod tests_quit_unsaved;
+28 -11
View File
@@ -169,7 +169,8 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
// content, and the sliders below would run past its edge
let max = ui.available_width() - 28.0 - ui.spacing().item_spacing.x;
let r = crate::widgets::dropdown_within(ui, "profile", crate::i18n::profile_label(&d.profile.id, name), t.font(15.0), t.text_label, max);
egui::Popup::menu(&r).show(|ui| profile_menu(app, ui, &d));
let below = r.rect.bottom();
egui::Popup::menu(&r).show(|ui| profile_menu(app, ui, &d, below));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if crate::widgets::icon_button(
ui,
@@ -429,8 +430,14 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
}
/// The profile dropdown: Favorites, Recent, one submenu per group, then favourite toggle and
/// Browse….
fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings) {
/// Browse…. Like the menu bar's menus, the dropdown and each submenu stay inside the window and
/// scroll when their rows don't fit (issue #2059: a folder of many imported profiles ran past the
/// window's bottom edge). `below` is the dropdown's bottom edge, which the menu hangs from.
fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, below: f32) {
crate::menu_level::level(ui, 1, Some(below), |ui| profile_menu_rows(app, ui, d, below));
}
fn profile_menu_rows(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings, below: f32) {
use lightcraft_engine::presets::{PROFILES, profile_groups};
let t = Tokens::get(ui.ctx());
ui.set_min_width(200.0);
@@ -463,12 +470,17 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
}
for g in profile_groups() {
let r = crate::widgets::menu_button(ui, crate::i18n::tr(g), |ui| {
ui.set_min_width(170.0);
for p in PROFILES.iter().filter(|p| p.group == g) {
item(ui, "group", p.id, p.name, &mut pick);
}
crate::menu_level::level(ui, 2, Some(below), |ui| {
ui.set_min_width(170.0);
for p in PROFILES.iter().filter(|p| p.group == g) {
item(ui, "group", p.id, p.name, &mut pick);
}
});
});
register(ui.ctx(), format!("profileMenu:groupMenu:{g}"), r.response.rect);
if r.inner.is_some() {
crate::menu_level::set_anchor(ui.ctx(), 2, r.response.rect);
}
}
// imported LUT profile groups
let mut lut_groups: Vec<String> = app.session.lut_profiles.iter().map(|p| p.group.clone()).collect();
@@ -477,12 +489,17 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
for g in &lut_groups {
// a folder's name as it is, and its own widget id even when it matches a built-in group
let r = crate::widgets::menu_button(ui, g.as_str(), |ui| {
ui.set_min_width(170.0);
for p in app.session.lut_profiles.iter().filter(|p| &p.group == g) {
item(ui, "group", &p.id, &p.name, &mut pick);
}
crate::menu_level::level(ui, 2, Some(below), |ui| {
ui.set_min_width(170.0);
for p in app.session.lut_profiles.iter().filter(|p| &p.group == g) {
item(ui, "group", &p.id, &p.name, &mut pick);
}
});
});
register(ui.ctx(), format!("profileMenu:lutGroupMenu:{g}"), r.response.rect);
if r.inner.is_some() {
crate::menu_level::set_anchor(ui.ctx(), 2, r.response.rect);
}
}
ui.separator();
if let Some((name, _)) = app.session.profile_info(cur) {
+52
View File
@@ -0,0 +1,52 @@
//! #2059: the profile dropdown and its submenus stay inside the window. A folder of many imported
//! profiles used to make its submenu run past the window's bottom edge, and the profiles below it
//! could not be reached.
use std::time::Duration;
use serde_json::json;
use crate::headless::Headless;
use crate::{LightcraftApp, Services};
const T: Duration = Duration::from_secs(5);
const SETTLE: Duration = Duration::from_secs(10);
const FOLDER: &str = "Imported";
fn widget(h: &Headless, id: &str) -> Option<egui::Rect> {
h.app.widgets.iter().find(|(w, _)| w == id).map(|(_, r)| *r)
}
fn go(h: &mut Headless, method: &str, id: &str) {
let r = h.request(method, json!({"id": id}), T);
assert_eq!(r["ok"], true, "{method} {id}: {r}");
h.settle(SETTLE);
}
#[test]
fn a_folder_of_many_imported_profiles_scrolls_inside_the_window() {
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
for i in 0..80 {
app.session.lut_profiles.push(lightcraft_engine::cmd::lut_profiles::LutProfile {
id: format!("lut:imported-{i}"),
name: format!("Imported {i}"),
group: FOLDER.into(),
file: String::new(),
});
}
let mut h = Headless::new(app, [1200.0, 900.0], 1.0);
let r = h.request("ui.set", json!({"view": "detail"}), T);
assert_eq!(r["ok"], true, "{r}");
h.app.ui.right = crate::state::RightPanel::Edit;
h.settle(SETTLE);
go(&mut h, "ui.clickWidget", "dropdown:profile");
let menu = widget(&h, "menu-level:1").expect("the profile menu is open");
assert!(menu.bottom() <= h.size.y + 0.5, "the profile menu {menu:?} runs past the window's bottom {}", h.size.y);
go(&mut h, "ui.hoverWidget", &format!("profileMenu:lutGroupMenu:{FOLDER}"));
let sub = widget(&h, "menu-level:2").expect("the imported folder's submenu is open");
assert!(sub.bottom() <= h.size.y + 0.5, "the submenu {sub:?} runs past the window's bottom {}", h.size.y);
// Its 80 rows don't all fit: some are scrolled out of view rather than drawn off screen.
let rows: Vec<egui::Rect> = (0..80).filter_map(|i| widget(&h, &format!("profileMenu:group:lut:imported-{i}"))).collect();
assert!(!rows.is_empty(), "no imported profile rows drawn");
assert!(rows.iter().any(|r| !sub.contains_rect(*r)), "every row fits a 900 pt window");
}