mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
V: Split Window and New Window — a second view of the document
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
340f84ec0c
commit
5b2cf6ec76
@@ -90,6 +90,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.
|
||||
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
|
||||
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
|
||||
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
|
||||
|
||||
@@ -1262,7 +1262,7 @@ fn handle_input(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, r
|
||||
}
|
||||
// Space-drag = hand (unless a tool drag is under way: then Space is a modifier, e.g. Live
|
||||
// Distribute while resizing).
|
||||
let tool_drag: bool = ui.data(|d| d.get_temp(egui::Id::new("canvas_pointer_down"))).unwrap_or(false);
|
||||
let tool_drag: bool = ui.data(|d| d.get_temp(egui::Id::new(("canvas_pointer_down", app.pane)))).unwrap_or(false);
|
||||
if space && !tool_drag && resp.dragged() {
|
||||
let d = resp.drag_delta();
|
||||
if let Some(v) = app.view_mut() {
|
||||
@@ -1277,7 +1277,7 @@ fn handle_input(app: &mut DesignApp, ui: &mut egui::Ui, resp: &egui::Response, r
|
||||
let vi = app.view_info();
|
||||
let pos = |p: Pos2| xf.to_canvas(p);
|
||||
let mut events: Vec<PointerEvent> = Vec::new();
|
||||
let down_id = egui::Id::new("canvas_pointer_down");
|
||||
let down_id = egui::Id::new(("canvas_pointer_down", app.pane));
|
||||
let mut down: bool = ui.data(|d| d.get_temp(down_id)).unwrap_or(false);
|
||||
let (pressed, released, origin, latest, dbl) = ui.input(|i| {
|
||||
(
|
||||
|
||||
+91
-22
@@ -227,6 +227,34 @@ pub struct Shown {
|
||||
pub size: (f32, f32),
|
||||
}
|
||||
|
||||
impl CanvasCache {
|
||||
pub fn new() -> Self {
|
||||
let mut renderer = designcraft_render::Renderer::new();
|
||||
renderer.threads = designcraft_render::default_threads();
|
||||
CanvasCache {
|
||||
renderer,
|
||||
texture: None,
|
||||
shown: None,
|
||||
pending: None,
|
||||
token: 0,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
worker: None,
|
||||
worker_started: false,
|
||||
last_ms: 0.0,
|
||||
shown_doc: None,
|
||||
pending_doc: None,
|
||||
patcher: designcraft_render::Renderer::new(),
|
||||
patches: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CanvasCache {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CanvasCache {
|
||||
pub renderer: designcraft_render::Renderer,
|
||||
pub texture: Option<egui::TextureHandle>,
|
||||
@@ -260,8 +288,16 @@ pub struct DesignApp {
|
||||
pub ui: UiState,
|
||||
pub services: Services,
|
||||
pub views: HashMap<u64, View>,
|
||||
/// The canvas cache and rect of the pane in `pane` (the other pane's are in `other_pane`).
|
||||
pub canvas: CanvasCache,
|
||||
pub canvas_rect: Option<egui::Rect>,
|
||||
/// Window › Arrange › Split Window: two views of the document side by side.
|
||||
pub split: 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).
|
||||
pub focus_pane: u8,
|
||||
pub other_pane: Option<(CanvasCache, Option<egui::Rect>)>,
|
||||
/// Power Zoom in progress: the zoom to return to and the canvas point it will centre on.
|
||||
pub power_zoom: Option<(f64, designcraft_geom::Point)>,
|
||||
pub perf: Perf,
|
||||
@@ -287,29 +323,17 @@ pub struct DesignApp {
|
||||
|
||||
impl DesignApp {
|
||||
pub fn new(session: Session, services: Services) -> Self {
|
||||
let mut renderer = designcraft_render::Renderer::new();
|
||||
renderer.threads = designcraft_render::default_threads();
|
||||
DesignApp {
|
||||
session,
|
||||
ui: UiState::default(),
|
||||
services,
|
||||
views: HashMap::new(),
|
||||
canvas: CanvasCache {
|
||||
renderer,
|
||||
texture: None,
|
||||
shown: None,
|
||||
pending: None,
|
||||
token: 0,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
worker: None,
|
||||
worker_started: false,
|
||||
last_ms: 0.0,
|
||||
shown_doc: None,
|
||||
pending_doc: None,
|
||||
patcher: designcraft_render::Renderer::new(),
|
||||
patches: 0,
|
||||
},
|
||||
canvas: CanvasCache::new(),
|
||||
canvas_rect: None,
|
||||
split: false,
|
||||
pane: 0,
|
||||
focus_pane: 0,
|
||||
other_pane: None,
|
||||
power_zoom: None,
|
||||
perf: Perf::default(),
|
||||
synthetic: vec![],
|
||||
@@ -334,13 +358,27 @@ impl DesignApp {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Option<&View> {
|
||||
/// The view key of the active document in the current pane.
|
||||
fn view_key(&self) -> Option<u64> {
|
||||
let uid = self.session.active()?.uid;
|
||||
self.views.get(&uid)
|
||||
Some(if self.pane == 1 { uid ^ (1 << 63) } else { uid })
|
||||
}
|
||||
pub fn view(&self) -> Option<&View> {
|
||||
self.views.get(&self.view_key()?)
|
||||
}
|
||||
pub fn view_mut(&mut self) -> Option<&mut View> {
|
||||
let uid = self.session.active()?.uid;
|
||||
Some(self.views.entry(uid).or_default())
|
||||
let k = self.view_key()?;
|
||||
Some(self.views.entry(k).or_default())
|
||||
}
|
||||
/// Make `pane` current: its canvas cache and rect move in.
|
||||
pub fn switch_pane(&mut self, pane: u8) {
|
||||
if pane == self.pane {
|
||||
return;
|
||||
}
|
||||
let (c, r) = self.other_pane.take().unwrap_or_default();
|
||||
let old = (std::mem::replace(&mut self.canvas, c), std::mem::replace(&mut self.canvas_rect, r));
|
||||
self.other_pane = Some(old);
|
||||
self.pane = pane;
|
||||
}
|
||||
pub fn view_info(&self) -> ViewInfo {
|
||||
ViewInfo { zoom: self.view().map(|v| v.zoom).unwrap_or(1.0) }
|
||||
@@ -547,7 +585,38 @@ impl DesignApp {
|
||||
if !presenting {
|
||||
chrome::doc_tabs(self, ui);
|
||||
}
|
||||
canvas::show(self, ui);
|
||||
if self.split && !presenting {
|
||||
// Two panes with a divider; each has its own view (zoom, scroll) and render cache.
|
||||
let r = ui.available_rect_before_wrap();
|
||||
let mid = r.center().x;
|
||||
let halves = [
|
||||
egui::Rect::from_min_max(r.min, egui::pos2(mid - 1.0, r.max.y)),
|
||||
egui::Rect::from_min_max(egui::pos2(mid + 1.0, r.min.y), r.max),
|
||||
];
|
||||
ui.painter().rect_filled(egui::Rect::from_min_max(egui::pos2(mid - 1.0, r.min.y), egui::pos2(mid + 1.0, r.max.y)), 0.0, t.border);
|
||||
for (k, half) in halves.into_iter().enumerate() {
|
||||
self.switch_pane(k as u8);
|
||||
// A new pane starts at the other pane's view.
|
||||
if k == 1 && 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;
|
||||
}
|
||||
}
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(half), |ui| canvas::show(self, ui));
|
||||
if ui.input(|i| i.pointer.any_pressed() && i.pointer.interact_pos().is_some_and(|p| half.contains(p))) {
|
||||
self.focus_pane = k as u8;
|
||||
}
|
||||
}
|
||||
self.switch_pane(self.focus_pane);
|
||||
} else {
|
||||
self.switch_pane(0);
|
||||
self.focus_pane = 0;
|
||||
canvas::show(self, ui);
|
||||
}
|
||||
});
|
||||
dock::flyout(self, &ctx);
|
||||
dock::floating(self, &ctx);
|
||||
|
||||
@@ -80,6 +80,8 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
||||
("window.floatPanel", "Float Panel", None, "{panel, x?, y?} — the panel in its own movable window"),
|
||||
("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.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"),
|
||||
@@ -508,6 +510,11 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
(
|
||||
"Window",
|
||||
&[
|
||||
">Arrange",
|
||||
"ui:window.newWindow",
|
||||
"ui:window.split",
|
||||
"<",
|
||||
"-",
|
||||
"ui:window.controlBar",
|
||||
"ui:window.taskBar",
|
||||
"ui:window.toolsDoubleColumn",
|
||||
@@ -994,6 +1001,15 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
Ok(json!({"floating": app.ui.floating.iter().map(|(p, _)| p.clone()).collect::<Vec<_>>()}))
|
||||
}
|
||||
"window.controlBar" => flag(&mut app.ui.control_bar),
|
||||
"window.split" | "window.newWindow" => {
|
||||
let on = if id == "window.newWindow" { true } else { p.get("on").and_then(Value::as_bool).unwrap_or(!app.split) };
|
||||
app.split = on;
|
||||
if !on {
|
||||
app.switch_pane(0);
|
||||
app.focus_pane = 0;
|
||||
}
|
||||
Ok(json!(on))
|
||||
}
|
||||
"window.taskBar" => flag(&mut app.ui.task_bar),
|
||||
"help.about" => {
|
||||
app.ui.about = p.get("open").and_then(Value::as_bool).unwrap_or(true);
|
||||
@@ -1372,6 +1388,7 @@ pub fn checked(app: &DesignApp, id: &str, params: &Value) -> Option<bool> {
|
||||
"view.taggedFrames" => app.ui.tagged_frames,
|
||||
"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.taskBar" => app.ui.task_bar,
|
||||
"window.toolsDoubleColumn" => app.ui.tools_double_column,
|
||||
"view.togglePreview" => app.ui.screen_mode == crate::ScreenMode::Preview,
|
||||
@@ -1700,6 +1717,41 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_window_panes_have_their_own_views() {
|
||||
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.split", &json!({})).unwrap().unwrap(), json!(true));
|
||||
frame(&mut app);
|
||||
frame(&mut app);
|
||||
// Both panes have a canvas, each half as wide.
|
||||
let r0 = app.canvas_rect.unwrap();
|
||||
let r1 = app.other_pane.as_ref().and_then(|o| o.1).unwrap();
|
||||
assert!(r0.width() < 800.0 && r1.width() < 800.0 && (r0.center().x - r1.center().x).abs() > 300.0, "{r0:?} {r1:?}");
|
||||
// Zooming the right pane leaves the left alone.
|
||||
app.switch_pane(1);
|
||||
let z0 = app.view().unwrap().zoom;
|
||||
crate::canvas::set_zoom(&mut app, z0 * 3.0);
|
||||
app.switch_pane(0);
|
||||
assert!((app.view().unwrap().zoom - z0).abs() < 1e-9);
|
||||
app.switch_pane(1);
|
||||
assert!((app.view().unwrap().zoom - z0 * 3.0).abs() < 1e-6);
|
||||
assert_eq!(run_ui(&mut app, "window.split", &json!({})).unwrap().unwrap(), json!(false));
|
||||
assert_eq!(app.pane, 0);
|
||||
frame(&mut app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panels_float_and_dock() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
+6
-6
@@ -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 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 92 | 4 | 1 | 97% |
|
||||
| P2 | 76 | 39 | 25 | 12 | 68% |
|
||||
| P1 | 97 | 92 | 5 | 0 | 97% |
|
||||
| P2 | 76 | 40 | 25 | 11 | 69% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
| Application shell & workspace | 88% |
|
||||
| Application shell & workspace | 91% |
|
||||
| Documents, pages, spreads | 94% |
|
||||
| Layers | 95% |
|
||||
| Frames, shapes & paths | 100% |
|
||||
@@ -31,7 +31,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Interactivity & digital | 90% |
|
||||
| Output & production | 80% |
|
||||
| XML & automation | 83% |
|
||||
| View & navigation | 89% |
|
||||
| View & navigation | 93% |
|
||||
| Undo, history, saving | 100% |
|
||||
| Accessibility | 70% |
|
||||
|
||||
@@ -43,7 +43,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
|---|---|---|---|
|
||||
| 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 | M |
|
||||
| Application shell & workspace | New Window for same document | P1 | P |
|
||||
| Application shell & workspace | Dockable panels | P0 | D |
|
||||
| Application shell & workspace | Workspaces | P1 | D |
|
||||
| Application shell & workspace | Control panel | P0 | D |
|
||||
@@ -306,7 +306,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
||||
| View & navigation | Structure / tag markers | P2 | P |
|
||||
| View & navigation | Display performance | P0 | D |
|
||||
| View & navigation | Navigate pages | P0 | D |
|
||||
| View & navigation | Split window | P2 | M |
|
||||
| View & navigation | Split window | P2 | D |
|
||||
| View & navigation | Rotate spread view | P2 | M |
|
||||
| View & navigation | Find/zoom to object | P1 | D |
|
||||
| Undo, history, saving | Unlimited undo/redo | P0 | D |
|
||||
|
||||
Reference in New Issue
Block a user