diff --git a/Cargo.lock b/Cargo.lock index 5db9c5acd..32b91c425 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/aiongg/craft-libs?rev=f4f9e5b206a721d28ea1214ef8bf85b8e30bb779#f4f9e5b206a721d28ea1214ef8bf85b8e30bb779" +source = "git+https://github.com/aiongg/craft-libs?rev=6261d9d39a0a109a832685f37af6ce75321caea1#6261d9d39a0a109a832685f37af6ce75321caea1" dependencies = [ "egui", "serde", diff --git a/Cargo.toml b/Cargo.toml index 63ea8c527..f5c0281d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,4 +126,4 @@ hayro-jbig2 = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0 # Docking trial: craft-ui from the craft-libs `docking` branch. [patch."https://github.com/storytold/craft-libs"] -craft-ui = { git = "https://github.com/aiongg/craft-libs", rev = "f4f9e5b206a721d28ea1214ef8bf85b8e30bb779" } +craft-ui = { git = "https://github.com/aiongg/craft-libs", rev = "6261d9d39a0a109a832685f37af6ce75321caea1" } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index de859f4ff..5fd50284e 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -22,7 +22,6 @@ mod modal_drag; mod new_document; mod object_control; mod panel_docking; -mod panel_icons; pub mod panels; mod prefs; pub mod render_worker; diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index 44b327196..a55f059d7 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -5,14 +5,22 @@ //! languages, the left in right-to-left ones). use craft_ui::docking::{ - Action, Column, DockCommand, DockOperation, Floating, Group, Layout, Node, Permissions, Placement, Side, Target, UiDock, Workspace, + Action, Column, DockCommand, DockOperation, FloatSpace, Floating, Group, Layout, Node, Permissions, Placement, Side, Target, UiDock, Workspace, }; use serde_json::{Value, json}; use crate::DesignApp; +/// The id of the dock area that draws the panels. +fn area_id() -> egui::Id { + egui::Id::new("designcraft-panel-docking") +} + +/// The dock area a headless app floats into, before it ever drew a frame. +const HEADLESS_AREA: egui::Rect = egui::Rect::from_min_max(egui::Pos2::ZERO, egui::pos2(1400.0, 900.0)); + pub(crate) fn cancel_gesture(ctx: &egui::Context) { - craft_ui::docking::cancel_drag::(ctx, egui::Id::new("designcraft-panel-docking")); + craft_ui::docking::cancel_drag::(ctx, area_id()); } fn normalize(raw: &str) -> Option<&'static str> { @@ -412,17 +420,20 @@ fn floats_alone(layout: &Layout, panel: &str) -> bool { /// A shared docking command (`window.panel.*`, `window.workspace.*`, `ui.dock`) on the layout in /// use, all or nothing. craft-ui's `DockCommand` parses it and makes the layout actions; /// DesignCraft resolves panel names, shows a hidden panel before moving, floating or docking -/// it, floats a panel at its own size, and returns a floated panel to its dock place on Dock -/// Panel. +/// it, floats a panel at its own size (also when it was hidden, which craft-ui never drew), +/// and returns a floated panel to its dock place on Dock Panel. fn shared_command(app: &mut DesignApp, id: &str, params: &Value) -> Result { let mut params = resolve_panels(params)?; + let whole = |key: &str| params.get(key).and_then(Value::as_bool).unwrap_or(false); if id == "window.panel.float" + && !whole("group") + && !whole("column") && let Some(panel) = named_panel(¶ms) && !floats_alone(&layout_of(app), &panel) && let Some(fields) = params.as_object_mut() { let size = floating_size(&panel); - for (key, default) in [("x", 400.0), ("y", 140.0), ("width", size.x), ("height", size.y)] { + for (key, default) in [("width", size.x), ("height", size.y)] { fields.entry(key).or_insert(json!(default)); } } @@ -440,6 +451,7 @@ fn shared_command(app: &mut DesignApp, id: &str, params: &Value) -> Result {} } let rtl = crate::i18n::is_rtl(&app.ui.language); + let space = float_space(app); let mut txn = Transaction::begin(app)?; let shows = match &command { DockCommand::Activate { panel } @@ -462,7 +474,7 @@ fn shared_command(app: &mut DesignApp, id: &str, params: &Value) -> Result Vec::new(), - _ => command.to_actions(&txn.workspace, rtl).map_err(|e| e.to_string())?, + _ => command.to_actions(&txn.workspace, rtl, &space).map_err(|e| e.to_string())?, }; let reveal = match &command { DockCommand::Layout { action } | DockCommand::UiDock(UiDock::Action { action }) => match action { @@ -481,6 +493,14 @@ fn shared_command(app: &mut DesignApp, id: &str, params: &Value) -> Result FloatSpace { + let Some(ctx) = app.egui_ctx.as_ref() else { return FloatSpace::new(HEADLESS_AREA) }; + craft_ui::docking::float_space::(ctx, area_id()).unwrap_or_else(|| FloatSpace::new(ctx.content_rect())) +} + /// A change to the layout in use, committed only when every step succeeded. struct Transaction { /// The layout, and the closed panels with where each one goes back. @@ -721,7 +741,7 @@ pub(crate) fn set_iconic(app: &mut DesignApp, iconic: bool) -> Result(area.with("generation")); data.insert_temp(area.with("generation"), app.ui.docking_generation); @@ -749,12 +769,12 @@ pub(crate) fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if let Some(panel) = raise { dock = dock.raise_panel(panel); } - let icons = crate::panel_icons::panel_icons(ui.ctx(), style.icon_size); - let output = dock.show_content(ui, &layout, &style, &mut PanelContent { app: &mut *app, icons }); - app.ui.docking_flyout = craft_ui::docking::flyout::(ui.ctx(), area); - for event in &output.events { - if *event == craft_ui::docking::DockEvent::ToggleAutoCollapse { - app.ui.dock_auto_collapse = !app.ui.dock_auto_collapse; + let mut output = dock.show_content(ui, &layout, &style, &mut PanelContent { app: &mut *app }); + for event in std::mem::take(&mut output.events) { + match event { + craft_ui::docking::DockEvent::ToggleAutoCollapse => app.ui.dock_auto_collapse = !app.ui.dock_auto_collapse, + craft_ui::docking::DockEvent::FlyoutChanged { panel } => app.ui.docking_flyout = panel, + _ => {} } } if let Some(panel) = output.focused { @@ -778,7 +798,7 @@ pub(crate) fn show(app: &mut DesignApp, ui: &mut egui::Ui) { /// panels closed with a remembered place, the panel whose flyout is open, and where each /// docking gesture starts in the last frame (`hitRects`). pub(crate) fn inspect(app: &DesignApp, ctx: &egui::Context) -> Value { - let hits = craft_ui::docking::DockArea::::hit_rects(ctx, egui::Id::new("designcraft-panel-docking")); + let hits = craft_ui::docking::DockArea::::hit_rects(ctx, area_id()); let hit_rects: Vec = hits.iter().map(|(hit, r)| json!({"hit": hit, "rect": [r.left(), r.top(), r.width(), r.height()]})).collect(); json!({ "layout": layout_of(app), @@ -901,10 +921,14 @@ fn floating_size(panel: &str) -> egui::Vec2 { } } +/// The name of a dock panel's icon in [`crate::icons`]. +fn panel_icon(panel: &str) -> Option<&'static str> { + crate::dock::DOCK_TABS.iter().chain(crate::dock::ICON_PANELS).find(|(id, _, _)| *id == panel).map(|(_, _, icon)| *icon) +} + /// The panels' content and icons. struct PanelContent<'a> { app: &'a mut DesignApp, - icons: std::collections::HashMap>, } impl craft_ui::docking::DockContent for PanelContent<'_> { fn body(&mut self, ui: &mut egui::Ui, id: &String) { @@ -939,8 +963,10 @@ impl craft_ui::docking::DockContent for PanelContent<'_> { fn floating_size(&self, id: &String) -> Option { Some(floating_size(id)) } - fn icon(&self, id: &String) -> Option> { - self.icons.get(id).cloned() + fn paint_icon(&mut self, painter: &egui::Painter, rect: egui::Rect, id: &String, color: egui::Color32) -> bool { + let Some(icon) = panel_icon(id) else { return false }; + crate::icons::paint(painter, rect, icon, color); + true } fn bare(&self, id: &String) -> bool { id == CANVAS diff --git a/crates/ui-egui/src/panel_docking_tests.rs b/crates/ui-egui/src/panel_docking_tests.rs index f182bf644..b410f76ac 100644 --- a/crates/ui-egui/src/panel_docking_tests.rs +++ b/crates/ui-egui/src/panel_docking_tests.rs @@ -972,3 +972,25 @@ fn clicking_an_iconic_column_icon_opens_its_flyout() { click(&mut h, icon.center(), PointerButton::Primary); assert!(h.state().ui.docking_flyout.is_none()); } + +/// Iconic columns paint DesignCraft's own panel icons, in the colour craft-ui passes; a panel +/// without an icon paints nothing. +#[test] +fn panel_icons_are_painted_in_code() { + let mut app = app(); + let mut content = PanelContent { app: &mut app }; + let ctx = egui::Context::default(); + let mut paint = |panel: &str| { + let mut painted = false; + let mut output = ctx.run_ui(egui::RawInput::default(), |ui| { + let painter = ui.ctx().layer_painter(egui::LayerId::background()); + let rect = egui::Rect::from_min_size(Pos2::ZERO, vec2(20.0, 20.0)); + painted = craft_ui::docking::DockContent::paint_icon(&mut content, &painter, rect, &panel.to_string(), egui::Color32::RED); + }); + output.textures_delta.clear(); + (painted, output.shapes.len()) + }; + let (painted, shapes) = paint("pages"); + assert!(painted && shapes > 0, "{painted} {shapes}"); + assert_eq!(paint(CANVAS), (false, 0)); +} diff --git a/crates/ui-egui/src/panel_icons.rs b/crates/ui-egui/src/panel_icons.rs deleted file mode 100644 index 5093d5ae1..000000000 --- a/crates/ui-egui/src/panel_icons.rs +++ /dev/null @@ -1,168 +0,0 @@ -//! Panel icons as textures, for craft-ui's iconic columns, which draw an `ImageSource`. -//! -//! DesignCraft draws its icons in code ([`crate::icons::paint`]). Each panel's icon is painted -//! once, white, into a scratch egui context; the shapes are tessellated there and the triangles -//! filled on the CPU. The column tints the texture with its text colour. - -use std::collections::HashMap; - -use egui::{ - Color32, ColorImage, Context, Id, ImageSource, LayerId, Pos2, RawInput, Rect, TextureHandle, TextureId, TextureOptions, - epaint::{ImageData, Mesh, Primitive}, - load::SizedTexture, - pos2, vec2, -}; - -/// Icons per row of the scratch canvas. -const COLUMNS: usize = 8; - -#[derive(Clone, Default)] -struct Cache { - /// Side of each texture in pixels. - px: usize, - textures: Vec<(String, TextureHandle)>, -} - -fn cache_id() -> Id { - Id::new("designcraft-panel-icon-textures") -} - -/// Every dock panel's icon at `size` points, keyed by panel id. Textures are made once per -/// pixel size and kept in the context. -pub(crate) fn panel_icons(ctx: &Context, size: f32) -> HashMap> { - let scale = size * ctx.pixels_per_point(); - let px = if scale.is_finite() { scale.round().clamp(8.0, 128.0) as usize } else { 20 }; - let cached = ctx.data(|d| d.get_temp::(cache_id())).filter(|cache| cache.px == px); - let cache = cached.unwrap_or_else(|| { - let panels: Vec<(&str, &str)> = crate::dock::DOCK_TABS.iter().chain(crate::dock::ICON_PANELS).map(|(id, _, icon)| (*id, *icon)).collect(); - let images = render(px, &panels); - let textures = panels - .iter() - .zip(images) - .map(|((id, _), image)| ((*id).to_string(), ctx.load_texture(format!("designcraft-panel-icon-{id}"), image, TextureOptions::LINEAR))) - .collect(); - let cache = Cache { px, textures }; - ctx.data_mut(|d| d.insert_temp(cache_id(), cache.clone())); - cache - }); - cache - .textures - .iter() - .map(|(id, texture)| (id.clone(), ImageSource::Texture(SizedTexture::new(texture.id(), vec2(px as f32, px as f32))))) - .collect() -} - -/// The icons of `panels` (id, icon name), white on transparent, `px` pixels square. -fn render(px: usize, panels: &[(&str, &str)]) -> Vec { - let cell = px as f32; - let rows = panels.len().div_ceil(COLUMNS).max(1); - let (width, height) = (COLUMNS * px, rows * px); - let cell_rect = |i: usize| Rect::from_min_size(pos2((i % COLUMNS) as f32 * cell, (i / COLUMNS) as f32 * cell), vec2(cell, cell)); - let scratch = Context::default(); - scratch.set_fonts(crate::theme::font_definitions(&[], "en")); - let input = RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(width as f32, height as f32))), ..Default::default() }; - let mut output = scratch.run_ui(input, |ui| { - let painter = ui.ctx().layer_painter(LayerId::background()); - for (i, (_, icon)) in panels.iter().enumerate() { - let rect = cell_rect(i); - crate::icons::paint(&painter.with_clip_rect(rect), rect, icon, Color32::WHITE); - } - }); - let atlas = - output.textures_delta.set.get(&TextureId::default()).and_then(|deltas| deltas.iter().find(|delta| delta.pos.is_none())).map(|delta| { - match &delta.image { - ImageData::Color(image) => image.clone(), - } - }); - let shapes = std::mem::take(&mut output.shapes); - let primitives = scratch.tessellate(shapes, output.pixels_per_point); - output.textures_delta.clear(); - let mut canvas = vec![[0.0f32; 4]; width * height]; - for primitive in &primitives { - if let Primitive::Mesh(mesh) = &primitive.primitive { - fill(&mut canvas, width, height, mesh, primitive.clip_rect, atlas.as_deref()); - } - } - (0..panels.len()) - .map(|i| { - let origin = cell_rect(i).min; - let (x0, y0) = (origin.x as usize, origin.y as usize); - let mut pixels = Vec::with_capacity(px * px * 4); - for y in y0..y0 + px { - for x in x0..x0 + px { - let [r, g, b, a] = canvas.get(y * width + x).copied().unwrap_or_default(); - pixels.extend([r, g, b, a].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)); - } - } - ColorImage::from_rgba_premultiplied([px, px], &pixels) - }) - .collect() -} - -/// Fill `mesh`'s triangles into a premultiplied RGBA canvas, sampling the font atlas for text. -/// Pixel centres inside a triangle take its interpolated colour; egui's feathered edges give -/// the anti-aliasing. -fn fill(canvas: &mut [[f32; 4]], width: usize, height: usize, mesh: &Mesh, clip: Rect, atlas: Option<&ColorImage>) { - let bounds = clip.intersect(Rect::from_min_size(Pos2::ZERO, vec2(width as f32, height as f32))); - if !bounds.is_positive() { - return; - } - let texel = |u: f32, v: f32| -> [f32; 4] { - if mesh.texture_id != TextureId::default() { - return [1.0; 4]; - } - let Some(atlas) = atlas else { return [1.0; 4] }; - let [w, h] = atlas.size; - let x = ((u * w as f32) as usize).min(w.saturating_sub(1)); - let y = ((v * h as f32) as usize).min(h.saturating_sub(1)); - atlas.pixels.get(y * w + x).map_or([1.0; 4], |c| c.to_array().map(|v| f32::from(v) / 255.0)) - }; - let edge = |a: Pos2, b: Pos2, p: Pos2| (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x); - for triangle in mesh.indices.as_chunks::<3>().0 { - let vertex = |i: usize| triangle.get(i).and_then(|&index| mesh.vertices.get(index as usize)); - let (Some(a), Some(b), Some(c)) = (vertex(0), vertex(1), vertex(2)) else { continue }; - let area = edge(a.pos, b.pos, c.pos); - if !area.is_finite() || area.abs() < 1e-6 { - continue; - } - let min = a.pos.min(b.pos).min(c.pos).max(bounds.min); - let max = a.pos.max(b.pos).max(c.pos).min(bounds.max); - if !(min.x < max.x && min.y < max.y) { - continue; - } - let colors = [a.color, b.color, c.color].map(|c| c.to_array().map(|v| f32::from(v) / 255.0)); - for y in min.y.floor() as usize..(max.y.ceil() as usize).min(height) { - for x in min.x.floor() as usize..(max.x.ceil() as usize).min(width) { - let p = pos2(x as f32 + 0.5, y as f32 + 0.5); - let wa = edge(b.pos, c.pos, p) / area; - let wb = edge(c.pos, a.pos, p) / area; - let wc = 1.0 - wa - wb; - if wa < 0.0 || wb < 0.0 || wc < 0.0 { - continue; - } - let u = wa * a.uv.x + wb * b.uv.x + wc * c.uv.x; - let v = wa * a.uv.y + wb * b.uv.y + wc * c.uv.y; - let t = texel(u, v); - let src: [f32; 4] = std::array::from_fn(|k| (wa * colors[0][k] + wb * colors[1][k] + wc * colors[2][k]) * t[k]); - if let Some(dst) = canvas.get_mut(y * width + x) { - let keep = 1.0 - src[3]; - *dst = std::array::from_fn(|k| src[k] + dst[k] * keep); - } - } - } - } -} - -#[cfg(test)] -mod tests { - #[test] - fn panel_icons_are_painted_into_their_textures() { - let images = super::render(20, &[("pages", "panel-pages"), ("character", "panel-character")]); - assert_eq!(images.len(), 2); - for image in &images { - assert_eq!(image.size, [20, 20]); - let inked = image.pixels.iter().filter(|p| p.a() > 128).count(); - assert!(inked > 10 && inked < 300, "{inked} inked pixels"); - } - } -} diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 41dc02a01..7c1f946cd 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -109,8 +109,11 @@ read as `anchor`. `zone` and `before`. - `window.panel.float {panel, x?, y?, width?, height?, group?, column?}` (and `window.floatPanel`) detaches a panel, its tab group (`group`) or its dock column - (`column`). A panel floats at 400, 140 at its own size unless the params say otherwise; a - panel already alone in a floating window moves or resizes. + (`column`). Without a position it floats 24 points inside the dock area's top-left corner, + clear of the menu bar; a panel floats at its own size and a tab group or column at the size + a drag would float it at. Given values replace these. A panel already alone in a floating + window moves or resizes, and a tab group that is already a whole floating window keeps its + rect unless the params change it. - `window.panel.dock {panel, anchor?, zone?, before?, group?, column?, side?}` (and `window.dockPanel`) docks beside `anchor`, or in a new column of the dock at `side` (`Start` or `End`; a new dock when there is none). Without either, a panel that floated from @@ -179,7 +182,8 @@ default; a column bar's menu toggles it), Escape or a click outside the flyout c `layout` is craft-ui's serialized `Layout`; `customized` is false while the workspace's default layout is in use; `focused` is the panel (or `canvas`) last clicked or selected, or -null; `flyout` is the panel whose iconic-column flyout is open; `autoCollapse` is the +null; `flyout` is the panel whose iconic-column flyout is open (as drawn in the last frame, +or as a command that opens one sets it); `autoCollapse` is the Auto-Collapse Iconic Panels setting; `hidden` lists closed panels that remember their place. `hitRects` lists where each docking gesture starts in the last frame, from craft-ui's `DockArea::hit_rects`: `hit` is a serialized craft-ui `Hit` (`Tab`, `DragBar`, `Border`,