Implement stable serialization and restoration for dock layouts, including auto-hidden and floating panels

- Add `persistence.rs` for stable serialization of dock layouts without runtime identifiers.
- Introduce `preview.rs` for geometry handling of dock drop previews.
- Create `sizing.rs` to manage content-aware sizing policies and constraint solving for dock panels.
- Implement `welcome.rs` to provide a welcome surface when no documents are open, featuring New and Open actions.
This commit is contained in:
2026-07-16 22:10:22 +03:00
parent 1240d25011
commit b09795ccff
72 changed files with 11898 additions and 3115 deletions
+66 -24
View File
@@ -20,10 +20,10 @@
//! buffers, dirty flags). They depend on `hcie_tile::TiledLayer` and the
//! dynamic `tiled::composite_tiled_into` compositor.
use crate::dynamic_loader::tiled;
use crate::Engine;
use hcie_protocol::LayerData;
use hcie_tile::TiledLayer;
use crate::Engine;
use crate::dynamic_loader::tiled;
/// Wrapper to send a raw pixel pointer to a background thread as a `usize`.
///
@@ -35,9 +35,13 @@ use crate::dynamic_loader::tiled;
struct SendPtr(usize);
impl SendPtr {
#[inline]
fn new(p: *const u8) -> Self { Self(p as usize) }
fn new(p: *const u8) -> Self {
Self(p as usize)
}
#[inline]
fn as_ptr(&self) -> *const u8 { self.0 as *const u8 }
fn as_ptr(&self) -> *const u8 {
self.0 as *const u8
}
}
// SAFETY: The usize encodes a pointer to layer.pixels which is stable for the engine lifetime.
// The background thread only reads, never writes.
@@ -74,7 +78,11 @@ impl Engine {
for item in items {
// Recycle returned before buffer back into the pool
if let Some(buf) = item.return_before {
if self.stroke_before_buf.as_ref().map_or(true, |b| b.len() != buf.len()) {
if self
.stroke_before_buf
.as_ref()
.map_or(true, |b| b.len() != buf.len())
{
self.stroke_before_buf = Some(buf);
}
}
@@ -127,8 +135,8 @@ impl Engine {
self.last_stroke_pos = Some((x, y, 1.0));
self.sketch_history.clear();
if let Some(layer) = self.document.get_layer_by_id_mut(layer_id) {
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
let layer_size = (layer.width * layer.height) as usize; // pixel count (for mask)
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
let layer_size = (layer.width * layer.height) as usize; // pixel count (for mask)
// Pool active_stroke_mask: reuse buffer if size matches, otherwise allocate.
// This avoids a ~16MB allocation per stroke on a 4K canvas.
@@ -211,8 +219,8 @@ impl Engine {
if let Some(idx) = self.document.layer_index_by_id(layer_id) {
let layer = &mut self.document.layers[idx];
let lw = layer.width;
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
// Restore layer effects from backup so they are re-composited correctly
let layer_pixels = (layer.width * layer.height * 4) as usize; // RGBA byte size
// Restore layer effects from backup so they are re-composited correctly
if let Some(backup) = self.stroke_effects_backup.take() {
layer.effects = backup;
}
@@ -221,12 +229,18 @@ impl Engine {
}
if !layer.effects.is_empty() || !layer.styles.is_empty() {
layer.effects_dirty.store(true, std::sync::atomic::Ordering::Release);
layer
.effects_dirty
.store(true, std::sync::atomic::Ordering::Release);
self.document.composite_dirty = true;
}
// Take the pooled before buffer (no allocation)
log::debug!("[end_stroke] layer_pixels={}, stroke_before_buf={:?}", layer_pixels, self.stroke_before_buf.as_ref().map(|b| b.len()));
log::debug!(
"[end_stroke] layer_pixels={}, stroke_before_buf={:?}",
layer_pixels,
self.stroke_before_buf.as_ref().map(|b| b.len())
);
let before = match self.stroke_before_buf.take() {
Some(buf) if buf.len() == layer_pixels => buf,
_ => vec![0u8; layer_pixels],
@@ -252,7 +266,8 @@ impl Engine {
let t_start = std::time::Instant::now();
// SAFETY: after_ptr points to layer.pixels which is valid for the
// engine lifetime. No mutations happen between end_stroke() and next begin_stroke().
let after_slice = unsafe { std::slice::from_raw_parts(after_ptr.as_ptr(), after_len) };
let after_slice =
unsafe { std::slice::from_raw_parts(after_ptr.as_ptr(), after_len) };
let mut item = PendingHistoryItem {
layer_idx: idx,
before_pixels: Vec::new(),
@@ -291,7 +306,10 @@ impl Engine {
item.return_before = Some(before);
pending_history.lock().unwrap().push(item);
}
log::trace!("[end_stroke_async] Background snapshot comparison took {}ms", t_start.elapsed().as_millis());
log::trace!(
"[end_stroke_async] Background snapshot comparison took {}ms",
t_start.elapsed().as_millis()
);
return;
}
}
@@ -302,7 +320,10 @@ impl Engine {
// Copy into owned memory for PendingHistoryItem.
item.after_pixels = after_slice.to_vec();
pending_history.lock().unwrap().push(item);
log::trace!("[end_stroke_async] Background fallback snapshot comparison took {}ms", t_start.elapsed().as_millis());
log::trace!(
"[end_stroke_async] Background fallback snapshot comparison took {}ms",
t_start.elapsed().as_millis()
);
});
// Stroke changed layer.pixels — invalidate effects backup so
@@ -364,7 +385,10 @@ impl Engine {
let can_reuse = !self.below_cache_dirty
&& self.below_cache_active_idx == Some(active_idx)
&& active_idx > 0
&& self.below_cache.as_ref().map_or(false, |b| b.len() == buf_size);
&& self
.below_cache
.as_ref()
.map_or(false, |b| b.len() == buf_size);
log::trace!(
"[begin_stroke] below_cache state: active_idx={}, reuse={}, dirty={}, existing_cache={}, existing_active_idx={:?}",
@@ -372,7 +396,10 @@ impl Engine {
);
if can_reuse {
log::trace!("[begin_stroke] REUSING below_cache for active_idx={}", active_idx);
log::trace!(
"[begin_stroke] REUSING below_cache for active_idx={}",
active_idx
);
self.below_cache_dirty = false;
return;
}
@@ -384,13 +411,18 @@ impl Engine {
}
let mut tiles_built = 0usize;
for (ti, tl) in self.document.layers.iter().enumerate() {
if ti >= active_idx { break; }
if ti >= active_idx {
break;
}
if !tl.pixels.is_empty() && self.tile_layers[ti].is_none() {
self.tile_layers[ti] = Some(TiledLayer::from_dense(
&tl.pixels, tl.width, tl.height,
));
self.tile_layers[ti] =
Some(TiledLayer::from_dense(&tl.pixels, tl.width, tl.height));
tiles_built += 1;
log::trace!("[begin_stroke] built tile for below layer[{}] id={}", ti, tl.id);
log::trace!(
"[begin_stroke] built tile for below layer[{}] id={}",
ti,
tl.id
);
}
}
let mut cache = match self.below_cache.take() {
@@ -402,7 +434,10 @@ impl Engine {
};
let tile_slice_len = active_idx.min(self.tile_layers.len());
let visible_below: Vec<(usize, bool)> = self.document.layers[..active_idx]
.iter().enumerate().map(|(i, l)| (i, l.visible)).collect();
.iter()
.enumerate()
.map(|(i, l)| (i, l.visible))
.collect();
log::trace!(
"[begin_stroke] compositing below_cache: {} below_layers, tile_slice_len={}, visible_below={:?}, tiles_built={}",
active_idx, tile_slice_len, visible_below, tiles_built
@@ -410,13 +445,20 @@ impl Engine {
tiled::composite_tiled_into(
&self.document.layers[..active_idx],
&self.tile_layers[..tile_slice_len],
cw, ch, 0, 0, cw, ch,
cw,
ch,
0,
0,
cw,
ch,
&mut cache,
);
let non_zero = cache.iter().filter(|&&b| b != 0).count();
log::trace!(
"[begin_stroke] below_cache built: {} bytes, non-zero bytes={}, active_idx={}",
cache.len(), non_zero, active_idx
cache.len(),
non_zero,
active_idx
);
self.below_cache = Some(cache);
self.below_cache_active_idx = Some(active_idx);