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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user