mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Merge PR #945
This commit is contained in:
15 files changed
+166
No files matched your search
@@ -581,6 +581,7 @@
|
||||
"Friday": "Freitag",
|
||||
"Full": "Vollständig",
|
||||
"Full Screen Preview": "Vollbildvorschau",
|
||||
"Lights Out": "Licht aus",
|
||||
"Full screen mode": "Vollbildmodus",
|
||||
"Full size": "Volle Größe",
|
||||
"GPS Track Log": "GPS-Track",
|
||||
|
||||
@@ -548,6 +548,7 @@
|
||||
"Friday": "Viernes",
|
||||
"Full": "Completa",
|
||||
"Full Screen Preview": "Vista previa a pantalla completa",
|
||||
"Lights Out": "Luces apagadas",
|
||||
"Full screen mode": "Modo de pantalla completa",
|
||||
"Full size": "Tamaño completo",
|
||||
"GPS Track Log": "Registro de ruta GPS",
|
||||
|
||||
@@ -545,6 +545,7 @@
|
||||
"Friday": "Vendredi",
|
||||
"Full": "Complet",
|
||||
"Full Screen Preview": "Aperçu en plein écran",
|
||||
"Lights Out": "Lumières éteintes",
|
||||
"Full screen mode": "Mode plein écran",
|
||||
"Full size": "Taille complète",
|
||||
"GPS Track Log": "Trace GPS",
|
||||
|
||||
@@ -737,6 +737,7 @@
|
||||
"Free": "自由",
|
||||
"Full": "全体",
|
||||
"Full Screen Preview": "全画面プレビュー",
|
||||
"Lights Out": "消灯モード",
|
||||
"Full size": "元のサイズ",
|
||||
"GPS Track Log": "GPS軌跡ファイル",
|
||||
"General": "一般",
|
||||
|
||||
@@ -554,6 +554,7 @@
|
||||
"Friday": "Sexta-feira",
|
||||
"Full": "Completa",
|
||||
"Full Screen Preview": "Pré-visualização em tela cheia",
|
||||
"Lights Out": "Luzes apagadas",
|
||||
"Full screen mode": "Modo tela cheia",
|
||||
"Full size": "Tamanho completo",
|
||||
"GPS Track Log": "Registro de trajeto GPS",
|
||||
|
||||
@@ -455,6 +455,7 @@
|
||||
"Friday": "Пятница",
|
||||
"Full": "Полный",
|
||||
"Full Screen Preview": "Полноэкранный просмотр",
|
||||
"Lights Out": "Выключить свет",
|
||||
"Full screen mode": "Полноэкранный режим",
|
||||
"Full size": "Полный размер",
|
||||
"GPS Track Log": "GPS-трек",
|
||||
|
||||
@@ -933,6 +933,7 @@
|
||||
"Forget unchanged photos": "Забути незмінені фото",
|
||||
"Forward": "Уперед",
|
||||
"Full Screen Preview": "Повноекранний перегляд",
|
||||
"Lights Out": "Вимкнути світло",
|
||||
"Full size": "Повний розмір",
|
||||
"GPS Track Log": "Журнал маршруту GPS",
|
||||
"Geometry": "Геометрія",
|
||||
|
||||
@@ -691,6 +691,7 @@
|
||||
"Friday": "星期五",
|
||||
"Full": "完整",
|
||||
"Full Screen Preview": "全屏预览",
|
||||
"Lights Out": "关灯模式",
|
||||
"Full screen mode": "全屏模式",
|
||||
"Full size": "完整大小",
|
||||
"GPS Track Log": "GPS 轨迹日志",
|
||||
|
||||
@@ -691,6 +691,7 @@
|
||||
"Friday": "星期五",
|
||||
"Full": "完整",
|
||||
"Full Screen Preview": "全螢幕預覽",
|
||||
"Lights Out": "關燈模式",
|
||||
"Full screen mode": "全螢幕模式",
|
||||
"Full size": "完整尺寸",
|
||||
"GPS Track Log": "GPS 軌跡記錄",
|
||||
|
||||
@@ -79,6 +79,8 @@ mod tests_labels;
|
||||
#[cfg(test)]
|
||||
mod tests_library_problem;
|
||||
#[cfg(test)]
|
||||
mod tests_lights_out;
|
||||
#[cfg(test)]
|
||||
mod tests_masking;
|
||||
#[cfg(test)]
|
||||
mod tests_masking_layout;
|
||||
@@ -1035,6 +1037,30 @@ impl LightcraftApp {
|
||||
}
|
||||
|
||||
/// Frame timings once layout is done (`t0`: when layout started).
|
||||
/// Lights Out: veil everything but the photo (the loupe's image, or the grid / Compare /
|
||||
/// Survey area) in black. Painted above the panels, below dialogs and notices.
|
||||
fn lights_out(&self, ctx: &egui::Context) {
|
||||
let a = self.ui.lights_out.veil();
|
||||
if a <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let screen = ctx.content_rect();
|
||||
let keep = if self.ui.view == state::ViewMode::Detail { self.image_rect.or(self.canvas_rect) } else { self.canvas_rect };
|
||||
let veil = egui::Color32::from_black_alpha((a.clamp(0.0, 1.0) * 255.0).round() as u8);
|
||||
let p = ctx.layer_painter(egui::LayerId::new(egui::Order::Middle, egui::Id::new("lights-out")));
|
||||
let k = keep.unwrap_or(egui::Rect::NOTHING).intersect(screen);
|
||||
if !k.is_positive() {
|
||||
p.rect_filled(screen, 0.0, veil);
|
||||
return;
|
||||
}
|
||||
// four bands around the photo
|
||||
use egui::{Rect, pos2};
|
||||
p.rect_filled(Rect::from_min_max(screen.min, pos2(screen.right(), k.top())), 0.0, veil);
|
||||
p.rect_filled(Rect::from_min_max(pos2(screen.left(), k.bottom()), screen.max), 0.0, veil);
|
||||
p.rect_filled(Rect::from_min_max(pos2(screen.left(), k.top()), pos2(k.left(), k.bottom())), 0.0, veil);
|
||||
p.rect_filled(Rect::from_min_max(pos2(k.right(), k.top()), pos2(screen.right(), k.bottom())), 0.0, veil);
|
||||
}
|
||||
|
||||
fn end_frame(&mut self, ctx: &egui::Context, t0: f64) {
|
||||
ctx.output(|o| {
|
||||
for c in &o.commands {
|
||||
@@ -1082,6 +1108,7 @@ impl LightcraftApp {
|
||||
panels::library_problem::banner(self, ui);
|
||||
panels::strip::show(self, ui);
|
||||
egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| docking::show(self, ui));
|
||||
self.lights_out(&ctx);
|
||||
panels::second::show(self, &ctx);
|
||||
panels::notices::show(self, &ctx);
|
||||
panels::dialogs::show(self, &ctx);
|
||||
|
||||
@@ -132,6 +132,7 @@ const LAYOUT: &[(&str, &[&str])] = &[
|
||||
"view.infoOverlay",
|
||||
"---",
|
||||
"view.fullScreenPreview",
|
||||
"view.lightsOut",
|
||||
"view.enterFullScreen",
|
||||
"---",
|
||||
"view.showOriginal",
|
||||
@@ -320,6 +321,7 @@ pub fn checked(app: &LightcraftApp, id: &str) -> Option<bool> {
|
||||
"view.filterBar" => Some(u.filter_bar),
|
||||
"view.navigator" => Some(u.navigator),
|
||||
"view.fullScreenPreview" => Some(u.fullscreen),
|
||||
"view.lightsOut" => Some(u.lights_out != crate::state::LightsOut::On),
|
||||
"view.enterFullScreen" => Some(app.window_is_fullscreen),
|
||||
"panel.edit" => panel(RightPanel::Edit),
|
||||
"panel.profiles" => panel(RightPanel::Profiles),
|
||||
|
||||
@@ -106,6 +106,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[
|
||||
("local.hide", "Remove from Local", None, ""),
|
||||
("local.restoreHidden", "Show Hidden Local Locations", None, ""),
|
||||
("view.fullScreenPreview", "Full Screen Preview", Some("F"), "View"),
|
||||
("view.lightsOut", "Lights Out", Some("Shift+L"), "View"),
|
||||
("view.enterFullScreen", "Enter Full Screen", Some("Cmd+Shift+F"), "View"),
|
||||
("view.infoOverlay", "Cycle Info Overlay", Some("Cmd+I"), "View"),
|
||||
("view.navigator", "Navigator", None, "View"),
|
||||
@@ -404,6 +405,8 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
||||
"view.back" => {
|
||||
if app.ui.dialog.is_some() {
|
||||
app.ui.dialog = None;
|
||||
} else if app.ui.lights_out != crate::state::LightsOut::On {
|
||||
app.ui.lights_out = crate::state::LightsOut::On;
|
||||
} else if app.ui.keyword_painter.is_some() {
|
||||
app.ui.keyword_painter = None;
|
||||
} else if app.ui.fullscreen {
|
||||
@@ -463,6 +466,17 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
|
||||
}
|
||||
Ok(json!({"fullscreen": app.ui.fullscreen}))
|
||||
}
|
||||
"view.lightsOut" => {
|
||||
// {mode?: on | dim | off} (cycles when omitted)
|
||||
match p.get("mode").and_then(Value::as_str) {
|
||||
Some(m) => serde_json::from_value(json!(m)).map_err(|_| format!("unknown lights-out mode: {m} (on|dim|off)")),
|
||||
None => Ok(app.ui.lights_out.next()),
|
||||
}
|
||||
.map(|mode| {
|
||||
app.ui.lights_out = mode;
|
||||
json!({"lightsOut": mode})
|
||||
})
|
||||
}
|
||||
"view.enterFullScreen" => {
|
||||
// applied by the host's frame logic (it knows the window's current state)
|
||||
let on = p.get("on").and_then(Value::as_bool);
|
||||
|
||||
@@ -127,6 +127,38 @@ impl InfoOverlay {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lights Out (View ▸ Lights Out, Shift+L cycles On → Dim → Off → On; Esc turns the lights back
|
||||
/// on): everything but the photo is veiled in black, the panels stay where they are.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum LightsOut {
|
||||
#[default]
|
||||
On,
|
||||
/// The interface at 20 % brightness.
|
||||
Dim,
|
||||
/// The interface black.
|
||||
Off,
|
||||
}
|
||||
|
||||
impl LightsOut {
|
||||
pub fn next(self) -> LightsOut {
|
||||
match self {
|
||||
LightsOut::On => LightsOut::Dim,
|
||||
LightsOut::Dim => LightsOut::Off,
|
||||
LightsOut::Off => LightsOut::On,
|
||||
}
|
||||
}
|
||||
|
||||
/// Opacity of the black veil over everything but the photo.
|
||||
pub fn veil(self) -> f32 {
|
||||
match self {
|
||||
LightsOut::On => 0.0,
|
||||
LightsOut::Dim => 0.8,
|
||||
LightsOut::Off => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// App-level preferences (Settings dialog). They belong to the app, not a library, and are
|
||||
/// saved with the UI state in the app's config folder (`ui.json`, key `settings`); library
|
||||
/// preferences (import defaults, XMP, cache size) live in the library's `prefs.json`.
|
||||
@@ -463,6 +495,9 @@ pub struct UiState {
|
||||
pub auto_advance: bool,
|
||||
/// Full-screen preview (F): the photo alone on black, no chrome.
|
||||
pub fullscreen: bool,
|
||||
/// Lights Out (Shift+L): not saved, every launch starts with the lights on.
|
||||
#[serde(skip)]
|
||||
pub lights_out: LightsOut,
|
||||
/// Window ▸ Second Window.
|
||||
pub second_window: bool,
|
||||
/// The keyword painter: clicking a photo in the grid toggles this keyword on it.
|
||||
@@ -873,6 +908,7 @@ impl Default for UiState {
|
||||
spots_threshold: 50.0,
|
||||
auto_advance: false,
|
||||
fullscreen: false,
|
||||
lights_out: LightsOut::On,
|
||||
slideshow: None,
|
||||
second_window: false,
|
||||
keyword_painter: None,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Headless tests of View ▸ Lights Out (Shift+L): On → Dim → Off → On, Esc turns the lights back
|
||||
//! on, and everything but the photo is veiled while the photo keeps its pixels.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::headless::Headless;
|
||||
use crate::state::LightsOut;
|
||||
use crate::{LightcraftApp, Services};
|
||||
|
||||
const T: Duration = Duration::from_secs(20);
|
||||
const SETTLE: Duration = Duration::from_secs(120);
|
||||
|
||||
fn loupe() -> Headless {
|
||||
let services = Services { png: None, ..Default::default() };
|
||||
let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services);
|
||||
app.ui.settings.gpu = false;
|
||||
let mut h = Headless::new(app, [1400.0, 900.0], 1.0);
|
||||
let r = h.request("ui.set", json!({"view": "detail", "right": "edit"}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.settle(SETTLE);
|
||||
h
|
||||
}
|
||||
|
||||
fn key(h: &mut Headless, k: &str, shift: bool) {
|
||||
let r = h.request("ui.key", json!({"key": k, "shift": shift}), T);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
h.step();
|
||||
h.step();
|
||||
}
|
||||
|
||||
fn luma(img: &egui::ColorImage, p: egui::Pos2) -> u32 {
|
||||
let [w, _] = img.size;
|
||||
let c = img.pixels[(p.y as usize) * w + p.x as usize];
|
||||
c.r() as u32 + c.g() as u32 + c.b() as u32
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_l_cycles_lights_out_and_esc_turns_the_lights_back_on() {
|
||||
let mut h = loupe();
|
||||
assert_eq!(h.app.ui.lights_out, LightsOut::On);
|
||||
key(&mut h, "L", true);
|
||||
assert_eq!(h.app.ui.lights_out, LightsOut::Dim);
|
||||
key(&mut h, "L", true);
|
||||
assert_eq!(h.app.ui.lights_out, LightsOut::Off);
|
||||
key(&mut h, "L", true);
|
||||
assert_eq!(h.app.ui.lights_out, LightsOut::On);
|
||||
// plain L stays the Linear Gradient tool
|
||||
key(&mut h, "L", false);
|
||||
assert_eq!(h.app.ui.lights_out, LightsOut::On);
|
||||
key(&mut h, "Escape", false);
|
||||
key(&mut h, "L", true);
|
||||
key(&mut h, "Escape", false);
|
||||
assert_eq!(h.app.ui.lights_out, LightsOut::On);
|
||||
let r = h.request("engine.execute", json!({"command": "view.lightsOut", "params": {"mode": "dim"}}), T);
|
||||
assert_eq!(h.app.ui.lights_out, LightsOut::Dim, "{r}");
|
||||
let r = h.request("engine.execute", json!({"command": "view.lightsOut", "params": {"mode": "bogus"}}), T);
|
||||
assert_eq!(h.app.ui.lights_out, LightsOut::Dim, "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lights_off_blacks_out_the_interface_but_not_the_photo() {
|
||||
let mut h = loupe();
|
||||
let before = h.snapshot(SETTLE);
|
||||
let img = h.app.image_rect.expect("the loupe shows the photo");
|
||||
let inside = img.center();
|
||||
let outside = egui::pos2(5.0, 5.0);
|
||||
assert!(!img.contains(outside));
|
||||
assert!(luma(&before, outside) > 0, "the title/top bar is not black to begin with");
|
||||
let r = h.request("engine.execute", json!({"command": "view.lightsOut", "params": {"mode": "off"}}), T);
|
||||
assert_eq!(h.app.ui.lights_out, LightsOut::Off, "{r}");
|
||||
let after = h.snapshot(SETTLE);
|
||||
assert_eq!(luma(&after, outside), 0, "the interface goes black");
|
||||
assert_eq!(luma(&after, inside), luma(&before, inside), "the photo keeps its pixels");
|
||||
}
|
||||
@@ -517,6 +517,7 @@ visible menu bar yet: the menu model is only exposed through the control channel
|
||||
| MENU-VIEW-DETAIL | Detail | P0 | ✅ | `cmd:view.detail` | |
|
||||
| MENU-VIEW-EDIT | Edit | P0 | ✅ | `cmd:panel.edit` | |
|
||||
| MENU-VIEW-FULLSCREENPREVIEW | Full Screen Preview | P1 | ✅ | `cmd:view.fullScreenPreview` | |
|
||||
| MENU-VIEW-LIGHTSOUT | Lights Out | P2 | ✅ | `cmd:view.lightsOut` | everything but the photo veiled; panels stay put |
|
||||
| MENU-VIEW-ENTERFULLSCREEN | Enter Full Screen | P1 | ✅ | `cmd:view.enterFullScreen` | |
|
||||
| MENU-VIEW-PHOTOSPANEL | Show/Hide photos panel | P0 | ✅ | `cmd:view.leftPanel` | |
|
||||
| MENU-VIEW-FILMSTRIP | Show/Hide filmstrip | P0 | ✅ | `cmd:view.filmstrip` | |
|
||||
@@ -586,6 +587,7 @@ From `06-shortcuts.md` part 1. Evidence is our binding; conflicts are explained
|
||||
| KEY-DETAIL | Detail — D | P0 | ✅ | `cmd:view.detail` | |
|
||||
| KEY-EDIT | Edit — E | P0 | ✅ | `cmd:panel.edit` | |
|
||||
| KEY-FULLSCREEN | Full-screen preview — F | P1 | ✅ | `cmd:view.fullScreenPreview` | |
|
||||
| KEY-LIGHTSOUT | Lights Out — Shift+L (Classic's L is Linear Gradient here) | P2 | ✅ | `cmd:view.lightsOut` | On → Dim (80 %) → Off; Esc turns the lights on |
|
||||
| KEY-GRID | Grid — G | P0 | ✅ | `cmd:view.gridToggle`, `cmd:view.photoGrid` | G toggles Photo Grid ↔ Square Grid |
|
||||
| KEY-INFO | Info — I | P0 | ✅ | `cmd:panel.info` | |
|
||||
| KEY-KEYWORDS | Keywords — K | P0 | ✅ | `cmd:panel.keywords` | |
|
||||
|
||||
Reference in new issue
Block a user