mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 02:06:54 -06:00
W: floating panels — tear off, dock back, persisted (window.floatPanel/dockPanel)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ba1843789d
commit
de51043c4c
@@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- Floating panels: tear a panel off the dock (its float button, or drag its header away) into a movable, resizable window; Dock puts it back; positions persist; `window.floatPanel` / `window.dockPanel`.
|
||||
- Place SVG (vector in PDF export via krilla-svg, resvg on screen, text in the bundled fonts), Photoshop (.psd composite), BMP, and Illustrator files saved with PDF compatibility (.ai). New `designcraft-images` crate (L2) for sniffing, sizes and decoding. EPS is still missing.
|
||||
- View › Display Performance: Fast (grey boxes, no effects, ⌥⇧⌘Z), Typical (72 ppi proxies, ⌥⌘Z), High Quality (⌥⌘H, our default — the renderer is fast enough).
|
||||
- OpenType menu (Character panel): discretionary ligatures, fractions, ordinal, swash, titling, contextual alternates, slashed zero, the four figure styles and stylistic sets 1–20; `type.openType`; mapped to IDML's OTF attributes.
|
||||
|
||||
+114
-19
@@ -79,8 +79,13 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
ui.vertical_centered(|ui| {
|
||||
for (id, label, icon) in ICON_PANELS {
|
||||
let open = app.ui.open_panel.as_deref() == Some(*id);
|
||||
if icons::button(ui, icon, 28.0, open, label).clicked() {
|
||||
app.ui.open_panel = if open { None } else { Some(id.to_string()) };
|
||||
let floats = app.ui.floating.iter().any(|(p, _)| p == id);
|
||||
if icons::button(ui, icon, 28.0, open || floats, label).clicked() {
|
||||
if floats {
|
||||
ui.ctx().move_to_top(egui::LayerId::new(egui::Order::Middle, egui::Id::new(("floating_panel", *id))));
|
||||
} else {
|
||||
app.ui.open_panel = if open { None } else { Some(id.to_string()) };
|
||||
}
|
||||
}
|
||||
ui.add_space(1.0);
|
||||
}
|
||||
@@ -116,24 +121,20 @@ pub fn flyout(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
open = false;
|
||||
}
|
||||
ui.painter().text(close.center(), egui::Align2::CENTER_CENTER, "×", egui::FontId::proportional(15.0), t.text_dim);
|
||||
// Float: the button beside the close box, or drag the header away from the dock.
|
||||
let float_r = egui::Rect::from_min_size(egui::pos2(strip.max.x - 44.0, strip.min.y + 4.0), vec2(18.0, 18.0));
|
||||
let fr = ui.interact(float_r, ui.id().with("flyfloat"), Sense::click()).on_hover_text("Float panel");
|
||||
float_glyph(ui.painter(), float_r, t.text_dim);
|
||||
let head = egui::Rect::from_min_max(strip.min, egui::pos2(float_r.min.x - 2.0, strip.max.y));
|
||||
let hr = ui.interact(head, ui.id().with("flyhead"), Sense::drag());
|
||||
let torn = hr.drag_stopped() && hr.total_drag_delta().is_some_and(|d| d.length() > 24.0);
|
||||
if fr.clicked() || torn {
|
||||
let at = if torn { ui.ctx().pointer_latest_pos().unwrap_or(pos) - vec2(20.0, 10.0) } else { pos - vec2(30.0, -30.0) };
|
||||
float_panel(app, &id, at);
|
||||
return;
|
||||
}
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::same(10)).show(ui, |ui| {
|
||||
egui::ScrollArea::vertical().max_height(screen.height() - 220.0).show(ui, |ui| match id.as_str() {
|
||||
"swatches" => panels::swatches::show(app, ui),
|
||||
"paragraphStyles" => panels::styles::paragraph(app, ui),
|
||||
"characterStyles" => panels::styles::character(app, ui),
|
||||
"stroke" => panels::properties::stroke_panel(app, ui),
|
||||
"character" => panels::properties::character_panel(app, ui),
|
||||
"paragraph" => panels::properties::paragraph_panel(app, ui),
|
||||
"textWrap" => panels::properties::wrap_panel(app, ui),
|
||||
"align" => panels::properties::align_panel(app, ui),
|
||||
"color" => panels::swatches::color_panel(app, ui),
|
||||
"gradient" => panels::swatches::gradient_panel(app, ui),
|
||||
"effects" => panels::properties::effects_panel(app, ui),
|
||||
"links" => panels::properties::links_panel(app, ui),
|
||||
"preflight" => panels::properties::preflight_panel(app, ui),
|
||||
"table" => panels::table::show(app, ui),
|
||||
_ => panels::properties::info_panel(app, ui),
|
||||
});
|
||||
egui::ScrollArea::vertical().max_height(screen.height() - 220.0).show(ui, |ui| panel_body(app, ui, &id));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -141,3 +142,97 @@ pub fn flyout(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
app.ui.open_panel = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The contents of an icon-column panel.
|
||||
pub fn panel_body(app: &mut DesignApp, ui: &mut egui::Ui, id: &str) {
|
||||
match id {
|
||||
"swatches" => panels::swatches::show(app, ui),
|
||||
"paragraphStyles" => panels::styles::paragraph(app, ui),
|
||||
"characterStyles" => panels::styles::character(app, ui),
|
||||
"stroke" => panels::properties::stroke_panel(app, ui),
|
||||
"character" => panels::properties::character_panel(app, ui),
|
||||
"paragraph" => panels::properties::paragraph_panel(app, ui),
|
||||
"textWrap" => panels::properties::wrap_panel(app, ui),
|
||||
"align" => panels::properties::align_panel(app, ui),
|
||||
"color" => panels::swatches::color_panel(app, ui),
|
||||
"gradient" => panels::swatches::gradient_panel(app, ui),
|
||||
"effects" => panels::properties::effects_panel(app, ui),
|
||||
"links" => panels::properties::links_panel(app, ui),
|
||||
"preflight" => panels::properties::preflight_panel(app, ui),
|
||||
"table" => panels::table::show(app, ui),
|
||||
_ => panels::properties::info_panel(app, ui),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tear panel `id` off the dock into a floating window at `at`.
|
||||
pub fn float_panel(app: &mut DesignApp, id: &str, at: egui::Pos2) {
|
||||
if app.ui.open_panel.as_deref() == Some(id) {
|
||||
app.ui.open_panel = None;
|
||||
}
|
||||
app.ui.floating.retain(|(p, _)| p != id);
|
||||
app.ui.floating.push((id.to_string(), [at.x, at.y]));
|
||||
}
|
||||
|
||||
/// Put a floating panel back in the dock's icon column.
|
||||
pub fn dock_panel(app: &mut DesignApp, id: &str) {
|
||||
app.ui.floating.retain(|(p, _)| p != id);
|
||||
}
|
||||
|
||||
/// Floating panels: movable, resizable windows; "Dock" returns one to the icon column.
|
||||
pub fn floating(app: &mut DesignApp, ctx: &egui::Context) {
|
||||
let t = Tokens::get(ctx);
|
||||
for (id, at) in app.ui.floating.clone() {
|
||||
let label = ICON_PANELS.iter().find(|p| p.0 == id).map(|p| p.1).unwrap_or("Panel");
|
||||
let mut open = true;
|
||||
let mut dock = false;
|
||||
let r = egui::Window::new(label)
|
||||
.id(egui::Id::new(("floating_panel", &id)))
|
||||
.title_bar(false)
|
||||
.default_pos(egui::pos2(at[0], at[1]))
|
||||
.default_width(256.0)
|
||||
.resizable(true)
|
||||
.frame(egui::Frame::window(&ctx.global_style()).fill(t.panel).inner_margin(egui::Margin::same(0)))
|
||||
.show(ctx, |ui| {
|
||||
// Header strip (drag it to move the panel): name, Dock, close.
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width().max(200.0), 24.0), Sense::hover());
|
||||
ui.painter().rect_filled(strip, 0.0, t.panel_darker);
|
||||
ui.painter().text(strip.min + vec2(10.0, 12.0), egui::Align2::LEFT_CENTER, label, semibold(12.0), t.text_strong);
|
||||
let close = egui::Rect::from_min_size(egui::pos2(strip.max.x - 22.0, strip.min.y + 3.0), vec2(18.0, 18.0));
|
||||
if ui.interact(close, ui.id().with("fclose"), Sense::click()).on_hover_text("Close").clicked() {
|
||||
open = false;
|
||||
}
|
||||
ui.painter().text(close.center(), egui::Align2::CENTER_CENTER, "×", egui::FontId::proportional(15.0), t.text_dim);
|
||||
let dock_r = egui::Rect::from_min_size(egui::pos2(strip.max.x - 44.0, strip.min.y + 3.0), vec2(18.0, 18.0));
|
||||
if ui.interact(dock_r, ui.id().with("fdock"), Sense::click()).on_hover_text("Dock panel").clicked() {
|
||||
dock = true;
|
||||
}
|
||||
dock_glyph(ui.painter(), dock_r, t.text_dim);
|
||||
egui::Frame::NONE.inner_margin(egui::Margin::same(10)).show(ui, |ui| {
|
||||
egui::ScrollArea::vertical().max_height(ctx.content_rect().height() - 160.0).show(ui, |ui| panel_body(app, ui, &id));
|
||||
});
|
||||
});
|
||||
// Remember where it was left.
|
||||
if let Some(r) = r
|
||||
&& let Some(f) = app.ui.floating.iter_mut().find(|(p, _)| *p == id)
|
||||
{
|
||||
f.1 = [r.response.rect.min.x, r.response.rect.min.y];
|
||||
}
|
||||
if !open || dock {
|
||||
dock_panel(app, &id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Two overlapping boxes: "float this panel".
|
||||
fn float_glyph(p: &egui::Painter, r: egui::Rect, c: Color32) {
|
||||
let s = Stroke::new(1.0, c);
|
||||
p.rect_stroke(egui::Rect::from_min_size(r.min + vec2(3.5, 6.5), vec2(9.0, 8.0)), 0.0, s, egui::StrokeKind::Middle);
|
||||
p.rect_stroke(egui::Rect::from_min_size(r.min + vec2(6.5, 3.5), vec2(9.0, 8.0)), 0.0, s, egui::StrokeKind::Middle);
|
||||
}
|
||||
|
||||
/// A box with a bar on its right: "back into the dock".
|
||||
fn dock_glyph(p: &egui::Painter, r: egui::Rect, c: Color32) {
|
||||
let b = egui::Rect::from_min_size(r.min + vec2(3.5, 4.5), vec2(11.0, 9.0));
|
||||
p.rect_stroke(b, 0.0, Stroke::new(1.0, c), egui::StrokeKind::Middle);
|
||||
p.rect_filled(egui::Rect::from_min_max(egui::pos2(b.max.x - 3.5, b.min.y), b.max), 0.0, c);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,8 @@ pub struct UiState {
|
||||
pub dock_tab: String,
|
||||
/// Panel opened from the collapsed icon column (flyout).
|
||||
pub open_panel: Option<String>,
|
||||
/// Panels torn off the dock into their own floating windows: (panel id, top-left position).
|
||||
pub floating: Vec<(String, [f32; 2])>,
|
||||
pub dock_expanded: bool,
|
||||
pub units: Unit,
|
||||
pub workspace: String,
|
||||
@@ -135,6 +137,7 @@ impl Default for UiState {
|
||||
tools_double_column: false,
|
||||
dock_tab: "properties".into(),
|
||||
open_panel: None,
|
||||
floating: Vec::new(),
|
||||
dock_expanded: true,
|
||||
units: Unit::Picas,
|
||||
workspace: "Essentials".into(),
|
||||
@@ -483,6 +486,7 @@ impl DesignApp {
|
||||
canvas::show(self, ui);
|
||||
});
|
||||
dock::flyout(self, &ctx);
|
||||
dock::floating(self, &ctx);
|
||||
story_editor::show(self, &ctx);
|
||||
dialogs::show(self, &ctx);
|
||||
about::show(self, &ctx);
|
||||
|
||||
@@ -52,6 +52,8 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
||||
None,
|
||||
"{panel: properties|pages|layers|swatches|paragraphStyles|characterStyles|stroke|character|paragraph|textWrap|links|table}",
|
||||
),
|
||||
("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.taskBar", "Contextual Task Bar", None, "{}"),
|
||||
("help.discord", "Join the ArtCraft Discord…", None, "{} — opens https://discord.gg/artcraft in the browser"),
|
||||
@@ -630,11 +632,27 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
||||
app.ui.dock_tab = panel;
|
||||
app.ui.dock_expanded = true;
|
||||
app.ui.open_panel = None;
|
||||
} else {
|
||||
} else if !app.ui.floating.iter().any(|(p, _)| *p == panel) {
|
||||
app.ui.open_panel = if app.ui.open_panel.as_deref() == Some(panel.as_str()) { None } else { Some(panel) };
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"window.floatPanel" | "window.dockPanel" => {
|
||||
let panel = p.get("panel").and_then(Value::as_str).unwrap_or("");
|
||||
if !crate::dock::ICON_PANELS.iter().any(|(id, _, _)| *id == panel) {
|
||||
return Some(Err(format!("{id}: unknown panel `{panel}`")));
|
||||
}
|
||||
if id == "window.floatPanel" {
|
||||
let at = egui::pos2(
|
||||
p.get("x").and_then(Value::as_f64).unwrap_or(400.0) as f32,
|
||||
p.get("y").and_then(Value::as_f64).unwrap_or(160.0) as f32,
|
||||
);
|
||||
crate::dock::float_panel(app, panel, at);
|
||||
} else {
|
||||
crate::dock::dock_panel(app, panel);
|
||||
}
|
||||
Ok(json!({"floating": app.ui.floating.iter().map(|(p, _)| p.clone()).collect::<Vec<_>>()}))
|
||||
}
|
||||
"window.controlBar" => flag(&mut app.ui.control_bar),
|
||||
"window.taskBar" => flag(&mut app.ui.task_bar),
|
||||
"help.about" => {
|
||||
@@ -1144,6 +1162,20 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panels_float_and_dock() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
let r = run_ui(&mut app, "window.floatPanel", &json!({"panel": "swatches", "x": 50, "y": 60})).unwrap().unwrap();
|
||||
assert_eq!(r["floating"], json!(["swatches"]));
|
||||
assert_eq!(app.ui.floating[0].1, [50.0, 60.0]);
|
||||
// Showing a floating panel doesn't also open its flyout.
|
||||
run_ui(&mut app, "window.panel", &json!({"panel": "swatches"})).unwrap().unwrap();
|
||||
assert_eq!(app.ui.open_panel, None);
|
||||
run_ui(&mut app, "window.dockPanel", &json!({"panel": "swatches"})).unwrap().unwrap();
|
||||
assert!(app.ui.floating.is_empty());
|
||||
assert!(run_ui(&mut app, "window.floatPanel", &json!({"panel": "nope"})).unwrap().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_apply_lists_styles_then_commands() {
|
||||
let mut s = designcraft_engine::Session::new();
|
||||
|
||||
+4
-4
@@ -9,13 +9,13 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
|
||||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 98 | 4 | 0 | 98% |
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 33 | 23 | 41 | 46% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
| Application shell & workspace | 65% |
|
||||
| Application shell & workspace | 69% |
|
||||
| Documents, pages, spreads | 71% |
|
||||
| Layers | 74% |
|
||||
| Frames, shapes & paths | 76% |
|
||||
@@ -35,7 +35,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Undo, history, saving | 100% |
|
||||
| Accessibility | 20% |
|
||||
|
||||
Open P0 items: Dockable panels, Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print).
|
||||
Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print).
|
||||
|
||||
## Rows
|
||||
|
||||
@@ -44,7 +44,7 @@ Open P0 items: Dockable panels, Preferences, Vector/page formats (PDF/AI/SVG/EPS
|
||||
| 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 | Dockable panels | P0 | P |
|
||||
| Application shell & workspace | Dockable panels | P0 | D |
|
||||
| Application shell & workspace | Workspaces | P1 | P |
|
||||
| Application shell & workspace | Control panel | P0 | D |
|
||||
| Application shell & workspace | Properties panel | P0 | D |
|
||||
|
||||
Reference in New Issue
Block a user