W: New Window in its own OS window

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-04 00:08:02 -04:00
1 parent 79f266c04c
commit 1c8952f6db
4 files changed
+93 -10

No files matched your search

+2 -2
View File
@@ -93,7 +93,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Graphic cells (`table.placeGraphic`, `table.textCell`, Table › Convert Cell Type, Table panel): images in table cells, fitted or filled, clipped to the cell on screen and in PDF.
- Live Distribute (Space while dragging a selection handle; `transform.resize {distribute}`): objects keep their size and spread with the bounds.
- Liquid Layout (`liquid.pageRule/object`, `guide.liquid`, `layout.createAlternate`, Liquid Layout panel): scale / re-center / guide-based / object-based page rules applied on page resize and Document Setup; dashed liquid guides; alternate layouts as resized page copies in a named section. Alternate layouts aren't linked to their source.
- Split Window / New Window (`window.split`, `window.newWindow`, Window ▸ Arrange): two side-by-side views of the document with their own zoom, scroll and render cache. New Window opens that second view rather than a separate OS window.
- Split Window / New Window (`window.split`, `window.newWindow`, Window ▸ Arrange): two side-by-side views of the document with their own zoom, scroll and render cache. New Window puts the second view in its own OS window (an egui viewport; a floating window on the web).
- Transparency flattener (`file.exportPdf {flatten: high|medium|low|ppi}`, Edit ▸ Transparency Flattener Presets): each object involving transparency becomes an opaque image of its area over what's beneath it, so everything else stays vector (spreads with transparent parent items are rasterised whole).
- Video and sound (place .mp4/.mov/.webm/.mp3/.wav…, `media.options/get`, Media panel): media frames show a poster or placeholder on screen, in print and in PDF, and export as HTML5 `<video>`/`<audio>` in EPUB. No in-app playback or PDF rich media.
- Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Kashida justification, caret movement in visual order and digit substitution aren't there yet.
@@ -177,7 +177,7 @@ four agents in parallel on separate crates:
| Work | Estimate |
|---|---|
| Open P0 (2: PDF/X-4 validation against a certified checker, PostScript interpretation for EPS) | 8 h |
| Open P1 (3 partial: New Window as a separate OS window, language support, on-screen blending in the blend space) | 10 h |
| Open P1 (1 partial: on-screen blending in the blend space) | 4 h |
| Open P2 (16 partial: vertical/RTL refinements, alternate-layout linking, separations from source colours, buttons as form fields, interactive PDF extras, scripting, localization breadth, …) | 45 h |
| Depth and pixel fidelity of every dialog, panel and menu against InDesign 2026 | 90 h |
| Performance (incremental composition, GPU raster) and interchange hardening (IDML/PDF corpus) | 55 h |
+42 -1
View File
@@ -332,6 +332,8 @@ pub struct DesignApp {
pub canvas_rect: Option<egui::Rect>,
/// Window › Arrange › Split Window: two views of the document side by side.
pub split: bool,
/// Window › Arrange › New Window: the second view in its own window (pane 1).
pub second_window: bool,
/// The current pane (0 or 1): views, cache and rect refer to it.
pub pane: u8,
/// The pane last clicked (menu zoom and scroll go there).
@@ -372,6 +374,7 @@ impl DesignApp {
canvas: CanvasCache::new(),
canvas_rect: None,
split: false,
second_window: false,
pane: 0,
focus_pane: 0,
other_pane: None,
@@ -666,10 +669,48 @@ impl DesignApp {
self.switch_pane(self.focus_pane);
} else {
self.switch_pane(0);
self.focus_pane = 0;
// With a second window, the main one takes the focus back when clicked.
if !self.second_window || (ui.input(|i| i.pointer.any_pressed()) && ui.ui_contains_pointer()) {
self.focus_pane = 0;
}
canvas::show(self, ui);
}
});
// New Window: pane 1 in a window of its own.
if self.second_window
&& !presenting
&& let Some(title) = self.session.active().map(|d| format!("{} — 2", d.doc.title))
{
let fill = t.pasteboard;
let ctx = ui.ctx().clone();
ctx.show_viewport_immediate(
egui::ViewportId::from_hash_of("designcraft_second_window"),
egui::ViewportBuilder::default().with_title(title).with_inner_size([960.0, 720.0]),
|ui, _class| {
egui::CentralPanel::default().frame(egui::Frame::NONE.fill(fill)).show(ui, |ui| {
self.switch_pane(1);
if self.view().is_none() {
self.switch_pane(0);
let v = self.view().copied();
self.switch_pane(1);
if let (Some(mut v), Some(m)) = (v, self.view_mut()) {
v.fitted = false;
*m = v;
}
}
if ui.input(|i| i.pointer.any_pressed()) {
self.focus_pane = 1;
}
canvas::show(self, ui);
});
if ui.input(|i| i.viewport().close_requested()) {
self.second_window = false;
self.focus_pane = 0;
}
},
);
self.switch_pane(self.focus_pane);
}
dock::flyout(self, &ctx);
dock::floating(self, &ctx);
story_editor::show(self, &ctx);
+46 -4
View File
@@ -112,7 +112,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
("window.dockPanel", "Dock Panel", None, "{panel} — back into the dock's icon column"),
("window.controlBar", "Control", Some("Cmd+Alt+6"), "{}"),
("window.split", "Split Window", None, "{on?: bool} — two views of the document side by side, each with its own zoom and scroll"),
("window.newWindow", "New Window", None, "{} — another view of the active document (shown beside the first)"),
("window.newWindow", "New Window", None, "{on?: bool} — another view of the active document in its own window"),
("window.taskBar", "Contextual Task Bar", None, "{}"),
("help.discord", "Join the ArtCraft Discord…", None, "{} — opens https://discord.gg/artcraft in the browser"),
("help.appPage", "DesignCraft Website…", None, "{} — opens https://getartcraft.com/apps/designcraft"),
@@ -1202,15 +1202,27 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
flag(&mut app.ui.tag_markers)
}
"edit.dynamicSpelling" => flag(&mut app.ui.dynamic_spelling),
"window.split" | "window.newWindow" => {
let on = if id == "window.newWindow" { true } else { p.get("on").and_then(Value::as_bool).unwrap_or(!app.split) };
"window.split" => {
let on = p.get("on").and_then(Value::as_bool).unwrap_or(!app.split);
app.split = on;
if !on {
if on {
app.second_window = false;
} else {
app.switch_pane(0);
app.focus_pane = 0;
}
Ok(json!(on))
}
"window.newWindow" => {
// The second view moves into its own window (on the web: a floating window).
app.second_window = p.get("on").and_then(Value::as_bool).unwrap_or(true);
app.split = false;
if !app.second_window {
app.switch_pane(0);
app.focus_pane = 0;
}
Ok(json!(app.second_window))
}
"window.taskBar" => flag(&mut app.ui.task_bar),
"help.about" => {
app.ui.about = p.get("open").and_then(Value::as_bool).unwrap_or(true);
@@ -1593,6 +1605,7 @@ pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option<bool> {
"changes.track" => app.session.active().is_some_and(|d| d.doc.settings.track_changes),
"window.controlBar" => app.ui.control_bar,
"window.split" => app.split,
"window.newWindow" => app.second_window,
"view.proofColors" => app.ui.proof_colors,
"view.flattenerPreview" => app.ui.flattener_preview,
"view.tagMarkers" => app.ui.tag_markers,
@@ -2116,6 +2129,35 @@ mod tests {
}
}
#[test]
fn new_window_shows_a_second_view() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.session.execute("file.new", &json!({})).unwrap();
let ctx = egui::Context::default();
let frame = |app: &mut crate::DesignApp| {
let input =
egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))), ..Default::default() };
let mut out = ctx.run_ui(input, |ui| {
app.logic(&ui.ctx().clone());
app.ui(ui);
});
out.textures_delta.clear();
};
frame(&mut app);
assert_eq!(run_ui(&mut app, "window.newWindow", &json!({})).unwrap().unwrap(), json!(true));
assert!(!app.split);
frame(&mut app);
frame(&mut app);
// Pane 1 was drawn (its rect is held for it) and has its own view.
assert!(app.other_pane.as_ref().and_then(|o| o.1).is_some() || app.pane == 1);
app.switch_pane(1);
assert!(app.view().is_some(), "the second window has a view");
app.switch_pane(0);
assert_eq!(checked(&app, "window.newWindow", &Value::Null), Some(true));
run_ui(&mut app, "window.newWindow", &json!({"on": false})).unwrap().unwrap();
frame(&mut app);
}
#[test]
fn panels_float_and_dock() {
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+3 -3
View File
@@ -10,12 +10,12 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
| Priority | Features | Done | Partial | Missing | Parity |
|---|---|---|---|---|---|
| P0 | 102 | 100 | 2 | 0 | 99% |
| P1 | 97 | 95 | 2 | 0 | 99% |
| P1 | 97 | 96 | 1 | 0 | 99% |
| P2 | 76 | 65 | 11 | 0 | 93% |
| Area | Parity |
|---|---|
| Application shell & workspace | 96% |
| Application shell & workspace | 99% |
| Documents, pages, spreads | 98% |
| Layers | 100% |
| Frames, shapes & paths | 100% |
@@ -43,7 +43,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
|---|---|---|---|
| Application shell & workspace | Start/Home screen | P0 | D |
| Application shell & workspace | Multiple documents in tabs | P0 | D |
| Application shell & workspace | New Window for same document | P1 | P |
| Application shell & workspace | New Window for same document | P1 | D |
| Application shell & workspace | Dockable panels | P0 | D |
| Application shell & workspace | Workspaces | P1 | D |
| Application shell & workspace | Control panel | P0 | D |