mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 18:18:12 -06:00
Merge PR #806
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
//! `albumPicker:<id>` (the button), `albumPickerSearch:<id>`, `albumPickerFolder:<folder>:<id>`
|
||||
//! and `albumPickerItem:<album>:<id>`.
|
||||
|
||||
use egui::{Response, RichText, Ui, vec2};
|
||||
use egui::{Rect, Response, RichText, Ui, vec2};
|
||||
|
||||
use crate::icons::Icon;
|
||||
use crate::widgets::register;
|
||||
@@ -146,14 +146,14 @@ pub fn search(entries: &[AlbumEntry], query: &str) -> Vec<usize> {
|
||||
first.into_iter().chain(rest).collect()
|
||||
}
|
||||
|
||||
/// A small filled triangle centred on `c`: pointing down (`down`), else right.
|
||||
fn triangle(p: &egui::Painter, c: egui::Pos2, down: bool, color: egui::Color32) {
|
||||
let pts = if down {
|
||||
vec![c + vec2(-4.0, -2.0), c + vec2(4.0, -2.0), c + vec2(0.0, 3.0)]
|
||||
} else {
|
||||
vec![c + vec2(-2.0, -4.0), c + vec2(3.0, 0.0), c + vec2(-2.0, 4.0)]
|
||||
};
|
||||
p.add(egui::Shape::convex_polygon(pts, color, egui::Stroke::NONE));
|
||||
/// A disclosure chevron centred on `c`: pointing down (`down`), else right.
|
||||
fn disclosure(p: &egui::Painter, c: egui::Pos2, down: bool, color: egui::Color32) {
|
||||
crate::icons::paint(
|
||||
p,
|
||||
Rect::from_center_size(c, vec2(16.0, 16.0)),
|
||||
if down { crate::icons::Icon::ChevronDown } else { crate::icons::Icon::ChevronRight },
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
/// What the open popup remembers between frames.
|
||||
@@ -195,7 +195,7 @@ impl<'a> AlbumPicker<'a> {
|
||||
// the name, and a drop-down arrow painted at the right (the UI font has no arrow glyph)
|
||||
let mut button = ui.add_sized(vec2(width, ui.spacing().interact_size.y), egui::Button::new(text).truncate().right_text(" "));
|
||||
let arrow_color = crate::theme::Tokens::get(ui.ctx()).icon;
|
||||
triangle(ui.painter(), egui::pos2(button.rect.right() - 10.0, button.rect.center().y), true, arrow_color);
|
||||
disclosure(ui.painter(), egui::pos2(button.rect.right() - 10.0, button.rect.center().y), true, arrow_color);
|
||||
register(ui.ctx(), format!("albumPicker:{id}"), button.rect);
|
||||
let key = egui::Id::new(("albumPicker", id.as_str()));
|
||||
if button.clicked() {
|
||||
@@ -255,7 +255,7 @@ impl<'a> AlbumPicker<'a> {
|
||||
ui.painter().rect_filled(rect, 3.0, t.hover);
|
||||
}
|
||||
let x = rect.left() + indent;
|
||||
triangle(ui.painter(), egui::pos2(x + 7.0, rect.center().y), open, if r.hovered() { t.text } else { t.text_dim });
|
||||
disclosure(ui.painter(), egui::pos2(x + 7.0, rect.center().y), open, if r.hovered() { t.text } else { t.text_dim });
|
||||
let icon = egui::Rect::from_center_size(egui::pos2(x + 23.0, rect.center().y), vec2(14.0, 14.0));
|
||||
crate::icons::paint(ui.painter(), icon, Icon::Folder, t.icon);
|
||||
ui.painter().text(
|
||||
|
||||
@@ -240,7 +240,7 @@ pub fn contributors_ui(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(crate::i18n::tr("Sort"));
|
||||
egui::ComboBox::from_id_salt("credits_sort").selected_text(crate::i18n::tr(v.key.label().0)).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box("credits_sort").selected_text(crate::i18n::tr(v.key.label().0)).show_ui(ui, |ui| {
|
||||
for k in SortKey::ALL {
|
||||
if ui.selectable_label(v.key == k, crate::i18n::tr(k.label().0)).clicked() {
|
||||
v.key = k;
|
||||
@@ -248,7 +248,10 @@ pub fn contributors_ui(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
});
|
||||
if ui.button(if v.ascending { "▲" } else { "▼" }).on_hover_text(crate::i18n::tr("Reverse the order")).clicked() {
|
||||
let reverse =
|
||||
crate::widgets::arrow_button(ui, "", if v.ascending { crate::icons::Icon::ChevronUp } else { crate::icons::Icon::ChevronDown }, false);
|
||||
reverse.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), crate::i18n::tr("Reverse the order")));
|
||||
if reverse.on_hover_text(crate::i18n::tr("Reverse the order")).clicked() {
|
||||
v.ascending = !v.ascending;
|
||||
}
|
||||
ui.separator();
|
||||
@@ -289,12 +292,18 @@ pub fn contributors_ui(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
fn table(app: &mut LightcraftApp, ui: &mut egui::Ui, list: &[&Contributor], v: &mut View) {
|
||||
egui::Grid::new("credits_table").striped(true).num_columns(SortKey::ALL.len()).show(ui, |ui| {
|
||||
for k in SortKey::ALL {
|
||||
let arrow = if v.key == k { if v.ascending { " ▲" } else { " ▼" } } else { "" };
|
||||
if ui
|
||||
.button(RichText::new(format!("{}{arrow}", crate::i18n::tr(k.label().1))).strong())
|
||||
.on_hover_text(crate::i18n::tr(k.label().0))
|
||||
.clicked()
|
||||
{
|
||||
let text = RichText::new(crate::i18n::tr(k.label().1)).strong();
|
||||
let header = if v.key == k {
|
||||
crate::widgets::arrow_button(
|
||||
ui,
|
||||
text,
|
||||
if v.ascending { crate::icons::Icon::ChevronUp } else { crate::icons::Icon::ChevronDown },
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
ui.button(text)
|
||||
};
|
||||
if header.on_hover_text(crate::i18n::tr(k.label().0)).clicked() {
|
||||
if v.key == k {
|
||||
v.ascending = !v.ascending;
|
||||
} else {
|
||||
|
||||
@@ -266,7 +266,8 @@ impl<'a> DatePicker<'a> {
|
||||
ui.separator();
|
||||
}
|
||||
ui.horizontal(|ui| {
|
||||
let prev = ui.small_button("‹");
|
||||
let prev = crate::widgets::arrow_button(ui, "", Icon::ChevronLeft, true);
|
||||
prev.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), crate::i18n::tr("Previous page")));
|
||||
register(ui.ctx(), format!("datePickerPrev:{id}"), prev.rect);
|
||||
let title = match view.precision {
|
||||
Precision::Day => crate::i18n::date_group_label(&format!("{:04}-{:02}", view.year, view.month), false),
|
||||
@@ -278,7 +279,8 @@ impl<'a> DatePicker<'a> {
|
||||
};
|
||||
ui.label(RichText::new(title).strong());
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let next = ui.small_button("›");
|
||||
let next = crate::widgets::arrow_button(ui, "", Icon::ChevronRight, false);
|
||||
next.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), crate::i18n::tr("Next page")));
|
||||
register(ui.ctx(), format!("datePickerNext:{id}"), next.rect);
|
||||
if next.clicked() {
|
||||
view = step(view, true);
|
||||
|
||||
+30
-12
@@ -44,7 +44,9 @@ pub enum Icon {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ChevronLeft,
|
||||
TriangleLeft,
|
||||
ChevronUp,
|
||||
ChevronDoubleLeft,
|
||||
ChevronDoubleRight,
|
||||
Plus,
|
||||
Minus,
|
||||
Album,
|
||||
@@ -179,6 +181,22 @@ pub fn paint_turned(p: &Painter, rect: Rect, icon: Icon, color: Color32, radians
|
||||
|
||||
fn paint_with(p: &Painter, rect: Rect, icon: Icon, color: Color32, turn: Option<(f32, f32)>) {
|
||||
let side = rect.width().min(rect.height());
|
||||
// All disclosure and navigation chevrons share a 16-point size and stroke.
|
||||
let side = if matches!(
|
||||
icon,
|
||||
Icon::Back
|
||||
| Icon::Forward
|
||||
| Icon::ChevronDown
|
||||
| Icon::ChevronUp
|
||||
| Icon::ChevronLeft
|
||||
| Icon::ChevronRight
|
||||
| Icon::ChevronDoubleLeft
|
||||
| Icon::ChevronDoubleRight
|
||||
) {
|
||||
side.min(16.0)
|
||||
} else {
|
||||
side
|
||||
};
|
||||
let r = Rect::from_center_size(rect.center(), vec2(side, side));
|
||||
let k = side / 20.0;
|
||||
let pen = Pen { p, r, s: Stroke::new((1.5 * k).max(1.0), color), k, turn };
|
||||
@@ -188,14 +206,6 @@ fn paint_with(p: &Painter, rect: Rect, icon: Icon, color: Color32, turn: Option<
|
||||
pen.rect(3.0, 4.0, 17.0, 16.0, 1.5);
|
||||
pen.line(&[(8.0, 4.0), (8.0, 16.0)]);
|
||||
}
|
||||
Back => {
|
||||
pen.line(&[(16.0, 10.0), (4.0, 10.0)]);
|
||||
pen.line(&[(9.0, 5.0), (4.0, 10.0), (9.0, 15.0)]);
|
||||
}
|
||||
Forward => {
|
||||
pen.line(&[(4.0, 10.0), (16.0, 10.0)]);
|
||||
pen.line(&[(11.0, 5.0), (16.0, 10.0), (11.0, 15.0)]);
|
||||
}
|
||||
Monitor => {
|
||||
pen.rect(2.5, 3.5, 17.5, 13.5, 1.5);
|
||||
pen.line(&[(10.0, 13.5), (10.0, 16.5)]);
|
||||
@@ -403,9 +413,17 @@ fn paint_with(p: &Painter, rect: Rect, icon: Icon, color: Color32, turn: Option<
|
||||
pen.rect_fill(10.0, 4.0, 17.0, 16.0, 0.5);
|
||||
}
|
||||
ChevronDown => pen.line(&[(5.0, 7.5), (10.0, 12.5), (15.0, 7.5)]),
|
||||
ChevronRight => pen.line(&[(7.5, 5.0), (12.5, 10.0), (7.5, 15.0)]),
|
||||
ChevronLeft => pen.line(&[(12.5, 5.0), (7.5, 10.0), (12.5, 15.0)]),
|
||||
TriangleLeft => pen.fill(&[(13.0, 6.0), (13.0, 14.0), (7.0, 10.0)], color),
|
||||
Forward | ChevronRight => pen.line(&[(7.5, 5.0), (12.5, 10.0), (7.5, 15.0)]),
|
||||
Back | ChevronLeft => pen.line(&[(12.5, 5.0), (7.5, 10.0), (12.5, 15.0)]),
|
||||
ChevronDoubleLeft => {
|
||||
pen.line(&[(9.0, 5.0), (4.0, 10.0), (9.0, 15.0)]);
|
||||
pen.line(&[(16.0, 5.0), (11.0, 10.0), (16.0, 15.0)]);
|
||||
}
|
||||
ChevronDoubleRight => {
|
||||
pen.line(&[(4.0, 5.0), (9.0, 10.0), (4.0, 15.0)]);
|
||||
pen.line(&[(11.0, 5.0), (16.0, 10.0), (11.0, 15.0)]);
|
||||
}
|
||||
ChevronUp => pen.line(&[(5.0, 12.5), (10.0, 7.5), (15.0, 12.5)]),
|
||||
Plus => {
|
||||
pen.line(&[(10.0, 4.0), (10.0, 16.0)]);
|
||||
pen.line(&[(4.0, 10.0), (16.0, 10.0)]);
|
||||
|
||||
@@ -1102,7 +1102,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
||||
];
|
||||
let key = if d.organize.is_empty() { "date".to_string() } else { d.organize.clone() };
|
||||
let cur = opts.iter().find(|o| o.0 == key).map_or(opts[0].1, |o| o.1);
|
||||
let combo = egui::ComboBox::from_id_salt("import-organize").selected_text(crate::i18n::tr(cur)).show_ui(ui, |ui| {
|
||||
let combo = crate::widgets::combo_box("import-organize").selected_text(crate::i18n::tr(cur)).show_ui(ui, |ui| {
|
||||
for (k, label) in opts {
|
||||
let r = ui.selectable_label(key == k, crate::i18n::tr(label));
|
||||
register(ui.ctx(), format!("button:importOrganize-{k}"), r.rect);
|
||||
@@ -1185,7 +1185,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
||||
_ => "Leave them",
|
||||
};
|
||||
let mut pick = None;
|
||||
egui::ComboBox::from_id_salt("import-on-deleted").selected_text(crate::i18n::tr(cur)).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box("import-on-deleted").selected_text(crate::i18n::tr(cur)).show_ui(ui, |ui| {
|
||||
for (v, label) in [("restore", "Restore them"), ("fresh", "Import as new"), ("", "Leave them")] {
|
||||
if ui.selectable_label(d.on_deleted == v, crate::i18n::tr(label)).clicked() {
|
||||
pick = Some(v);
|
||||
@@ -1215,7 +1215,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
||||
None if !d.new_album.is_empty() => crate::i18n::tr("New album").into(),
|
||||
None => crate::i18n::tr("None").into(),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("import-album").selected_text(cur).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box("import-album").selected_text(cur).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(d.album.is_none() && d.new_album.is_empty(), crate::i18n::tr("None")).clicked() {
|
||||
d.album = None;
|
||||
d.new_album.clear();
|
||||
@@ -1246,7 +1246,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
||||
.find(|p| p.id == d.preset)
|
||||
.map(|p| crate::i18n::builtin_label(&p.name, p.builtin).to_string())
|
||||
.unwrap_or_else(|| crate::i18n::tr("None").into());
|
||||
egui::ComboBox::from_id_salt("import-preset").selected_text(cur).height(300.0).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box("import-preset").selected_text(cur).height(300.0).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(d.preset.is_empty(), crate::i18n::tr("None")).clicked() {
|
||||
d.preset.clear();
|
||||
}
|
||||
@@ -1259,7 +1259,7 @@ pub fn body(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &mut ImportDialog) {
|
||||
if !app.session.metadata_presets.is_empty() {
|
||||
field(ui, "Metadata", |ui| {
|
||||
let cur = if d.metadata_preset.is_empty() { crate::i18n::tr("None").to_string() } else { d.metadata_preset.clone() };
|
||||
egui::ComboBox::from_id_salt("import-metadata").selected_text(cur).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box("import-metadata").selected_text(cur).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(d.metadata_preset.is_empty(), crate::i18n::tr("None")).clicked() {
|
||||
d.metadata_preset.clear();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! it: wheel, dragging, and ▲ / ▼ strips that scroll while the pointer rests on them, like
|
||||
//! native menus on a small display. The same approach as PhotoCraft's menu bar (its #319).
|
||||
|
||||
use egui::{Id, Rect, Sense, Ui, pos2, vec2};
|
||||
use egui::{Id, Rect, Sense, Ui, vec2};
|
||||
|
||||
/// Height of a scroll-arrow strip.
|
||||
pub const ARROW: f32 = 16.0;
|
||||
@@ -109,13 +109,12 @@ pub fn level(ui: &mut Ui, depth: usize, bar_bottom: Option<f32>, rows: impl FnOn
|
||||
for (strip, dir, live) in [(up, -1.0, offset > 0.5), (down, 1.0, offset < max - 0.5)] {
|
||||
let strip = Rect::from_x_y_ranges(x, strip.y_range());
|
||||
let c = strip.center();
|
||||
let (h, w) = (4.5, 6.0);
|
||||
let tri = if dir < 0.0 {
|
||||
vec![pos2(c.x - w, c.y + h * 0.5), pos2(c.x + w, c.y + h * 0.5), pos2(c.x, c.y - h * 0.5)]
|
||||
} else {
|
||||
vec![pos2(c.x - w, c.y - h * 0.5), pos2(c.x + w, c.y - h * 0.5), pos2(c.x, c.y + h * 0.5)]
|
||||
};
|
||||
ui.painter().add(egui::Shape::convex_polygon(tri, if live { t.text } else { t.text_disabled }, egui::Stroke::NONE));
|
||||
crate::icons::paint(
|
||||
ui.painter(),
|
||||
Rect::from_center_size(c, vec2(16.0, 16.0)),
|
||||
if dir < 0.0 { crate::icons::Icon::ChevronUp } else { crate::icons::Icon::ChevronDown },
|
||||
if live { t.text } else { t.text_disabled },
|
||||
);
|
||||
let id = key.with(dir < 0.0);
|
||||
let r = ui.interact(strip, id, Sense::hover());
|
||||
r.widget_info(|| {
|
||||
@@ -143,6 +142,7 @@ pub fn level(ui: &mut Ui, depth: usize, bar_bottom: Option<f32>, rows: impl FnOn
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use egui::pos2;
|
||||
|
||||
#[test]
|
||||
fn level_room_keeps_every_level_below_the_bar() {
|
||||
|
||||
@@ -930,7 +930,7 @@ fn switch_open_title(ctx: &egui::Context, titles: &[egui::Response]) -> Option<e
|
||||
/// bounded like every level. The row is registered as `menusub:<label>` (the untranslated
|
||||
/// label), and while its submenu is open it is the anchor the next frame's room is measured from.
|
||||
fn submenu(ui: &mut egui::Ui, label: &str, text: String, depth: usize, bar_bottom: Option<f32>, children: impl FnOnce(&mut egui::Ui)) {
|
||||
let r = ui.menu_button(text, |ui| crate::menu_level::level(ui, depth + 1, bar_bottom, children));
|
||||
let r = crate::widgets::menu_button(ui, text, |ui| crate::menu_level::level(ui, depth + 1, bar_bottom, children));
|
||||
crate::widgets::register(ui.ctx(), format!("menusub:{label}"), r.response.rect);
|
||||
if r.inner.is_some() {
|
||||
crate::menu_level::set_anchor(ui.ctx(), depth + 1, r.response.rect);
|
||||
|
||||
@@ -62,9 +62,11 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button(crate::i18n::tr("Sort"), |ui| sort_menu(app, ui));
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Sort"), |ui| sort_menu(app, ui));
|
||||
if compact_zoom && matches!(app.ui.view, ViewMode::Detail | ViewMode::Compare) {
|
||||
ui.menu_button(crate::i18n::tr_format!("Click {}:1", app.ui.click_zoom / 100), |ui| click_zoom_menu(app, ui));
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr_format!("Click {}:1", app.ui.click_zoom / 100), |ui| {
|
||||
click_zoom_menu(app, ui)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ fn work_section(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens, list: &V
|
||||
ui.label(RichText::new(crate::i18n::tr("Limit")).color(t.text_label));
|
||||
let current = list["cacheGb"].as_u64().unwrap_or(20) as u32;
|
||||
let mut choice = current;
|
||||
egui::ComboBox::from_id_salt("denoise-cache-limit").selected_text(tr_format!("{size} GB", size = current)).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box("denoise-cache-limit").selected_text(tr_format!("{size} GB", size = current)).show_ui(ui, |ui| {
|
||||
for gb in CACHE_LIMITS {
|
||||
ui.selectable_value(&mut choice, gb, tr_format!("{size} GB", size = gb));
|
||||
}
|
||||
|
||||
@@ -800,7 +800,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
preview_only_pill(ui, canvas, why);
|
||||
}
|
||||
resp.context_menu(|ui| {
|
||||
ui.menu_button(crate::i18n::tr("Zoom"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Zoom"), |ui| {
|
||||
for (label, cmd) in [("Fit", "view.zoomFit"), ("100%", "view.zoom100"), ("Zoom In", "view.zoomIn"), ("Zoom Out", "view.zoomOut")] {
|
||||
if ui.button(crate::i18n::tr(label)).clicked() {
|
||||
let _ = app.run(cmd, json!({}));
|
||||
|
||||
@@ -574,7 +574,8 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
egui::Grid::new("rename-preview").num_columns(3).spacing([8.0, 2.0]).show(ui, |ui| {
|
||||
for pl in rows {
|
||||
ui.label(egui::RichText::new(&pl.from).color(t.text_dim));
|
||||
ui.label(egui::RichText::new("→").color(t.text_dim));
|
||||
let (arrow, _) = ui.allocate_exact_size(egui::vec2(16.0, 16.0), egui::Sense::hover());
|
||||
crate::icons::paint(ui.painter(), arrow, crate::icons::Icon::ChevronRight, t.text_dim);
|
||||
ui.label(egui::RichText::new(&pl.to).color(t.text));
|
||||
ui.end_row();
|
||||
}
|
||||
@@ -663,7 +664,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
let n = app.session.targets(&json!({})).len();
|
||||
ui.label(format!("{n} selected photos · {} pages", n.div_ceil(options.columns * options.rows)));
|
||||
field(ui, "Paper", |ui| {
|
||||
egui::ComboBox::from_id_salt("contactSheetPaper").selected_text(&options.paper).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box("contactSheetPaper").selected_text(&options.paper).show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut options.paper, "a4".into(), "A4");
|
||||
ui.selectable_value(&mut options.paper, "letter".into(), "Letter");
|
||||
});
|
||||
@@ -682,7 +683,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
// Preset: load a built-in or saved set of options into the dialog
|
||||
field(ui, "Preset", |ui| {
|
||||
let mut chosen = None;
|
||||
let c = egui::ComboBox::from_id_salt("exportPreset").width(220.0).selected_text(crate::i18n::tr("Choose…")).show_ui(ui, |ui| {
|
||||
let c = crate::widgets::combo_box("exportPreset").width(220.0).selected_text(crate::i18n::tr("Choose…")).show_ui(ui, |ui| {
|
||||
let mut builtin = true;
|
||||
for (p, b) in app.session.all_export_presets() {
|
||||
if builtin && !b {
|
||||
@@ -1858,7 +1859,7 @@ fn export_size(ui: &mut egui::Ui, full: &mut bool, r: &mut lightcraft_engine::ex
|
||||
field(ui, "Resize to", |ui| {
|
||||
let cur = MODES.iter().find(|m| m.0 == r.mode).map_or("Long Edge", |m| m.1);
|
||||
let before = r.mode;
|
||||
let c = egui::ComboBox::from_id_salt("exportResizeMode").width(150.0).selected_text(crate::i18n::tr(cur)).show_ui(ui, |ui| {
|
||||
let c = crate::widgets::combo_box("exportResizeMode").width(150.0).selected_text(crate::i18n::tr(cur)).show_ui(ui, |ui| {
|
||||
for (m, l) in MODES {
|
||||
ui.selectable_value(&mut r.mode, m, crate::i18n::tr(l));
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
||||
ui.separator();
|
||||
}
|
||||
for g in profile_groups() {
|
||||
let r = ui.menu_button(crate::i18n::tr(g), |ui| {
|
||||
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);
|
||||
@@ -469,7 +469,7 @@ fn profile_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, d: &DevelopSettings)
|
||||
lut_groups.dedup();
|
||||
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 = ui.menu_button(g.as_str(), |ui| {
|
||||
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);
|
||||
@@ -620,6 +620,7 @@ fn histogram(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
"Left: the standard (SDR) range up to SDR white. Right: the HDR range, one band per stop above SDR white (the orange line: the HDR headroom). With Visualize HDR range on, the bands take its colours.",
|
||||
));
|
||||
}
|
||||
let clipping = hist.as_ref().map(|h| h.clipping());
|
||||
if let Some(h) = hist {
|
||||
let smooth = |v: &[u32]| -> Vec<f32> {
|
||||
(0..256)
|
||||
@@ -678,15 +679,58 @@ fn histogram(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
p.add(egui::Shape::line(pts, Stroke::new(1.5, *c)));
|
||||
}
|
||||
}
|
||||
let (lo, hi) = h.clipping();
|
||||
for (on, x, active) in [(lo > 0.003, plot.left() + 12.0, app.ui.show_clipping), (hi > 0.003, plot.right() - 12.0, app.ui.show_clipping)] {
|
||||
let tri = Rect::from_center_size(pos2(x, plot.top() + 10.0), vec2(14.0, 12.0));
|
||||
p.rect_filled(tri, 2.0, if active { t.hover } else { Color32::from_gray(60) });
|
||||
paint(p, tri.shrink(2.0), Icon::ChevronDown, if on { Color32::WHITE } else { Color32::from_gray(130) });
|
||||
}
|
||||
}
|
||||
if resp.clicked() {
|
||||
app.ui.show_clipping = !app.ui.show_clipping;
|
||||
let mut clicked = resp.clicked();
|
||||
for (name, x, label, clipped) in [
|
||||
("shadows", plot.left() + 20.0, "Shadow clipping", clipping.map(|(lo, _)| lo > 0.003)),
|
||||
("highlights", plot.right() - 20.0, "Highlight clipping", clipping.map(|(_, hi)| hi > 0.003)),
|
||||
] {
|
||||
let rect = Rect::from_center_size(pos2(x, plot.top() + 14.0), vec2(24.0, 22.0));
|
||||
let response = ui.interact(rect, ui.id().with(("histogram-clipping", name)), Sense::click());
|
||||
register(ui.ctx(), format!("histogram:{name}"), rect);
|
||||
response.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), app.ui.show_clipping, crate::i18n::tr(label)));
|
||||
clicked |= response.clicked();
|
||||
let active = app.ui.show_clipping;
|
||||
// Keep the hit target comfortable, with a smaller box and clear space to the plot edges.
|
||||
let box_rect = rect.shrink2(vec2(2.0, 2.0));
|
||||
p.rect_filled(
|
||||
box_rect,
|
||||
3.0,
|
||||
if active {
|
||||
t.pressed
|
||||
} else if response.hovered() {
|
||||
t.hover
|
||||
} else {
|
||||
Color32::from_gray(60)
|
||||
},
|
||||
);
|
||||
if active {
|
||||
p.rect_stroke(box_rect, 3.0, Stroke::new(1.0, t.text), egui::StrokeKind::Inside);
|
||||
}
|
||||
paint(
|
||||
p,
|
||||
Rect::from_center_size(rect.center(), vec2(16.0, 16.0)),
|
||||
Icon::ChevronDown,
|
||||
if clipped == Some(true) || active { t.text } else { t.text_dim },
|
||||
);
|
||||
let status = match clipped {
|
||||
Some(true) => "Clipping detected in this preview.",
|
||||
Some(false) => "No clipping detected in this preview.",
|
||||
None => "Histogram not yet available.",
|
||||
};
|
||||
response.on_hover_text(format!(
|
||||
"{}\n{}\n{}",
|
||||
crate::i18n::tr(label),
|
||||
crate::i18n::tr(if active {
|
||||
"Click to hide clipping warnings for shadows and highlights."
|
||||
} else {
|
||||
"Click to show clipping warnings for shadows and highlights."
|
||||
}),
|
||||
crate::i18n::tr(status)
|
||||
));
|
||||
}
|
||||
if clicked {
|
||||
let _ = app.run("view.clipping", json!({}));
|
||||
}
|
||||
// EXIF strip
|
||||
if let Some(ph) = app.session.catalog.photo(id) {
|
||||
@@ -741,7 +785,7 @@ fn soft_proofing(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
let device = app.ui.proof_profile.as_ref().map(|(_, n)| n.clone());
|
||||
let shown = device.clone().unwrap_or_else(|| cur.label().to_string());
|
||||
let mut pick = false;
|
||||
egui::ComboBox::from_id_salt("proof-space").width(170.0).selected_text(shown).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box("proof-space").width(170.0).selected_text(shown).show_ui(ui, |ui| {
|
||||
for s in OutputSpace::ALL {
|
||||
if ui.selectable_label(device.is_none() && cur == s, s.label()).clicked() {
|
||||
let _ = app.run("view.softProof", json!({"space": s}));
|
||||
@@ -1666,8 +1710,21 @@ fn quick_develop(app: &mut LightcraftApp, ui: &mut egui::Ui, n: usize) {
|
||||
egui::Grid::new("quick-develop").num_columns(5).spacing([4.0, 3.0]).show(ui, |ui| {
|
||||
for (label, ctl, small, big) in rows {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr(label)).size(11.5).color(t.text_dim));
|
||||
for (txt, d) in [("◀◀", -big), ("◀", -small), ("▶", small), ("▶▶", big)] {
|
||||
let r = ui.add(egui::Button::new(egui::RichText::new(txt).size(10.0)).min_size(egui::vec2(28.0, 18.0)));
|
||||
// Keep the existing automation ids while drawing all step arrows as vectors.
|
||||
for (txt, icon, d) in [
|
||||
("◀◀", Icon::ChevronDoubleLeft, -big),
|
||||
("◀", Icon::ChevronLeft, -small),
|
||||
("▶", Icon::ChevronRight, small),
|
||||
("▶▶", Icon::ChevronDoubleRight, big),
|
||||
] {
|
||||
let r = crate::widgets::arrow_button(ui, "", icon, false);
|
||||
r.widget_info(|| {
|
||||
egui::WidgetInfo::labeled(
|
||||
egui::WidgetType::Button,
|
||||
ui.is_enabled(),
|
||||
crate::i18n::tr_format!("{label} {d:+} on every selected photo", d = d, label = crate::i18n::tr(label)),
|
||||
)
|
||||
});
|
||||
crate::widgets::register(ui.ctx(), format!("button:quick-{ctl}-{txt}"), r.rect);
|
||||
if r.on_hover_text(crate::i18n::tr_format!("{label} {d:+} on every selected photo", d = d, label = crate::i18n::tr(label)))
|
||||
.clicked()
|
||||
|
||||
@@ -280,7 +280,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
let _ = app.run("library.clearFilter", json!({}));
|
||||
}
|
||||
// filter presets: apply one, save the current filter, delete
|
||||
let presets = ui.button(egui::RichText::new(crate::i18n::tr("Presets ▾")).font(t.font(12.5)));
|
||||
let presets = crate::widgets::dropdown(ui, "filterPresets", crate::i18n::tr("Presets"), t.font(12.5), t.text);
|
||||
register(ui.ctx(), "button:filterPresets", presets.rect);
|
||||
egui::Popup::menu(&presets).show(|ui| {
|
||||
let names: Vec<String> = app.session.filter_presets.iter().map(|f| f.name.clone()).collect();
|
||||
|
||||
@@ -827,10 +827,15 @@ fn folder_breadcrumbs(app: &mut LightcraftApp, ui: &mut egui::Ui, path: &str) {
|
||||
// Prefer the current folder, showing more ancestors as space permits.
|
||||
let visible = ((ui.available_width() / 80.0) as usize).clamp(1, 4);
|
||||
let skip = parts.len().saturating_sub(visible);
|
||||
let separator = |ui: &mut egui::Ui| {
|
||||
let (r, _) = ui.allocate_exact_size(vec2(16.0, 24.0), Sense::hover());
|
||||
paint(ui.painter(), Rect::from_center_size(r.center(), vec2(16.0, 16.0)), Icon::ChevronRight, t.text_dim);
|
||||
};
|
||||
if skip > 0 {
|
||||
ui.label(egui::RichText::new("… ›").color(t.text_dim)).on_hover_text(path);
|
||||
ui.label(egui::RichText::new("…").color(t.text_dim)).on_hover_text(path);
|
||||
separator(ui);
|
||||
}
|
||||
let separator_w = ui.painter().layout_no_wrap("›".into(), t.font(13.0), t.text_dim).size().x + 8.0;
|
||||
let separator_w = 16.0 + 2.0 * ui.spacing().item_spacing.x;
|
||||
for (i, part) in parts.iter().enumerate().skip(skip) {
|
||||
let remaining = parts.len() - i;
|
||||
let last = remaining == 1;
|
||||
@@ -848,7 +853,7 @@ fn folder_breadcrumbs(app: &mut LightcraftApp, ui: &mut egui::Ui, path: &str) {
|
||||
{
|
||||
let _ = app.run("library.browse", json!({"path": to}));
|
||||
}
|
||||
ui.label(egui::RichText::new("›").size(13.0).color(t.text_dim));
|
||||
separator(ui);
|
||||
}
|
||||
r.on_hover_text(path);
|
||||
}
|
||||
@@ -989,7 +994,7 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
// Photo ▸ Edit In: a copy with this photo's edits, opened in PhotoCraft or another editor
|
||||
// (desktop only: the web can't start programs)
|
||||
ui.add_enabled_ui(crate::menus::ui_enabled(app, "photo.editInPhotoCraft"), |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Edit In"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Edit In"), |ui| {
|
||||
if ui.button(crate::i18n::tr("Edit in PhotoCraft")).clicked() {
|
||||
let _ = app.run("photo.editInPhotoCraft", json!({}));
|
||||
}
|
||||
@@ -1025,27 +1030,27 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
let _ = app.run("photo.setReference", json!({"id": id.0}));
|
||||
}
|
||||
ui.separator();
|
||||
ui.menu_button(crate::i18n::tr("Set Rating"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Set Rating"), |ui| {
|
||||
for r in 0..=5 {
|
||||
if ui.button(if r == 0 { crate::i18n::tr("No Stars").to_string() } else { "★".repeat(r) }).clicked() {
|
||||
let _ = app.run("photo.rate", json!({"rating": r}));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.menu_button(crate::i18n::tr("Set Flag"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Set Flag"), |ui| {
|
||||
for (l, f) in [("Pick", "pick"), ("Reject", "reject"), ("Unflagged", "none")] {
|
||||
if ui.button(crate::i18n::tr(l)).clicked() {
|
||||
let _ = app.run("photo.flag", json!({"flag": f}));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.menu_button(crate::i18n::tr("Set Color Label"), |ui| label_menu(app, ui));
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Set Color Label"), |ui| label_menu(app, ui));
|
||||
if local_only(app, &app.session.targets(&serde_json::Value::Null)) {
|
||||
// albums hold library photos (issue #684): nothing here to add until they join My Photos
|
||||
let add = ui.add_enabled(false, egui::Button::new(crate::i18n::tr("Add to Album"))).on_disabled_hover_text(albums_hint());
|
||||
register(ui.ctx(), "button:contextAddToAlbum", add.rect);
|
||||
} else {
|
||||
let add = ui.menu_button(crate::i18n::tr("Add to Album"), |ui| {
|
||||
let add = crate::widgets::menu_button(ui, crate::i18n::tr("Add to Album"), |ui| {
|
||||
let albums: Vec<_> = app.session.catalog.albums().filter(|a| !a.folder && !a.is_smart()).map(|a| (a.id.0, a.name.clone())).collect();
|
||||
let any = !albums.is_empty();
|
||||
for (aid, name) in albums {
|
||||
@@ -1110,7 +1115,7 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
if ui.button(crate::i18n::tr("Create Version")).clicked() {
|
||||
let _ = app.run("version.create", json!({}));
|
||||
}
|
||||
ui.menu_button(crate::i18n::tr("Stack"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Stack"), |ui| {
|
||||
let stacked = app.session.catalog.stack_of(id).is_some();
|
||||
let several = app.session.selection.ids.len() > 1;
|
||||
for (label, cmd, on) in [
|
||||
@@ -1145,7 +1150,7 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
if ui.button(crate::i18n::tr("Reset Edits")).clicked() {
|
||||
let _ = app.run("develop.reset", json!({}));
|
||||
}
|
||||
ui.menu_button(crate::i18n::tr("Photo Merge"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Photo Merge"), |ui| {
|
||||
let n = app.session.targets(&json!({})).len();
|
||||
for (id, label) in [("dialog.mergeHdr", "HDR…"), ("dialog.mergePanorama", "Panorama…"), ("dialog.mergeHdrPanorama", "HDR Panorama…")] {
|
||||
if ui.add_enabled(n >= 2, egui::Button::new(crate::i18n::tr(label))).clicked() {
|
||||
@@ -1173,7 +1178,7 @@ pub fn context_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
if ui.button(crate::i18n::tr("Export…")).clicked() {
|
||||
let _ = app.run("dialog.export", json!({}));
|
||||
}
|
||||
ui.menu_button(crate::i18n::tr("Export with Preset"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Export with Preset"), |ui| {
|
||||
for (p, _) in app.session.all_export_presets() {
|
||||
if ui.button(&p.name).clicked() {
|
||||
match app.run("app.export", json!({"preset": p.name, "background": true})) {
|
||||
|
||||
@@ -8,6 +8,7 @@ use lightcraft_catalog::{Catalog, PhotoId};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::LightcraftApp;
|
||||
use crate::icons::{Icon, paint};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
use crate::widgets::register;
|
||||
@@ -178,17 +179,12 @@ fn row(app: &mut LightcraftApp, ui: &mut egui::Ui, r: &Row, selection: &[PhotoId
|
||||
// the triangle
|
||||
if r.has_children {
|
||||
let c = pos2(x + 6.0, cy);
|
||||
let tri = Rect::from_center_size(c, vec2(14.0, 14.0));
|
||||
let tri = Rect::from_center_size(c, vec2(16.0, 16.0));
|
||||
let tr = ui.interact(tri, egui::Id::new(("keyword-list-tri", r.path.to_lowercase())), Sense::click());
|
||||
register(ui.ctx(), format!("keywordRowToggle:{}", r.path), tri);
|
||||
let pts = if r.open {
|
||||
vec![c + vec2(-4.0, -2.0), c + vec2(4.0, -2.0), c + vec2(0.0, 3.0)]
|
||||
} else {
|
||||
vec![c + vec2(-2.0, -4.0), c + vec2(3.0, 0.0), c + vec2(-2.0, 4.0)]
|
||||
};
|
||||
// while a filter opens the levels the triangle does nothing: it doesn't light up either
|
||||
let lit = tr.hovered() && can_fold;
|
||||
ui.painter().add(egui::Shape::convex_polygon(pts, if lit { t.text } else { t.text_dim }, Stroke::NONE));
|
||||
paint(ui.painter(), tri, if r.open { Icon::ChevronDown } else { Icon::ChevronRight }, if lit { t.text } else { t.text_dim });
|
||||
if tr.clicked() && can_fold {
|
||||
let key = r.path.to_lowercase();
|
||||
if r.open {
|
||||
@@ -243,7 +239,7 @@ fn row(app: &mut LightcraftApp, ui: &mut egui::Ui, r: &Row, selection: &[PhotoId
|
||||
let ar = ui.interact(arrow, egui::Id::new(("keyword-list-show", r.path.to_lowercase())), Sense::click());
|
||||
register(ui.ctx(), format!("keywordShow:{}", r.path), arrow);
|
||||
if resp.hovered() || ar.hovered() {
|
||||
ui.painter().text(arrow.center(), Align2::CENTER_CENTER, "→", t.font(13.0), if ar.hovered() { t.text } else { t.text_dim });
|
||||
paint(ui.painter(), arrow, Icon::ChevronRight, if ar.hovered() { t.text } else { t.text_dim });
|
||||
}
|
||||
if ar.on_hover_text(crate::i18n::tr("Show Photos with Keyword")).clicked() {
|
||||
super::left::browse_all_photos(app, true);
|
||||
|
||||
@@ -173,17 +173,17 @@ fn row_need(indent: f32, name_w: f32, mark_w: f32, count_w: Option<f32>) -> f32
|
||||
42.0 + indent + name_w.min(MAX_NAME_NEED) + 18.0 + mark_w + count_w.map_or(0.0, |w| w + 16.0)
|
||||
}
|
||||
|
||||
/// The disclosure triangle at the left of a tree row (`indent` is the row's): right-pointing
|
||||
/// The disclosure chevron at the left of a tree row (`indent` is the row's): right-pointing
|
||||
/// when closed, down when open, registered as widget `widget` for the control channel. Returns
|
||||
/// its response: the caller flips the open state on `clicked()`.
|
||||
fn disclosure_triangle(ui: &mut egui::Ui, row: &egui::Response, indent: f32, open: bool, id: egui::Id, widget: String) -> egui::Response {
|
||||
fn disclosure_chevron(ui: &mut egui::Ui, row: &egui::Response, indent: f32, open: bool, id: egui::Id, widget: String) -> egui::Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let c = pos2(row.rect.left() + 10.0 + indent, row.rect.center().y);
|
||||
let tri = Rect::from_center_size(c, vec2(14.0, 14.0));
|
||||
let tri = Rect::from_center_size(c, vec2(16.0, 16.0));
|
||||
let tr = ui.interact(tri, id, Sense::click());
|
||||
register(ui.ctx(), widget, tri);
|
||||
let col = if tr.hovered() { t.text } else { t.text_dim };
|
||||
ui.painter().add(egui::Shape::convex_polygon(triangle_points(c, open), col, egui::Stroke::NONE));
|
||||
paint(ui.painter(), Rect::from_center_size(c, vec2(16.0, 16.0)), if open { Icon::ChevronDown } else { Icon::ChevronRight }, col);
|
||||
tr
|
||||
}
|
||||
|
||||
@@ -197,11 +197,11 @@ fn set_tree_open(ui: &egui::Ui, key: egui::Id, open: bool) {
|
||||
ui.data_mut(|d| d.insert_temp(key, open));
|
||||
}
|
||||
|
||||
/// The triangle of a tree row that has rows inside it (Albums' folders, Local, By Date, Folders,
|
||||
/// Keywords): a click flips `open` and remembers it under `key`. Returns the triangle's response
|
||||
/// The chevron of a tree row that has rows inside it (Albums' folders, Local, By Date, Folders,
|
||||
/// Keywords): a click flips `open` and remembers it under `key`. Returns the chevron's response
|
||||
/// (it sits on the row and takes its clicks, so a row's context menu is attached to it too).
|
||||
fn tree_toggle(ui: &mut egui::Ui, row: &egui::Response, indent: f32, key: egui::Id, open: &mut bool, widget: String) -> egui::Response {
|
||||
let tri = disclosure_triangle(ui, row, indent, *open, key.with("triangle"), widget);
|
||||
let tri = disclosure_chevron(ui, row, indent, *open, key.with("triangle"), widget);
|
||||
if tri.clicked() {
|
||||
*open = !*open;
|
||||
set_tree_open(ui, key, *open);
|
||||
@@ -211,15 +211,6 @@ fn tree_toggle(ui: &mut egui::Ui, row: &egui::Response, indent: f32, key: egui::
|
||||
tri
|
||||
}
|
||||
|
||||
/// A disclosure triangle around `c`: pointing down when open, right when closed.
|
||||
fn triangle_points(c: egui::Pos2, open: bool) -> Vec<egui::Pos2> {
|
||||
if open {
|
||||
vec![c + vec2(-4.0, -2.0), c + vec2(4.0, -2.0), c + vec2(0.0, 3.0)]
|
||||
} else {
|
||||
vec![c + vec2(-2.0, -4.0), c + vec2(3.0, 0.0), c + vec2(-2.0, 4.0)]
|
||||
}
|
||||
}
|
||||
|
||||
/// Space above a section's header.
|
||||
const SECTION_GAP: f32 = 10.0;
|
||||
/// Height of a section's header.
|
||||
@@ -399,7 +390,7 @@ pub fn section_drag_feedback(app: &mut LightcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
|
||||
/// A section's header in `r`: a disclosure chevron in the gutter (where tree rows have their
|
||||
/// triangle), then the bold title; a click folds or unfolds the section (kept in the UI state, so
|
||||
/// chevron), then the bold title; a click folds or unfolds the section (kept in the UI state, so
|
||||
/// it survives restarts), and what the section adds to its header (its button, see `header_button`) sits at
|
||||
/// the visible edge. A folded section says how many rows it holds (`rows`, with View ▸ Show Photo
|
||||
/// Counts). `pinned`: this is the copy kept at the top of the sidebar (see `pinned_header`).
|
||||
@@ -433,9 +424,9 @@ fn section_header(app: &mut LightcraftApp, ui: &mut egui::Ui, section: SidebarSe
|
||||
ui.painter().rect_filled(bar, 4.0, t.hover.gamma_multiply(0.6));
|
||||
}
|
||||
let c = pos2(r.left() + 10.0, r.center().y);
|
||||
register(ui.ctx(), format!("{widget}Chevron:{id}"), Rect::from_center_size(c, vec2(14.0, 14.0)));
|
||||
register(ui.ctx(), format!("{widget}Chevron:{id}"), Rect::from_center_size(c, vec2(16.0, 16.0)));
|
||||
let col = if hovered { t.text } else { t.text_dim };
|
||||
ui.painter().add(egui::Shape::convex_polygon(triangle_points(c, open), col, egui::Stroke::NONE));
|
||||
paint(ui.painter(), Rect::from_center_size(c, vec2(16.0, 16.0)), if open { Icon::ChevronDown } else { Icon::ChevronRight }, col);
|
||||
ui.painter().text(pos2(r.left() + 18.0, r.center().y), Align2::LEFT_CENTER, title, t.semibold(13.5), t.text_label);
|
||||
let button = header_button(section);
|
||||
// (an album being dragged puts "Top Level" in the Albums header)
|
||||
@@ -1549,7 +1540,7 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
||||
resp.context_menu(|ui| {
|
||||
if a.folder {
|
||||
// the same commands as File ▸ New…, aimed at this folder
|
||||
ui.menu_button(crate::i18n::tr("New"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("New"), |ui| {
|
||||
for (label, command) in [
|
||||
("Create Album…", "dialog.newAlbum"),
|
||||
("Create Smart Album…", "dialog.smartAlbum"),
|
||||
@@ -1582,7 +1573,7 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
||||
}
|
||||
}
|
||||
// File ▸ Import's ways in; the import review opens with this album chosen
|
||||
let menu = ui.menu_button(crate::i18n::tr("Import"), |ui| {
|
||||
let menu = crate::widgets::menu_button(ui, crate::i18n::tr("Import"), |ui| {
|
||||
let mut entry = |ui: &mut egui::Ui, label: String, widget: String, command: &str, params: serde_json::Value| {
|
||||
let r = ui.button(label);
|
||||
crate::widgets::register(ui.ctx(), widget, r.rect);
|
||||
@@ -1600,7 +1591,7 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
||||
entry(ui, crate::i18n::tr(label).into(), widget.into(), command, json!({"album": a.id.0}));
|
||||
}
|
||||
// like File ▸ Import from Device: one entry per camera or card found
|
||||
let device = ui.menu_button(crate::i18n::tr("Import from Device"), |ui| {
|
||||
let device = crate::widgets::menu_button(ui, crate::i18n::tr("Import from Device"), |ui| {
|
||||
let devices = lightcraft_engine::devices::devices();
|
||||
if devices.is_empty() {
|
||||
ui.add_enabled(false, egui::Button::new(crate::i18n::tr("No Camera or Card Found")));
|
||||
@@ -1649,7 +1640,7 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
||||
let _ = app.run("dialog.export", json!({}));
|
||||
}
|
||||
ui.add_enabled_ui(has_photos, |ui| {
|
||||
ui.menu_button(crate::i18n::tr("Export Album with Preset"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Export Album with Preset"), |ui| {
|
||||
for (p, _) in app.session.all_export_presets() {
|
||||
if ui.button(&p.name).clicked() {
|
||||
show_all(app);
|
||||
@@ -1666,7 +1657,7 @@ fn folder_menu(app: &mut LightcraftApp, resp: &egui::Response, a: &Album) {
|
||||
let mut folders: Vec<(u64, String)> =
|
||||
app.session.catalog.albums().filter(|f| f.folder && !is_within(app, f.id, a.id)).map(|f| (f.id.0, f.name.clone())).collect();
|
||||
folders.sort_by_key(|(_, n)| n.to_lowercase());
|
||||
ui.menu_button(crate::i18n::tr("Move to"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Move to"), |ui| {
|
||||
if ui.add_enabled(a.parent.is_some(), egui::Button::new(crate::i18n::tr("Top Level"))).clicked() {
|
||||
let _ = app.run("album.move", json!({"id": a.id.0, "parent": null}));
|
||||
}
|
||||
@@ -1879,7 +1870,7 @@ fn synchronize_item(app: &mut LightcraftApp, ui: &mut egui::Ui, n: &FolderNode,
|
||||
/// Set Color Label ▸ of a Folders row (`folder.label`; the submenu is widget `folderLabelMenu`,
|
||||
/// its colours `folderLabel:<colour>`).
|
||||
fn folder_label_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, n: &FolderNode) {
|
||||
let menu = ui.menu_button(crate::i18n::tr("Set Color Label"), |ui| {
|
||||
let menu = crate::widgets::menu_button(ui, crate::i18n::tr("Set Color Label"), |ui| {
|
||||
if let Some(l) = crate::panels::grid::label_items(app, ui, n.label, "folderLabel") {
|
||||
if let Err(e) = app.run("folder.label", json!({"path": n.path, "label": crate::panels::grid::label_param(l)})) {
|
||||
app.toast(ui.ctx(), e);
|
||||
|
||||
@@ -659,14 +659,14 @@ fn component_row_menu(app: &mut LightcraftApp, ui: &mut egui::Ui, mask: u32, k:
|
||||
run(ui, "Invert", json!({"action": "invert"}));
|
||||
run(ui, &crate::i18n::tr_format!("Duplicate \"{label}\"", label = label), json!({"action": "duplicate"}));
|
||||
if k > 0 {
|
||||
ui.menu_button(crate::i18n::tr("Mode"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Mode"), |ui| {
|
||||
for (op, text) in [("add", "Add"), ("subtract", "Subtract"), ("intersect", "Intersect")] {
|
||||
run(ui, text, json!({"action": "op", "op": op}));
|
||||
}
|
||||
});
|
||||
}
|
||||
ui.menu_button(crate::i18n::tr("Intersect with"), |ui| component_menu(app, ui, "intersect"));
|
||||
ui.menu_button(crate::i18n::tr("Subtract"), |ui| component_menu(app, ui, "subtract"));
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Intersect with"), |ui| component_menu(app, ui, "intersect"));
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Subtract"), |ui| component_menu(app, ui, "subtract"));
|
||||
ui.separator();
|
||||
if ui.button(crate::i18n::tr("Rename…")).clicked() {
|
||||
app.ui.renaming_component = Some((mask, k, label.to_string()));
|
||||
|
||||
@@ -69,10 +69,11 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
ui.painter().text(pos2(head.left() + PAD, head.center().y), Align2::LEFT_CENTER, crate::i18n::tr("Named People"), t.semibold(15.0), t.text);
|
||||
// the person page last left (a face opened from it, or its back button): one click returns to it
|
||||
if let Some(last) = app.ui.last_person.clone().filter(|n| people.iter().any(|p| &p.name == n)) {
|
||||
let label = fit(ui.painter(), &format!("{last} ›"), t.font(13.0), 240.0);
|
||||
let width = ui.painter().layout_no_wrap(label.clone(), t.font(13.0), Color32::WHITE).size().x + 24.0;
|
||||
let label = fit(ui.painter(), &last, t.font(13.0), 224.0);
|
||||
let width = ui.painter().layout_no_wrap(label.clone(), t.font(13.0), Color32::WHITE).size().x + 44.0;
|
||||
let r = Rect::from_min_size(pos2(title.right() + 14.0, head.center().y - 12.0), vec2(width, 24.0));
|
||||
let b = ui.put(r, egui::Button::new(RichText::new(label).font(t.font(13.0))).wrap_mode(egui::TextWrapMode::Extend));
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(r));
|
||||
let b = crate::widgets::arrow_button(&mut child, RichText::new(label).font(t.font(13.0)), crate::icons::Icon::ChevronRight, false);
|
||||
register(ui.ctx(), "people:last", b.rect);
|
||||
if b.on_hover_text(crate::i18n::tr("Back to this person")).clicked() {
|
||||
let _ = app.run("view.person", json!({"name": last}));
|
||||
|
||||
@@ -93,7 +93,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, name: &str) {
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(PAD - 4.0);
|
||||
let back = ui.button("‹ People");
|
||||
let back = crate::widgets::arrow_button(ui, crate::i18n::tr("People"), crate::icons::Icon::ChevronLeft, true);
|
||||
register(ui.ctx(), "person:back", back.rect);
|
||||
if back.clicked() {
|
||||
let _ = app.run("view.people", json!({}));
|
||||
|
||||
@@ -134,7 +134,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
register(ui.ctx(), format!("presetGroup:{g}"), r);
|
||||
paint(
|
||||
ui.painter(),
|
||||
Rect::from_center_size(pos2(r.left() + 22.0, r.center().y), vec2(12.0, 12.0)),
|
||||
Rect::from_center_size(pos2(r.left() + 22.0, r.center().y), vec2(16.0, 16.0)),
|
||||
if open { Icon::ChevronDown } else { Icon::ChevronRight },
|
||||
t.text_label,
|
||||
);
|
||||
@@ -185,7 +185,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
ui.painter().text(pos2(text_x, r.center().y), Align2::LEFT_CENTER, &name, t.font(13.0), t.text_label);
|
||||
if fav {
|
||||
paint(ui.painter(), Rect::from_center_size(pos2(r.right() - 20.0, r.center().y), vec2(12.0, 12.0)), Icon::StarFilled, t.star);
|
||||
paint(ui.painter(), Rect::from_center_size(pos2(r.right() - 20.0, r.center().y), vec2(16.0, 16.0)), Icon::StarFilled, t.star);
|
||||
}
|
||||
// resting on a preset previews it in the loupe (no history entry)
|
||||
if resp.hovered()
|
||||
@@ -223,7 +223,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
if ui.button(crate::i18n::tr("Rename…")).clicked() {
|
||||
crate::panels::dialogs::prompt(app, "Rename Preset", "Preset name", &name, "preset.rename", json!({"id": pid}), "name");
|
||||
}
|
||||
ui.menu_button(crate::i18n::tr("Move to Group"), |ui| {
|
||||
crate::widgets::menu_button(ui, crate::i18n::tr("Move to Group"), |ui| {
|
||||
for g in groups.iter().filter(|x| x.user && x.name != group).map(|x| &x.name) {
|
||||
if ui.button(g).clicked() {
|
||||
let _ = app.run("preset.move", json!({"id": pid, "group": g}));
|
||||
|
||||
@@ -82,7 +82,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui, id: PhotoId) {
|
||||
register(ui.ctx(), if imported { format!("profileGroup:lut:{title}") } else { format!("profileGroup:{title}") }, r);
|
||||
paint(
|
||||
ui.painter(),
|
||||
Rect::from_center_size(pos2(r.left() + 22.0, r.center().y), vec2(12.0, 12.0)),
|
||||
Rect::from_center_size(pos2(r.left() + 22.0, r.center().y), vec2(16.0, 16.0)),
|
||||
if open { Icon::ChevronDown } else { Icon::ChevronRight },
|
||||
t.text_label,
|
||||
);
|
||||
|
||||
@@ -777,7 +777,7 @@ fn human_size(bytes: u64) -> String {
|
||||
fn copyright_status(app: &mut LightcraftApp, ui: &mut egui::Ui, current: lightcraft_catalog::CopyrightStatus) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Copyright Status")).size(11.5).color(t.text_dim));
|
||||
let r = egui::ComboBox::from_id_salt("info-copyright-status").selected_text(crate::i18n::tr(current.label())).show_ui(ui, |ui| {
|
||||
let r = crate::widgets::combo_box("info-copyright-status").selected_text(crate::i18n::tr(current.label())).show_ui(ui, |ui| {
|
||||
for st in lightcraft_catalog::CopyrightStatus::ALL {
|
||||
if ui.selectable_label(st == current, crate::i18n::tr(st.label())).clicked() && st != current {
|
||||
let _ = app.run("photo.setMeta", json!({"copyrightStatus": st.id()}));
|
||||
@@ -911,7 +911,7 @@ fn keyword_set(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
let current = sets["current"].as_str().unwrap_or_default().to_string();
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(egui::RichText::new(crate::i18n::tr("Keyword Set")).color(t.text_dim));
|
||||
let combo = egui::ComboBox::from_id_salt("kw-set")
|
||||
let combo = crate::widgets::combo_box("kw-set")
|
||||
.selected_text(crate::i18n::builtin_label(¤t, current == lightcraft_engine::cmd::keywords::RECENT))
|
||||
.show_ui(ui, |ui| {
|
||||
for set in sets["sets"].as_array().into_iter().flatten() {
|
||||
|
||||
@@ -51,7 +51,7 @@ fn match_combo(ui: &mut egui::Ui, salt: &str, m: &mut Match) {
|
||||
Match::Any => "any",
|
||||
Match::None => "none",
|
||||
};
|
||||
egui::ComboBox::from_id_salt(format!("{salt}-match")).width(64.0).selected_text(crate::i18n::tr(label(*m))).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box(format!("{salt}-match")).width(64.0).selected_text(crate::i18n::tr(label(*m))).show_ui(ui, |ui| {
|
||||
for x in [Match::All, Match::Any, Match::None] {
|
||||
ui.selectable_value(m, x, crate::i18n::tr(label(x)));
|
||||
}
|
||||
@@ -149,7 +149,7 @@ fn value_editor(ui: &mut egui::Ui, field: &str, op: &str, v: &mut Value, salt: &
|
||||
let mut unit = stored_unit.to_lowercase();
|
||||
let mut changed =
|
||||
ui.add(egui::DragValue::new(&mut n).range(1.0..=10_000.0).clamp_existing_to_range(false).speed(0.2).fixed_decimals(0)).changed();
|
||||
egui::ComboBox::from_id_salt(format!("{salt}-unit")).width(70.0).selected_text(crate::i18n::tr(&unit)).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box(format!("{salt}-unit")).width(70.0).selected_text(crate::i18n::tr(&unit)).show_ui(ui, |ui| {
|
||||
for u in ["hours", "days", "weeks", "months", "years"] {
|
||||
if ui.selectable_label(unit.trim_end_matches('s') == u.trim_end_matches('s'), crate::i18n::tr(u)).clicked() {
|
||||
unit = u.to_string();
|
||||
@@ -198,23 +198,20 @@ fn value_editor(ui: &mut egui::Ui, field: &str, op: &str, v: &mut Value, salt: &
|
||||
(Some(Kind::Number), _) => number_value(ui, v, field),
|
||||
(Some(Kind::Choice(c)), _) => {
|
||||
let cur = v.as_str().unwrap_or("").to_string();
|
||||
egui::ComboBox::from_id_salt(format!("{salt}-choice")).width(120.0).selected_text(crate::i18n::choice_text(field, &cur)).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for (id, _) in c.iter() {
|
||||
if ui.selectable_label(cur == *id, crate::i18n::choice_text(field, id)).clicked() {
|
||||
*v = json!(id);
|
||||
}
|
||||
crate::widgets::combo_box(format!("{salt}-choice")).width(120.0).selected_text(crate::i18n::choice_text(field, &cur)).show_ui(ui, |ui| {
|
||||
for (id, _) in c.iter() {
|
||||
if ui.selectable_label(cur == *id, crate::i18n::choice_text(field, id)).clicked() {
|
||||
*v = json!(id);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
(Some(Kind::Bool), _) => {
|
||||
// the catalog's reading of the value ("false" is no); one that is neither yes nor no is
|
||||
// shown as written and kept until a choice replaces it, so the dialog reports it
|
||||
let cur = bool_value(v);
|
||||
let text = cur.map_or_else(|| v.to_string(), |b| crate::i18n::bool_text(b).to_string());
|
||||
egui::ComboBox::from_id_salt(format!("{salt}-bool")).width(60.0).selected_text(text).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box(format!("{salt}-bool")).width(60.0).selected_text(text).show_ui(ui, |ui| {
|
||||
for b in [true, false] {
|
||||
if ui.selectable_label(cur == Some(b), crate::i18n::bool_text(b)).clicked() {
|
||||
*v = json!(b);
|
||||
@@ -235,38 +232,37 @@ fn value_editor(ui: &mut egui::Ui, field: &str, op: &str, v: &mut Value, salt: &
|
||||
fn field_menu(ui: &mut egui::Ui, salt: &str, field: &mut String, op: &mut String, value: &mut Value) {
|
||||
let label = field_label(field).unwrap_or(field.as_str()).to_string();
|
||||
let current = field.clone();
|
||||
let r =
|
||||
egui::ComboBox::from_id_salt(format!("{salt}-field")).width(150.0).height(400.0).selected_text(crate::i18n::tr(&label)).show_ui(ui, |ui| {
|
||||
let mut pick = None;
|
||||
let mut item = |ui: &mut egui::Ui, id: &'static str| {
|
||||
let r = ui.selectable_label(current == id, crate::i18n::tr(field_label(id).unwrap_or(id)));
|
||||
register(ui.ctx(), format!("ruleFieldItem:{id}:{salt}"), r.rect);
|
||||
if r.clicked() {
|
||||
pick = Some(id);
|
||||
ui.close();
|
||||
let r = crate::widgets::combo_box(format!("{salt}-field")).width(150.0).height(400.0).selected_text(crate::i18n::tr(&label)).show_ui(ui, |ui| {
|
||||
let mut pick = None;
|
||||
let mut item = |ui: &mut egui::Ui, id: &'static str| {
|
||||
let r = ui.selectable_label(current == id, crate::i18n::tr(field_label(id).unwrap_or(id)));
|
||||
register(ui.ctx(), format!("ruleFieldItem:{id}:{salt}"), r.rect);
|
||||
if r.clicked() {
|
||||
pick = Some(id);
|
||||
ui.close();
|
||||
}
|
||||
};
|
||||
for id in TOP_LEVEL_FIELDS {
|
||||
item(ui, id);
|
||||
}
|
||||
ui.separator();
|
||||
// submenu rows look like the items above them; the one holding the current field stands out
|
||||
// (a transparent frame, not none: a frameless button drops its padding and hover fill)
|
||||
let inactive = &mut ui.visuals_mut().widgets.inactive;
|
||||
inactive.weak_bg_fill = egui::Color32::TRANSPARENT;
|
||||
inactive.bg_stroke = egui::Stroke::NONE;
|
||||
for (group, ids) in FIELD_GROUPS {
|
||||
let text = RichText::new(crate::i18n::tr(group));
|
||||
let text = if ids.contains(¤t.as_str()) { text.strong() } else { text };
|
||||
let r = crate::widgets::menu_button(ui, text, |ui| {
|
||||
for id in *ids {
|
||||
item(ui, id);
|
||||
}
|
||||
};
|
||||
for id in TOP_LEVEL_FIELDS {
|
||||
item(ui, id);
|
||||
}
|
||||
ui.separator();
|
||||
// submenu rows look like the items above them; the one holding the current field stands out
|
||||
// (a transparent frame, not none: a frameless button drops its padding and hover fill)
|
||||
let inactive = &mut ui.visuals_mut().widgets.inactive;
|
||||
inactive.weak_bg_fill = egui::Color32::TRANSPARENT;
|
||||
inactive.bg_stroke = egui::Stroke::NONE;
|
||||
for (group, ids) in FIELD_GROUPS {
|
||||
let text = RichText::new(crate::i18n::tr(group));
|
||||
let text = if ids.contains(¤t.as_str()) { text.strong() } else { text };
|
||||
let r = ui.menu_button(text, |ui| {
|
||||
for id in *ids {
|
||||
item(ui, id);
|
||||
}
|
||||
});
|
||||
register(ui.ctx(), format!("ruleFieldGroup:{group}:{salt}"), r.response.rect);
|
||||
}
|
||||
pick
|
||||
});
|
||||
});
|
||||
register(ui.ctx(), format!("ruleFieldGroup:{group}:{salt}"), r.response.rect);
|
||||
}
|
||||
pick
|
||||
});
|
||||
register(ui.ctx(), format!("ruleField:{salt}"), r.response.rect);
|
||||
let Some(Some(id)) = r.inner else { return };
|
||||
if field == id {
|
||||
@@ -321,7 +317,7 @@ pub fn edit(ui: &mut egui::Ui, rs: &mut RuleSet, salt: &str, depth: usize, path:
|
||||
field_menu(ui, &rsalt, field, op, value);
|
||||
let kind = field_kind(field).unwrap_or(Kind::Text);
|
||||
let op_label = ops_for(kind).iter().find(|o| o.0 == op.as_str()).map_or(op.as_str(), |o| o.1).to_string();
|
||||
egui::ComboBox::from_id_salt(format!("{rsalt}-op")).width(110.0).selected_text(crate::i18n::tr(&op_label)).show_ui(ui, |ui| {
|
||||
crate::widgets::combo_box(format!("{rsalt}-op")).width(110.0).selected_text(crate::i18n::tr(&op_label)).show_ui(ui, |ui| {
|
||||
for (id, l) in ops_for(kind) {
|
||||
if ui.selectable_label(op == id, crate::i18n::tr(l)).clicked() && op != id {
|
||||
let was_special = matches!(op.as_str(), "between" | "inLast" | "notInLast" | "isEmpty" | "isNotEmpty");
|
||||
|
||||
@@ -210,7 +210,7 @@ fn preset_combo(app: &LightcraftApp, ui: &mut egui::Ui, id: &str, current: Optio
|
||||
};
|
||||
let text = current.map(name).unwrap_or_else(|| crate::i18n::tr(none_label).to_string());
|
||||
let mut out = None;
|
||||
let r = egui::ComboBox::from_id_salt(id).width(240.0).selected_text(text).show_ui(ui, |ui| {
|
||||
let r = crate::widgets::combo_box(id).width(240.0).selected_text(text).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(current.is_none(), crate::i18n::tr(none_label)).clicked() {
|
||||
out = Some(None);
|
||||
}
|
||||
@@ -280,7 +280,7 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
.unwrap_or_else(|| pid.to_string()),
|
||||
};
|
||||
let id = format!("settingsCamera-{i}");
|
||||
let r = egui::ComboBox::from_id_salt(&id).width(240.0).selected_text(label).show_ui(ui, |ui| {
|
||||
let r = crate::widgets::combo_box(&id).width(240.0).selected_text(label).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(current == Some(RAW_DEFAULT), crate::i18n::tr("Same as raw default")).clicked() {
|
||||
pick = Some(json!({"camera": cam, "remove": true}));
|
||||
}
|
||||
@@ -329,7 +329,7 @@ fn import_tab(app: &mut LightcraftApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
row(ui, t, crate::i18n::tr("Metadata preset"), |ui| {
|
||||
let cur = d.metadata_preset.clone();
|
||||
let mut pick = None;
|
||||
let r = egui::ComboBox::from_id_salt("settingsMetaPreset")
|
||||
let r = crate::widgets::combo_box("settingsMetaPreset")
|
||||
.width(240.0)
|
||||
.selected_text(cur.clone().unwrap_or_else(|| crate::i18n::tr("None").into()))
|
||||
.show_ui(ui, |ui| {
|
||||
|
||||
@@ -2365,3 +2365,30 @@ fn new_album_can_leave_the_selected_photos_out() {
|
||||
confirm(&mut h);
|
||||
assert!(photos(&h, "Nothing").is_empty());
|
||||
}
|
||||
|
||||
/// Histogram corner controls stay usable even before a histogram is available, and each
|
||||
/// click invokes the same clipping-warning command as the J shortcut exactly once.
|
||||
#[test]
|
||||
fn histogram_corners_toggle_clipping_warnings() {
|
||||
let mut h = demo([1400.0, 1000.0], json!({"view": "detail", "right": "edit"}));
|
||||
for texture in h.app.renderer.textures.values_mut() {
|
||||
texture.histogram = None;
|
||||
}
|
||||
h.step();
|
||||
h.app.ui.show_clipping = false;
|
||||
let histogram = h.app.widgets.iter().find(|(id, _)| id == "histogram").unwrap().1;
|
||||
for id in ["histogram:shadows", "histogram:highlights"] {
|
||||
let rect = h.app.widgets.iter().find(|(widget, _)| widget == id).unwrap().1;
|
||||
assert!(histogram.shrink(8.0).contains_rect(rect), "{id} keeps clear space to the histogram edges: {rect:?} in {histogram:?}");
|
||||
}
|
||||
for (id, enabled) in [("histogram:shadows", true), ("histogram:highlights", false), ("histogram", true)] {
|
||||
let r = h.request("ui.clickWidget", json!({"id": id}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
assert_eq!(h.app.ui.show_clipping, enabled, "{id} must toggle exactly once");
|
||||
}
|
||||
let r = h.request("ui.key", json!({"key": "J"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
assert!(!h.app.ui.show_clipping);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,42 @@ use lightcraft_develop::{ControlSpec, Track};
|
||||
use crate::icons::{Icon, paint};
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// Standard dropdown chevrons, including settings, import/export and rule editors.
|
||||
pub fn combo_box(id: impl egui::AsIdSalt) -> egui::ComboBox {
|
||||
egui::ComboBox::from_id_salt(id).icon(|ui, rect, visuals, _| {
|
||||
paint(ui.painter(), Rect::from_center_size(rect.center(), vec2(16.0, 16.0)), Icon::ChevronDown, visuals.fg_stroke.color);
|
||||
})
|
||||
}
|
||||
|
||||
/// A native button with a vector arrow beside its text; egui reserves the icon's space.
|
||||
pub fn arrow_button<'a>(ui: &mut Ui, text: impl egui::IntoAtoms<'a>, icon: Icon, leading: bool) -> Response {
|
||||
let id = ui.next_auto_id().with("arrow");
|
||||
let mut atoms = text.into_atoms();
|
||||
let arrow = egui::Atom::custom(id, vec2(16.0, 16.0));
|
||||
if leading {
|
||||
atoms.push_left(arrow);
|
||||
} else {
|
||||
atoms.push_right(arrow);
|
||||
}
|
||||
let r = egui::Button::new(atoms).atom_ui(ui);
|
||||
if let Some(rect) = r.rect(id) {
|
||||
paint(ui.painter(), rect, icon, ui.style().interact(&r.response).fg_stroke.color);
|
||||
}
|
||||
r.response
|
||||
}
|
||||
|
||||
/// Keep egui's submenu interaction and layout, using our vector chevron instead of a font glyph.
|
||||
pub fn menu_button<'a, R>(ui: &mut Ui, atoms: impl egui::IntoAtoms<'a>, contents: impl FnOnce(&mut Ui) -> R) -> egui::InnerResponse<Option<R>> {
|
||||
if !egui::containers::menu::is_in_menu(ui) {
|
||||
return ui.menu_button(atoms, contents);
|
||||
}
|
||||
let button = egui::Button::new(atoms.into_atoms()).right_text(" ");
|
||||
let (response, inner) = egui::containers::menu::SubMenuButton::from_button(button).ui(ui, contents);
|
||||
let center = pos2(response.rect.right() - ui.spacing().button_padding.x - 8.0, response.rect.center().y);
|
||||
paint(ui.painter(), Rect::from_center_size(center, vec2(16.0, 16.0)), Icon::ChevronRight, ui.style().interact(&response).fg_stroke.color);
|
||||
egui::InnerResponse::new(inner.map(|r| r.inner), response)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ automation registry
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -441,7 +477,7 @@ pub fn section_header(ui: &mut Ui, id: &str, title: &str, open: bool, enabled: O
|
||||
if ui.rect_contains_pointer(r) {
|
||||
p.rect_filled(r, 0.0, t.chrome.gamma_multiply(1.06));
|
||||
}
|
||||
let chev = Rect::from_center_size(pos2(r.left() + 30.0, r.center().y), vec2(14.0, 14.0));
|
||||
let chev = Rect::from_center_size(pos2(r.left() + 30.0, r.center().y), vec2(16.0, 16.0));
|
||||
paint(p, chev, if open { Icon::ChevronDown } else { Icon::ChevronRight }, t.text_label);
|
||||
p.text(pos2(r.left() + 44.0, r.center().y), Align2::LEFT_CENTER, title, t.semibold(14.0), t.text);
|
||||
let mut toggled = None;
|
||||
@@ -491,8 +527,8 @@ pub fn flyout_row(ui: &mut Ui, id: &str, title: &str, icon: Icon, open: bool) ->
|
||||
p.text(pos2(tile.right() + 10.0, r.center().y), Align2::LEFT_CENTER, title, t.semibold(13.0), t.text_label);
|
||||
paint(
|
||||
p,
|
||||
Rect::from_center_size(pos2(r.right() - 14.0, r.center().y), vec2(12.0, 12.0)),
|
||||
if open { Icon::ChevronDown } else { Icon::TriangleLeft },
|
||||
Rect::from_center_size(pos2(r.right() - 14.0, r.center().y), vec2(16.0, 16.0)),
|
||||
if open { Icon::ChevronDown } else { Icon::ChevronLeft },
|
||||
t.text_label,
|
||||
);
|
||||
resp
|
||||
@@ -629,7 +665,7 @@ pub fn dropdown(ui: &mut Ui, id: &str, text: &str, font: egui::FontId, color: Co
|
||||
register(ui.ctx(), format!("dropdown:{id}"), r);
|
||||
let c = if resp.hovered() { t.text } else { color };
|
||||
ui.painter().galley(pos2(r.left(), r.center().y - galley.size().y / 2.0), galley, c);
|
||||
paint(ui.painter(), Rect::from_center_size(pos2(r.right() - 7.0, r.center().y + 1.0), vec2(12.0, 12.0)), Icon::ChevronDown, c);
|
||||
paint(ui.painter(), Rect::from_center_size(pos2(r.right() - 10.0, r.center().y), vec2(16.0, 16.0)), Icon::ChevronDown, c);
|
||||
resp
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user