mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-01 14:07:34 -06:00
Merge PR #2804: fix(ui): wrap the Edit header buttons instead of letting Reset cover HDR
crates/ui-egui/src/panels/edit.rs: main split it into panels/edit/; the header call site stays in edit.rs and header_buttons, HeaderClicks and the overlap test are ported into edit/sliders.rs next to reset_tip. The reset_tip doc comment sits directly above reset_tip in the port.
This commit is contained in:
@@ -31,7 +31,7 @@ pub(crate) use grading::{WheelGrab, WheelLock, WheelMods, wheel_pick};
|
||||
pub(crate) use histogram::histogram;
|
||||
use histogram::soft_proofing;
|
||||
use mixer::{mixer, point_color};
|
||||
use sliders::{REL_TEMP, REL_TINT, hdr_row, k_to_rel, profile_menu, quick_develop, rel_to_k, reset_tip, section};
|
||||
use sliders::{REL_TEMP, REL_TINT, hdr_row, header_buttons, k_to_rel, profile_menu, quick_develop, rel_to_k, reset_tip, section};
|
||||
pub use sliders::{apply_slider_out, sub_title};
|
||||
pub(crate) use sliders::{control, reload_preview_only};
|
||||
|
||||
@@ -65,32 +65,19 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
let (hr, _) = ui.allocate_exact_size(vec2(ui.available_width(), 40.0), Sense::hover());
|
||||
ui.painter().text(pos2(hr.left() + 24.0, hr.bottom() - 10.0), Align2::LEFT_CENTER, crate::i18n::tr("Edit"), t.semibold(15.0), t.text);
|
||||
egui::Frame::NONE.inner_margin(egui::Margin { left: 24, right: 22, top: 8, bottom: 14 }).show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 8.0;
|
||||
if text_button(ui, "auto", crate::i18n::tr("Auto"), false).clicked() {
|
||||
let _ = app.run("develop.auto", json!({}));
|
||||
app.toast(ui.ctx(), crate::i18n::tr("Auto settings applied"));
|
||||
}
|
||||
let bw = crate::is_bw(&d);
|
||||
if text_button(ui, "bw", crate::i18n::tr("B&W"), bw).clicked() {
|
||||
let _ = app.run("develop.treatment", json!({}));
|
||||
}
|
||||
// HDR editing, next to B&W as Lightroom has it
|
||||
if text_button(ui, "hdr", crate::i18n::tr("HDR"), d.hdr.enabled)
|
||||
.on_hover_text(crate::i18n::tr("Edit in HDR: highlights above SDR white, for HDR exports and displays"))
|
||||
.clicked()
|
||||
{
|
||||
let _ = app.run("develop.hdr", json!({"enabled": !d.hdr.enabled}));
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if text_button(ui, "reset", crate::i18n::tr("Reset"), false)
|
||||
.on_hover_text(reset_tip(&app.ui.settings.keymap, ui.ctx().os() == egui::os::OperatingSystem::Mac))
|
||||
.clicked()
|
||||
{
|
||||
reset_clicked = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
let tip = reset_tip(&app.ui.settings.keymap, ui.ctx().os() == egui::os::OperatingSystem::Mac);
|
||||
let clicks = header_buttons(ui, crate::is_bw(&d), d.hdr.enabled, &tip);
|
||||
if clicks.auto {
|
||||
let _ = app.run("develop.auto", json!({}));
|
||||
app.toast(ui.ctx(), crate::i18n::tr("Auto settings applied"));
|
||||
}
|
||||
if clicks.bw {
|
||||
let _ = app.run("develop.treatment", json!({}));
|
||||
}
|
||||
if clicks.hdr {
|
||||
let _ = app.run("develop.hdr", json!({"enabled": !d.hdr.enabled}));
|
||||
}
|
||||
reset_clicked = clicks.reset;
|
||||
});
|
||||
if let Some(why) = &preview_only {
|
||||
let reload = egui::Frame::NONE
|
||||
|
||||
@@ -296,6 +296,75 @@ pub(crate) fn reload_preview_only(app: &mut LightcraftApp, ctx: &egui::Context,
|
||||
}
|
||||
}
|
||||
|
||||
/// Which of the Edit header's buttons were clicked.
|
||||
#[derive(Default)]
|
||||
pub(super) struct HeaderClicks {
|
||||
pub(super) auto: bool,
|
||||
pub(super) bw: bool,
|
||||
pub(super) hdr: bool,
|
||||
pub(super) reset: bool,
|
||||
}
|
||||
|
||||
/// The Edit header's buttons: Auto, B&W and HDR on the left, Reset on the right. A button that doesn't fit on the
|
||||
/// row starts the next one (long translations in a narrow panel, such as German *Zurücksetzen* beside HDR), so none
|
||||
/// covers another; Reset stays right-aligned on whichever row it lands.
|
||||
pub(super) fn header_buttons(ui: &mut egui::Ui, bw: bool, hdr: bool, reset_tip: &str) -> HeaderClicks {
|
||||
const GAP: f32 = 8.0;
|
||||
let mut clicks = HeaderClicks::default();
|
||||
let avail = ui.available_width();
|
||||
let left = [("auto", "Auto", false), ("bw", "B&W", bw), ("hdr", "HDR", hdr)];
|
||||
// rows of `left` indices, filled greedily; `used` is the width taken on the current (last) row
|
||||
let mut rows: Vec<Vec<usize>> = Vec::new();
|
||||
let mut row: Vec<usize> = Vec::new();
|
||||
let mut used = 0.0;
|
||||
for (i, (_, label, _)) in left.iter().enumerate() {
|
||||
let w = crate::widgets::text_button_size(ui, label);
|
||||
if row.is_empty() {
|
||||
used = w;
|
||||
} else if used + GAP + w > avail {
|
||||
rows.push(std::mem::take(&mut row));
|
||||
used = w;
|
||||
} else {
|
||||
used += GAP + w;
|
||||
}
|
||||
row.push(i);
|
||||
}
|
||||
rows.push(row);
|
||||
let reset_on_last = used + GAP + crate::widgets::text_button_size(ui, "Reset") <= avail;
|
||||
let reset = |ui: &mut egui::Ui, clicks: &mut HeaderClicks| {
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
clicks.reset = crate::widgets::text_button(ui, "reset", crate::i18n::tr("Reset"), false).on_hover_text(reset_tip).clicked();
|
||||
});
|
||||
};
|
||||
let last = rows.len() - 1;
|
||||
for (r, row) in rows.iter().enumerate() {
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = GAP;
|
||||
for &i in row {
|
||||
let (id, label, active) = left[i];
|
||||
let mut resp = crate::widgets::text_button(ui, id, crate::i18n::tr(label), active);
|
||||
if id == "hdr" {
|
||||
// HDR editing, next to B&W as Lightroom has it
|
||||
resp = resp.on_hover_text(crate::i18n::tr("Edit in HDR: highlights above SDR white, for HDR exports and displays"));
|
||||
}
|
||||
let clicked = resp.clicked();
|
||||
match id {
|
||||
"auto" => clicks.auto = clicked,
|
||||
"bw" => clicks.bw = clicked,
|
||||
_ => clicks.hdr = clicked,
|
||||
}
|
||||
}
|
||||
if r == last && reset_on_last {
|
||||
reset(ui, &mut clicks);
|
||||
}
|
||||
});
|
||||
}
|
||||
if !reset_on_last {
|
||||
ui.horizontal(|ui| reset(ui, &mut clicks));
|
||||
}
|
||||
clicks
|
||||
}
|
||||
|
||||
/// The Reset button's tooltip with the reset shortcut as this platform writes it (issue #1751).
|
||||
pub(super) fn reset_tip(keymap: &crate::shortcuts::Keymap, mac: bool) -> String {
|
||||
let base = crate::i18n::tr("Reset all edits");
|
||||
@@ -304,3 +373,54 @@ pub(super) fn reset_tip(keymap: &crate::shortcuts::Keymap, mac: bool) -> String
|
||||
None => base.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The Edit header's buttons never cover each other or run past the panel, in any language: a button that doesn't
|
||||
/// fit starts the next row, and Reset stays right-aligned. Before, Reset covered HDR in 11 of the 18 languages at
|
||||
/// the default 222 pt (German *Zurücksetzen* among them) and in all but the two Chinese ones at some width. 200 pt
|
||||
/// is about the narrowest panel (`RIGHT_WIDTH.min` less the margins).
|
||||
#[test]
|
||||
fn edit_header_buttons_do_not_overlap_in_any_language() {
|
||||
use crate::i18n::Locale;
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
crate::theme::apply(&ctx);
|
||||
let mut wrapped = Vec::new();
|
||||
for &locale in Locale::ALL {
|
||||
crate::i18n::set_language(locale);
|
||||
for width in [200.0, 222.0, 260.0, 400.0] {
|
||||
let mut rects = Vec::new();
|
||||
for frame in 0..2 {
|
||||
let raw = crate::headless::HeadlessView::raw_input(vec2(width, 200.0), 1.0, frame as f64 / 60.0, vec![]);
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| {
|
||||
header_buttons(ui, false, false, "");
|
||||
});
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
rects = crate::widgets::take_registry(&ctx);
|
||||
}
|
||||
let get = |id: &str| rects.iter().find(|(i, _)| i == id).map(|(_, r)| *r).unwrap();
|
||||
let buttons = ["button:auto", "button:bw", "button:hdr", "button:reset"].map(get);
|
||||
for (i, a) in buttons.iter().enumerate() {
|
||||
assert!(a.right() <= width + 0.5, "{locale:?} {width}: button {i} past the edge: {a:?}");
|
||||
for b in &buttons[i + 1..] {
|
||||
assert!(!a.intersects(b.shrink(0.5)), "{locale:?} {width}: {a:?} overlaps {b:?}");
|
||||
}
|
||||
}
|
||||
let [_, _, hdr, reset] = buttons;
|
||||
assert!((reset.right() - width).abs() < 0.5, "{locale:?} {width}: Reset stays right-aligned: {reset:?}");
|
||||
if reset.top() > hdr.top() {
|
||||
wrapped.push((locale, width));
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::i18n::set_language(Locale::En);
|
||||
assert!(wrapped.contains(&(Locale::De, 222.0)), "German wraps in the default panel: {wrapped:?}");
|
||||
// English keeps its single row from the default width up
|
||||
assert!(!wrapped.iter().any(|&(l, w)| l == Locale::En && w >= 222.0), "{wrapped:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -892,7 +892,7 @@ pub fn text_button(ui: &mut Ui, id: &str, label: &str, active: bool) -> Response
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let font = t.semibold(11.5);
|
||||
let galley = ui.painter().layout_no_wrap(label.to_string(), font, t.text);
|
||||
let size = vec2((galley.size().x + 18.0).max(37.0), 24.0);
|
||||
let size = vec2(text_button_width(galley.size().x), 24.0);
|
||||
let (r, resp) = ui.allocate_exact_size(size, Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, active, label));
|
||||
register(ui, format!("button:{id}"), r);
|
||||
@@ -909,6 +909,18 @@ pub fn text_button(ui: &mut Ui, id: &str, label: &str, active: bool) -> Response
|
||||
resp
|
||||
}
|
||||
|
||||
/// The width of a [`text_button`] whose label is `text_width` wide.
|
||||
fn text_button_width(text_width: f32) -> f32 {
|
||||
(text_width + 18.0).max(37.0)
|
||||
}
|
||||
|
||||
/// The width [`text_button`] takes for `label` (translated as it would be), to plan a row before drawing it.
|
||||
pub fn text_button_size(ui: &Ui, label: &str) -> f32 {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let galley = ui.painter().layout_no_wrap(crate::i18n::tr(label).to_string(), t.semibold(11.5), t.text);
|
||||
text_button_width(galley.size().x)
|
||||
}
|
||||
|
||||
/// A segmented control: `labels` share the available width equally, `per_row` per row. Each segment is
|
||||
/// addressable as `button:{id}-{key}` (keys parallel to labels). Returns the clicked index.
|
||||
pub fn segmented(ui: &mut Ui, id: &str, items: &[(&str, &str)], active: Option<usize>, per_row: usize) -> Option<usize> {
|
||||
|
||||
Reference in New Issue
Block a user