mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-30 05:06:48 -06:00
Docking trial: craft-ui 6261d9d, paint_icon, float space, flyout event
This commit is contained in:
Generated
+1
-1
@@ -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",
|
||||
|
||||
+1
-1
@@ -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" }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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::<String>(ctx, egui::Id::new("designcraft-panel-docking"));
|
||||
craft_ui::docking::cancel_drag::<String>(ctx, area_id());
|
||||
}
|
||||
|
||||
fn normalize(raw: &str) -> Option<&'static str> {
|
||||
@@ -412,17 +420,20 @@ fn floats_alone(layout: &Layout<String>, 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<Value, String> {
|
||||
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<Value
|
||||
_ => {}
|
||||
}
|
||||
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<Value
|
||||
let actions = match &command {
|
||||
// Showing it was the whole command (craft-ui's `DockCommand::apply` does the same).
|
||||
DockCommand::Dock { anchor: None, side: None, .. } if shown || returned => 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<Value
|
||||
Ok(txn.commit(app, closed, reveal))
|
||||
}
|
||||
|
||||
/// Where a command floats a panel, group or column it gives no rect: inside the dock area as
|
||||
/// last drawn, else inside the window, else (headless, before any frame) inside
|
||||
/// [`HEADLESS_AREA`].
|
||||
fn float_space(app: &DesignApp) -> FloatSpace<String> {
|
||||
let Some(ctx) = app.egui_ctx.as_ref() else { return FloatSpace::new(HEADLESS_AREA) };
|
||||
craft_ui::docking::float_space::<String>(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<Value, Str
|
||||
}
|
||||
|
||||
pub(crate) fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
||||
let area = egui::Id::new("designcraft-panel-docking");
|
||||
let area = area_id();
|
||||
let changed_workspace = ui.ctx().data_mut(|data| {
|
||||
let previous = data.get_temp::<u64>(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::<String>(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::<String>::hit_rects(ctx, egui::Id::new("designcraft-panel-docking"));
|
||||
let hits = craft_ui::docking::DockArea::<String>::hit_rects(ctx, area_id());
|
||||
let hit_rects: Vec<Value> = 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<String, egui::ImageSource<'static>>,
|
||||
}
|
||||
impl craft_ui::docking::DockContent<String> for PanelContent<'_> {
|
||||
fn body(&mut self, ui: &mut egui::Ui, id: &String) {
|
||||
@@ -939,8 +963,10 @@ impl craft_ui::docking::DockContent<String> for PanelContent<'_> {
|
||||
fn floating_size(&self, id: &String) -> Option<egui::Vec2> {
|
||||
Some(floating_size(id))
|
||||
}
|
||||
fn icon(&self, id: &String) -> Option<egui::ImageSource<'static>> {
|
||||
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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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<String, ImageSource<'static>> {
|
||||
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>(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<ColorImage> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user