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
+160 -54
View File
@@ -2,7 +2,7 @@
use eframe::egui;
use eframe::egui::Color32;
use egui::ColorImage;
use hcie_brush_engine::{BrushStyle, presets::BrushPreset};
use hcie_brush_engine::{presets::BrushPreset, BrushStyle};
// ── tunables ─────────────────────────────────────────────────────────────────
const TILE_SIZE: u32 = 512;
@@ -23,38 +23,55 @@ struct SliderCaps {
fn slider_caps(style: BrushStyle) -> SliderCaps {
match style {
BrushStyle::Calligraphy => SliderCaps {
angle: true, roundness: true, jitter: false, scatter: false,
spray_particle_size: false, spray_density: false,
angle: true,
roundness: true,
jitter: false,
scatter: false,
spray_particle_size: false,
spray_density: false,
..SliderCaps::common()
},
BrushStyle::Pencil | BrushStyle::InkPen => SliderCaps {
jitter: true, ..SliderCaps::common()
jitter: true,
..SliderCaps::common()
},
BrushStyle::Spray | BrushStyle::Airbrush => SliderCaps {
jitter: true, scatter: true,
spray_particle_size: true, spray_density: true,
jitter: true,
scatter: true,
spray_particle_size: true,
spray_density: true,
..SliderCaps::common()
},
BrushStyle::Charcoal => SliderCaps {
jitter: true, scatter: true, ..SliderCaps::common()
jitter: true,
scatter: true,
..SliderCaps::common()
},
BrushStyle::Crayon => SliderCaps {
jitter: true, scatter: true, ..SliderCaps::common()
jitter: true,
scatter: true,
..SliderCaps::common()
},
BrushStyle::Tree => SliderCaps {
jitter: true, angle: true, ..SliderCaps::common()
jitter: true,
angle: true,
..SliderCaps::common()
},
BrushStyle::Meadow | BrushStyle::Rock | BrushStyle::Dirt => SliderCaps {
jitter: true, ..SliderCaps::common()
jitter: true,
..SliderCaps::common()
},
BrushStyle::Bristle => SliderCaps {
roundness: true, ..SliderCaps::common()
roundness: true,
..SliderCaps::common()
},
BrushStyle::HardRound | BrushStyle::SoftRound => SliderCaps {
hardness: false, ..SliderCaps::common()
hardness: false,
..SliderCaps::common()
},
BrushStyle::Mixer | BrushStyle::Blender => SliderCaps {
flow: false, ..SliderCaps::common()
flow: false,
..SliderCaps::common()
},
_ => SliderCaps::common(),
}
@@ -63,20 +80,32 @@ fn slider_caps(style: BrushStyle) -> SliderCaps {
impl SliderCaps {
fn common() -> Self {
Self {
hardness: true, opacity: true, spacing: true, flow: true,
jitter: false, scatter: false, angle: false, roundness: false,
spray_particle_size: false, spray_density: false,
hardness: true,
opacity: true,
spacing: true,
flow: true,
jitter: false,
scatter: false,
angle: false,
roundness: false,
spray_particle_size: false,
spray_density: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum CanvasPreset { S512, S1024, S2048, S4096 }
enum CanvasPreset {
S512,
S1024,
S2048,
S4096,
}
impl CanvasPreset {
fn size(self) -> (u32, u32) {
match self {
Self::S512 => (512, 512),
Self::S512 => (512, 512),
Self::S1024 => (1024, 1024),
Self::S2048 => (2048, 2048),
Self::S4096 => (4096, 4096),
@@ -84,7 +113,7 @@ impl CanvasPreset {
}
fn name(self) -> &'static str {
match self {
Self::S512 => "512",
Self::S512 => "512",
Self::S1024 => "1024",
Self::S2048 => "2048",
Self::S4096 => "4096",
@@ -93,21 +122,26 @@ impl CanvasPreset {
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum FpsCap { Unlimited, Fps30, Fps60, Fps165 }
enum FpsCap {
Unlimited,
Fps30,
Fps60,
Fps165,
}
impl FpsCap {
fn value(self) -> Option<f32> {
match self {
Self::Fps30 => Some(30.0),
Self::Fps60 => Some(60.0),
Self::Fps30 => Some(30.0),
Self::Fps60 => Some(60.0),
Self::Fps165 => Some(165.0),
Self::Unlimited => None,
}
}
fn name(self) -> &'static str {
match self {
Self::Fps30 => "30",
Self::Fps60 => "60",
Self::Fps30 => "30",
Self::Fps60 => "60",
Self::Fps165 => "165",
Self::Unlimited => "",
}
@@ -137,7 +171,12 @@ impl TiledCanvas {
let total = (cols * rows) as usize;
Self {
pixels: vec![255; (w * h * 4) as usize],
tiles: (0..total).map(|_| Tile { texture: None, dirty: true }).collect(),
tiles: (0..total)
.map(|_| Tile {
texture: None,
dirty: true,
})
.collect(),
w,
h,
cols,
@@ -181,18 +220,28 @@ impl TiledCanvas {
break;
}
let e2 = 2.0 * err;
if e2 > -dy { err -= dy; x0 += sx; }
if e2 < dx { err += dx; y0 += sy; }
if e2 > -dy {
err -= dy;
x0 += sx;
}
if e2 < dx {
err += dx;
y0 += sy;
}
}
}
fn mark_all_dirty(&mut self) {
for t in &mut self.tiles { t.dirty = true; }
for t in &mut self.tiles {
t.dirty = true;
}
self.dirty_count = self.tiles.len();
}
fn upload_dirty(&mut self, ctx: &egui::Context) {
if self.dirty_count == 0 { return; }
if self.dirty_count == 0 {
return;
}
let rows = self.h.div_ceil(TILE_SIZE);
for ty in 0..rows {
let tile_h = if ty == rows - 1 {
@@ -202,7 +251,9 @@ impl TiledCanvas {
} as usize;
for tx in 0..self.cols {
let idx = (ty * self.cols + tx) as usize;
if !self.tiles[idx].dirty { continue; }
if !self.tiles[idx].dirty {
continue;
}
self.tiles[idx].dirty = false;
self.dirty_count = self.dirty_count.saturating_sub(1);
@@ -223,10 +274,7 @@ impl TiledCanvas {
.copy_from_slice(&self.pixels[src..src + tile_w * 4]);
}
let img = ColorImage::from_rgba_unmultiplied(
[tile_w, tile_h],
&data,
);
let img = ColorImage::from_rgba_unmultiplied([tile_w, tile_h], &data);
let key = format!("t{}_{}", tx, ty);
if let Some(tex) = &mut self.tiles[idx].texture {
tex.set(img, egui::TextureOptions::NEAREST);
@@ -274,7 +322,9 @@ impl App {
}
fn set_canvas_size(&mut self, p: CanvasPreset) {
if self.canvas_preset == p { return; }
if self.canvas_preset == p {
return;
}
self.canvas_preset = p;
let (w, h) = p.size();
self.canvas = TiledCanvas::new(w, h);
@@ -325,8 +375,16 @@ impl eframe::App for App {
egui::ComboBox::from_id_salt("canvas_preset")
.selected_text(self.canvas_preset.name())
.show_ui(ui, |ui| {
for p in &[CanvasPreset::S512, CanvasPreset::S1024, CanvasPreset::S2048, CanvasPreset::S4096] {
if ui.selectable_label(self.canvas_preset == *p, p.name()).clicked() {
for p in &[
CanvasPreset::S512,
CanvasPreset::S1024,
CanvasPreset::S2048,
CanvasPreset::S4096,
] {
if ui
.selectable_label(self.canvas_preset == *p, p.name())
.clicked()
{
self.set_canvas_size(*p);
}
}
@@ -336,7 +394,12 @@ impl eframe::App for App {
egui::ComboBox::from_id_salt("fps_cap")
.selected_text(self.fps_cap.name())
.show_ui(ui, |ui| {
for c in &[FpsCap::Fps30, FpsCap::Fps60, FpsCap::Fps165, FpsCap::Unlimited] {
for c in &[
FpsCap::Fps30,
FpsCap::Fps60,
FpsCap::Fps165,
FpsCap::Unlimited,
] {
if ui.selectable_label(self.fps_cap == *c, c.name()).clicked() {
self.fps_cap = *c;
}
@@ -355,16 +418,48 @@ impl eframe::App for App {
ui.add(egui::Slider::new(&mut self.preset.tip.size, 1.0..=500.0).text("Size"));
let caps = slider_caps(self.preset.style);
ui.add_enabled(caps.hardness, egui::Slider::new(&mut self.preset.tip.hardness, 0.0..=1.0).text("Hardness"));
ui.add_enabled(caps.opacity, egui::Slider::new(&mut self.preset.tip.opacity, 0.0..=1.0).text("Opacity"));
ui.add_enabled(caps.spacing, egui::Slider::new(&mut self.preset.tip.spacing, 0.01..=1.0).text("Spacing"));
ui.add_enabled(caps.flow, egui::Slider::new(&mut self.preset.tip.flow, 0.01..=1.0).text("Flow"));
ui.add_enabled(caps.jitter, egui::Slider::new(&mut self.preset.tip.jitter_amount, 0.0..=5.0).text("Jitter"));
ui.add_enabled(caps.scatter, egui::Slider::new(&mut self.preset.tip.scatter_amount, 0.0..=1.0).text("Scatter"));
ui.add_enabled(caps.angle, egui::Slider::new(&mut self.preset.tip.angle, 0.0..=360.0).text("Angle"));
ui.add_enabled(caps.roundness, egui::Slider::new(&mut self.preset.tip.roundness, 0.0..=1.0).text("Roundness"));
ui.add_enabled(caps.spray_particle_size, egui::Slider::new(&mut self.preset.tip.spray_particle_size, 0.5..=20.0).text("Spray Size"));
ui.add_enabled(caps.spray_density, egui::Slider::new(&mut self.preset.tip.spray_density, 1..=500).text("Spray Density"));
ui.add_enabled(
caps.hardness,
egui::Slider::new(&mut self.preset.tip.hardness, 0.0..=1.0).text("Hardness"),
);
ui.add_enabled(
caps.opacity,
egui::Slider::new(&mut self.preset.tip.opacity, 0.0..=1.0).text("Opacity"),
);
ui.add_enabled(
caps.spacing,
egui::Slider::new(&mut self.preset.tip.spacing, 0.01..=1.0).text("Spacing"),
);
ui.add_enabled(
caps.flow,
egui::Slider::new(&mut self.preset.tip.flow, 0.01..=1.0).text("Flow"),
);
ui.add_enabled(
caps.jitter,
egui::Slider::new(&mut self.preset.tip.jitter_amount, 0.0..=5.0).text("Jitter"),
);
ui.add_enabled(
caps.scatter,
egui::Slider::new(&mut self.preset.tip.scatter_amount, 0.0..=1.0).text("Scatter"),
);
ui.add_enabled(
caps.angle,
egui::Slider::new(&mut self.preset.tip.angle, 0.0..=360.0).text("Angle"),
);
ui.add_enabled(
caps.roundness,
egui::Slider::new(&mut self.preset.tip.roundness, 0.0..=1.0).text("Roundness"),
);
ui.add_enabled(
caps.spray_particle_size,
egui::Slider::new(&mut self.preset.tip.spray_particle_size, 0.5..=20.0)
.text("Spray Size"),
);
ui.add_enabled(
caps.spray_density,
egui::Slider::new(&mut self.preset.tip.spray_density, 1..=500)
.text("Spray Density"),
);
let mut rgba = [
self.color[0] as f32 / 255.,
@@ -374,8 +469,10 @@ impl eframe::App for App {
];
if ui.color_edit_button_rgba_unmultiplied(&mut rgba).changed() {
self.color = [
(rgba[0] * 255.) as u8, (rgba[1] * 255.) as u8,
(rgba[2] * 255.) as u8, (rgba[3] * 255.) as u8,
(rgba[0] * 255.) as u8,
(rgba[1] * 255.) as u8,
(rgba[2] * 255.) as u8,
(rgba[3] * 255.) as u8,
];
}
@@ -399,7 +496,13 @@ impl eframe::App for App {
ui.allocate_exact_size(egui::vec2(disp_w, disp_h), egui::Sense::drag());
// paint tiles
if self.canvas.tiles.first().and_then(|t| t.texture.as_ref()).is_some() {
if self
.canvas
.tiles
.first()
.and_then(|t| t.texture.as_ref())
.is_some()
{
let rows = self.canvas.h.div_ceil(TILE_SIZE);
for ty in 0..rows {
for tx in 0..self.canvas.cols {
@@ -438,7 +541,8 @@ impl eframe::App for App {
if let Some(pos) = resp.interact_pointer_pos() {
let cx = (pos.x - rect.min.x) / scale;
let cy = (pos.y - rect.min.y) / scale;
if cx >= 0.0 && cy >= 0.0
if cx >= 0.0
&& cy >= 0.0
&& cx < self.canvas.w as f32
&& cy < self.canvas.h as f32
{
@@ -450,8 +554,10 @@ impl eframe::App for App {
};
self.last_pt = Some(pt);
self.canvas.mark_line_dirty(
seg[0].0, seg[0].1,
seg[seg.len() - 1].0, seg[seg.len() - 1].1,
seg[0].0,
seg[0].1,
seg[seg.len() - 1].0,
seg[seg.len() - 1].1,
self.preset.tip.size,
);
hcie_brush_engine::draw_specialized_stroke(