mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-19 02:10:49 -05:00
Camera Raw opens in a window of its own (#1361)
* Camera Raw opens in a window of its own The Camera Raw dialog (Filter › Camera Raw Filter and the open-time dialog) was a full-window overlay drawn over the PhotoCraft window, covering its title bar: it looked like a fullscreen app without window controls. Adobe Camera Raw is a separate window. Show it in an egui immediate viewport: an OS window titled "Camera Raw (name)" / "Camera Raw Filter (layer)", 1360 x 900 by default, resizable (minimum 900 x 600). The dialog stays modal: the main window is dimmed and takes no input while it is open. Closing the window is Cancel (for the open-time dialog that closes the raw being opened). Where the platform has no extra windows (ctx.embed_viewports(), the web build) it is drawn over the main window as before, with its painted title strip. The drawing code is unchanged apart from the title strip, which is not painted in a window of its own. * Camera Raw window: scrim theme token, close-is-Cancel test, document the screenshot gap - theme::Tokens gains `scrim` (the dim over the main window behind a modal that takes all input); Camera Raw uses it instead of a hard-coded colour. - closing_the_camera_raw_window_cancels: the overlay fallback keeps the dialog open, a close request on its window cancels without history. - control-protocol.md: eframe 0.36 cannot screenshot an immediate viewport, so ui.screenshot shows the dimmed main window while Camera Raw has its own window; agents read it via ui.inspect.cameraRaw and see it offscreen with the snapshot example or kittest (which embed viewports). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+95
-11
No files matched your search
@@ -835,9 +835,48 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
app.ui.status = e;
|
||||
app.ui.status_error = true;
|
||||
}
|
||||
if app.camera_raw.is_none() {
|
||||
let Some(d) = app.camera_raw.as_ref() else { return };
|
||||
// Where the platform has no extra windows (the web build), draw over the main window.
|
||||
if ctx.embed_viewports() {
|
||||
draw(app, ctx, false);
|
||||
return;
|
||||
}
|
||||
let window_title = d.window_title();
|
||||
// Camera Raw is modal, as in Photoshop: the main window is dimmed and takes no input while it
|
||||
// is open (in a separate window, or drawn over it where there are no extra windows).
|
||||
let main = ctx.content_rect();
|
||||
egui::Area::new(egui::Id::new("camera-raw-modal")).order(egui::Order::Foreground).fixed_pos(main.min).show(ctx, |ui| {
|
||||
let (r, _) = ui.allocate_exact_size(main.size(), Sense::click_and_drag());
|
||||
ui.painter().rect_filled(r, 0.0, Tokens::get(ui.ctx()).scrim);
|
||||
});
|
||||
// A window of its own (title bar, moved and resized like any window), like Adobe Camera Raw.
|
||||
let builder = egui::ViewportBuilder::default().with_title(window_title).with_inner_size([1360.0, 900.0]).with_min_inner_size([900.0, 600.0]);
|
||||
let mut close = false;
|
||||
ctx.show_viewport_immediate(egui::ViewportId::from_hash_of("camera-raw-window"), builder, |ui, class| {
|
||||
if ui.ctx().input(|i| i.viewport().close_requested()) {
|
||||
close = true;
|
||||
}
|
||||
draw(app, ui.ctx(), class != egui::ViewportClass::EmbeddedWindow);
|
||||
});
|
||||
if close && app.camera_raw.is_some() {
|
||||
cancel(app);
|
||||
}
|
||||
}
|
||||
|
||||
impl CameraRawDialog {
|
||||
/// "Camera Raw (name)" for the open-time dialog, "Camera Raw Filter (layer)" otherwise.
|
||||
fn window_title(&self) -> String {
|
||||
if self.target == Target::OpenRaw {
|
||||
crate::i18n::fmt(tl!("Camera Raw ({name})"), &[("name", &self.layer_name)])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Camera Raw Filter ({layer})"), &[("layer", &self.layer_name)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The dialog's contents over the whole of `ctx`'s window. `own_window`: the OS window already
|
||||
/// shows the title, so none is painted.
|
||||
fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
|
||||
let t = Tokens::get(ctx);
|
||||
let screen = ctx.content_rect();
|
||||
let mut action: Option<&str> = None;
|
||||
@@ -851,16 +890,13 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
|
||||
let (full, _) = ui.allocate_exact_size(screen.size(), Sense::click());
|
||||
let painter = ui.painter().clone();
|
||||
painter.rect_filled(full, 0.0, t.chrome);
|
||||
let title = ERect::from_min_size(full.min, vec2(full.width(), 30.0));
|
||||
painter.rect_filled(title, 0.0, t.dock);
|
||||
painter.line_segment([title.left_bottom(), title.right_bottom()], Stroke::new(1.0, t.separator));
|
||||
let window_title = if d.target == Target::OpenRaw {
|
||||
crate::i18n::fmt(tl!("Camera Raw ({name})"), &[("name", &d.layer_name)])
|
||||
} else {
|
||||
crate::i18n::fmt(tl!("Camera Raw Filter ({layer})"), &[("layer", &d.layer_name)])
|
||||
};
|
||||
let title = ERect::from_min_size(full.min, vec2(full.width(), if own_window { 0.0 } else { 30.0 }));
|
||||
if !own_window {
|
||||
painter.rect_filled(title, 0.0, t.dock);
|
||||
painter.line_segment([title.left_bottom(), title.right_bottom()], Stroke::new(1.0, t.separator));
|
||||
painter.text(title.center(), Align2::CENTER_CENTER, d.window_title(), FontId::proportional(13.0), t.text);
|
||||
}
|
||||
let ok_label = if d.target == Target::OpenRaw { tl!("Open") } else { tl!("OK") };
|
||||
painter.text(title.center(), Align2::CENTER_CENTER, window_title, FontId::proportional(13.0), t.text);
|
||||
let footer_h = 48.0;
|
||||
let body = ERect::from_min_max(pos2(full.left(), title.bottom()), pos2(full.right(), full.bottom() - footer_h));
|
||||
let scope = &mut app.ui.camera_raw_scope;
|
||||
@@ -1104,6 +1140,37 @@ mod tests {
|
||||
assert!((pixel[0] - 128.0 / 255.0).abs() < 0.0001);
|
||||
}
|
||||
|
||||
/// Closing Camera Raw's window is Cancel. Without a multi-window backend egui embeds the
|
||||
/// "window" (`ViewportClass::EmbeddedWindow`), and the close request comes from the window
|
||||
/// it is drawn in; the overlay fallback (`embed_viewports`) keeps the dialog open.
|
||||
#[test]
|
||||
fn closing_the_camera_raw_window_cancels() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
let ctx = egui::Context::default();
|
||||
PhotocraftApp::setup_context(&ctx, crate::theme::ThemeKind::Pro);
|
||||
app.run("file.new", json!({"width": 64, "height": 48})).unwrap();
|
||||
app.run("layer.new.layer", json!({})).unwrap();
|
||||
app.run("edit.fill", json!({"color": "#808080"})).unwrap();
|
||||
let steps = app.session.active().unwrap().history.past_len();
|
||||
let frame = |app: &mut PhotocraftApp, close: bool| {
|
||||
let mut input = egui::RawInput::default();
|
||||
if close {
|
||||
let info = input.viewports.entry(egui::ViewportId::ROOT).or_default();
|
||||
info.events.push(egui::ViewportEvent::Close);
|
||||
}
|
||||
ctx.run_ui(input, |ui| show(app, ui.ctx())).textures_delta.clear();
|
||||
};
|
||||
open(&mut app, &ctx).unwrap();
|
||||
frame(&mut app, false);
|
||||
assert!(app.camera_raw.is_some(), "drawn over the main window");
|
||||
ctx.set_embed_viewports(false);
|
||||
frame(&mut app, false);
|
||||
assert!(app.camera_raw.is_some(), "drawn in its window");
|
||||
frame(&mut app, true);
|
||||
assert!(app.camera_raw.is_none(), "closing the window is Cancel");
|
||||
assert_eq!(app.session.active().unwrap().history.past_len(), steps);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smart_filter_double_click_edits_the_stored_camera_raw_filter_in_its_own_dialog() {
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
|
||||
|
||||
@@ -115,6 +115,8 @@ pub struct Tokens {
|
||||
/// The custom title bar's Close button while hovered (Windows' red), and its glyph.
|
||||
pub caption_close: Color32,
|
||||
pub caption_close_text: Color32,
|
||||
/// Dims the main window behind a modal that takes all input (Camera Raw).
|
||||
pub scrim: Color32,
|
||||
}
|
||||
|
||||
impl Tokens {
|
||||
@@ -178,6 +180,7 @@ impl Tokens {
|
||||
histogram_level: 225,
|
||||
caption_close: Color32::from_rgb(196, 43, 28),
|
||||
caption_close_text: Color32::WHITE,
|
||||
scrim: Color32::from_black_alpha(110),
|
||||
},
|
||||
ThemeKind::Studio => Tokens {
|
||||
kind,
|
||||
@@ -216,6 +219,7 @@ impl Tokens {
|
||||
histogram_level: 225,
|
||||
caption_close: Color32::from_rgb(196, 43, 28),
|
||||
caption_close_text: Color32::WHITE,
|
||||
scrim: Color32::from_black_alpha(110),
|
||||
},
|
||||
ThemeKind::StudioLight => Tokens {
|
||||
kind,
|
||||
@@ -254,6 +258,7 @@ impl Tokens {
|
||||
histogram_level: 240,
|
||||
caption_close: Color32::from_rgb(196, 43, 28),
|
||||
caption_close_text: Color32::WHITE,
|
||||
scrim: Color32::from_black_alpha(110),
|
||||
},
|
||||
ThemeKind::Classic => Tokens {
|
||||
kind,
|
||||
@@ -292,6 +297,7 @@ impl Tokens {
|
||||
histogram_level: 240,
|
||||
caption_close: Color32::from_rgb(196, 43, 28),
|
||||
caption_close_text: Color32::WHITE,
|
||||
scrim: Color32::from_black_alpha(110),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -724,7 +730,8 @@ pub mod live {
|
||||
danger,
|
||||
warning,
|
||||
tab_strip,
|
||||
row_selected
|
||||
row_selected,
|
||||
scrim
|
||||
);
|
||||
}
|
||||
unknown
|
||||
|
||||
@@ -307,6 +307,16 @@ a driver process crash is detected by the startup marker on the next launch.
|
||||
|
||||
### Camera Raw dialog
|
||||
|
||||
On the desktop the dialog is a window of its own (title bar, moved and resized like any window), as Adobe Camera Raw is; the main window is dimmed and takes no input while it is open, and closing the window is Cancel. Where the platform has no extra windows (the web build) it is drawn over the main window.
|
||||
|
||||
**Screenshots:** `ui.screenshot` captures the main window only, and eframe cannot capture an
|
||||
immediate viewport, so while Camera Raw has its own window the PNG shows the dimmed main window,
|
||||
not the dialog. Read the dialog's state through `ui.inspect` (`cameraRaw`: params, histogram,
|
||||
zoom, control rectangles) and drive it with the `ui` params below. To *see* it, render offscreen
|
||||
(`cargo run -p photocraft-ui-egui --example snapshot -- --script '[["ui.menu.invoke", {"id": "filter.cameraRaw"}]]'`
|
||||
or an `egui_kittest` harness): those embed
|
||||
viewports, so the dialog is drawn over the main window and appears in the image.
|
||||
|
||||
`ui.menu.invoke {"id":"filter.cameraRaw","params":{}}` opens Camera Raw on the active
|
||||
RGB/Grayscale layer. `params: {"smartFilter": {"layer": id, "index": i}}` opens it on an existing
|
||||
Camera Raw smart filter instead (as double-clicking the filter in the Layers panel does): the
|
||||
|
||||
Reference in new issue
Block a user