BIG REFACTOR GPT SOL

feat: Refactor Iced panel adapter and introduce build metadata management

- Updated Cargo.toml files across multiple crates to use workspace versioning.
- Enhanced the `iced-panel-adapter` to include a new `plain_slider` module and updated widget rendering to support theme colors.
- Added new theme color utilities for recessed and elevated surfaces in `iced-panel-adapter`.
- Introduced a new `hcie-build-info` crate to manage build metadata, including a build ID system.
- Created a build script to synchronize build IDs across the workspace.
- Added a Makefile for simplified build commands for the Iced application.
- Implemented regression tests for vector shape creation history in the engine API.
- Added a new script for managing Cargo commands with synchronized build ID increments.
- Updated line count report to reflect recent changes in codebase.
- Created a visual plan document for future improvements in the Iced history and panel systems.
This commit is contained in:
2026-07-21 04:25:23 +03:00
parent 325bd0d8e8
commit b49106dc1d
60 changed files with 2716 additions and 1266 deletions
+236 -65
View File
@@ -100,7 +100,11 @@ fn load_colors() -> PersistedColors {
.unwrap_or_default(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => PersistedColors::default(),
Err(e) => {
log::warn!("Failed to read persisted colors at {}: {}", path.display(), e);
log::warn!(
"Failed to read persisted colors at {}: {}",
path.display(),
e
);
PersistedColors::default()
}
}
@@ -171,8 +175,8 @@ pub struct HcieIcedApp {
pub active_dialog: ActiveDialog,
/// Index of the active tool slot in the sidebar (for sub-tools).
pub active_tool_slot: usize,
/// Whether the sub-tools popup is open for the active slot.
pub sub_tools_open: bool,
/// Slot whose expanded sub-tool chooser remains open until toggled again.
pub open_subtool_slot: Option<usize>,
/// Per-slot last-used sub-tool index (egui's `slot_last_used`). The sidebar
/// shows the last-used tool's icon/label for each slot rather than always
/// the primary tool.
@@ -323,11 +327,18 @@ pub struct HcieIcedApp {
pub show_dock_profile_dialog: bool,
/// Current UI language for internationalization.
pub _language: i18n::Language,
/// Active Brushes & Tips category filter retained while the panel redraws.
pub brush_category: crate::panels::brushes::BrushCategory,
/// Context menu screen position (x, y). None means closed.
pub canvas_context_menu: Option<(f32, f32)>,
/// Dirty flag set whenever fg/bg/recent colors change, so colors can be
/// persisted to disk (egui persists these under the `hcie_colors` key).
pub colors_dirty: bool,
/// True while the user is dragging on the color wheel; shape sync and
/// composite refresh are deferred until the drag ends to avoid per-move lag.
pub color_dragging: bool,
/// Final color sampled during the active wheel drag; committed to Recent on mouse-up.
pub pending_drag_color: Option<[u8; 4]>,
/// Layer-bound snapshot of vector shapes before editing (for exact Cancel restoration).
pub vector_shapes_snapshot: Option<crate::vector_edit::VectorEditSnapshot>,
/// Cached `(kind, svg)` pairs from the engine's shape catalog for the active document.
@@ -567,6 +578,8 @@ pub struct ToolState {
pub last_update_instant: std::time::Instant,
/// Imported brush presets from ABR/KPP files.
pub imported_brushes: Vec<hcie_engine_api::BrushPreset>,
/// Complete preset currently supplying non-style brush-tip parameters.
pub active_brush_preset: Option<hcie_engine_api::BrushPreset>,
/// True if currently holding shift to resize brush
pub is_resizing_brush: bool,
/// Starting brush size before resize drag
@@ -596,6 +609,7 @@ impl Default for ToolState {
last_composite_refresh: std::time::Instant::now(),
last_update_instant: std::time::Instant::now(),
imported_brushes: Vec::new(),
active_brush_preset: None,
is_resizing_brush: false,
brush_resize_start_size: 20.0,
brush_resize_start_pos: None,
@@ -621,6 +635,10 @@ pub enum Message {
// ── Color ───────────────────────────────────────────
FgColorChanged([u8; 4]),
BgColorChanged([u8; 4]),
/// Signals the start of a color-picker drag with the initial color.
ColorDragStarted([u8; 4]),
/// Signals the end of a color-picker drag; triggers deferred shape sync.
ColorDragEnded,
SwapColors,
ResetColors,
ColorTabChanged(usize),
@@ -719,6 +737,8 @@ pub enum Message {
// ── Brushes ─────────────────────────────────────────
BrushStyleSelected(BrushStyle),
BrushPresetSelected(hcie_engine_api::BrushPreset),
BrushCategorySelected(crate::panels::brushes::BrushCategory),
BrushSizeChanged(f32),
BrushSizeRelative(f32),
BrushOpacityChanged(f32),
@@ -1258,6 +1278,79 @@ fn build_brush_tip(state: &ToolState) -> BrushTip {
}
}
/// Converts an imported/default brush-engine preset tip into the protocol tip used by `Engine`.
///
/// **Arguments:** `source` is the preset-owned brush-engine tip. **Returns:** A protocol tip with
/// all shared rendering fields copied and protocol-only time dynamics left at defaults.
/// **Side Effects / Dependencies:** None.
fn protocol_tip_from_preset(source: &hcie_engine_api::brush::BrushTip) -> BrushTip {
let mut tip = BrushTip::default();
tip.style = protocol_style_from_preset(source.style);
tip.size = source.size;
tip.opacity = source.opacity;
tip.hardness = source.hardness;
tip.spacing = source.spacing;
tip.flow = source.flow;
tip.jitter_amount = source.jitter_amount;
tip.scatter_amount = source.scatter_amount;
tip.angle = source.angle;
tip.roundness = source.roundness;
tip.spray_particle_size = source.spray_particle_size;
tip.spray_density = source.spray_density;
tip.bitmap_pixels = source.bitmap_pixels.clone();
tip.bitmap_w = source.bitmap_w;
tip.bitmap_h = source.bitmap_h;
tip.color_variant = source.color_variant;
tip.variant_amount = source.variant_amount;
tip.density = source.density;
tip.drawing_angle = source.drawing_angle;
tip.rotation_random = source.rotation_random;
tip
}
/// Maps the brush-engine preset enum to the protocol enum used by GUI tool state.
///
/// **Arguments:** `style` is a serialized preset style. **Returns:** Its protocol equivalent.
/// **Side Effects / Dependencies:** None.
fn protocol_style_from_preset(style: hcie_engine_api::brush::BrushStyle) -> BrushStyle {
use hcie_engine_api::brush::BrushStyle as PresetStyle;
match style {
PresetStyle::Round => BrushStyle::Round,
PresetStyle::Square => BrushStyle::Square,
PresetStyle::HardRound => BrushStyle::HardRound,
PresetStyle::SoftRound => BrushStyle::SoftRound,
PresetStyle::Star => BrushStyle::Star,
PresetStyle::Noise => BrushStyle::Noise,
PresetStyle::Texture => BrushStyle::Texture,
PresetStyle::Spray => BrushStyle::Spray,
PresetStyle::Pencil => BrushStyle::Pencil,
PresetStyle::Pen => BrushStyle::Pen,
PresetStyle::Calligraphy => BrushStyle::Calligraphy,
PresetStyle::Oil => BrushStyle::Oil,
PresetStyle::Charcoal => BrushStyle::Charcoal,
PresetStyle::Leaf => BrushStyle::Leaf,
PresetStyle::Rock => BrushStyle::Rock,
PresetStyle::Meadow => BrushStyle::Meadow,
PresetStyle::Wood => BrushStyle::Wood,
PresetStyle::Watercolor => BrushStyle::Watercolor,
PresetStyle::Marker => BrushStyle::Marker,
PresetStyle::Sketch => BrushStyle::Sketch,
PresetStyle::Hatch => BrushStyle::Hatch,
PresetStyle::Glow => BrushStyle::Glow,
PresetStyle::Airbrush => BrushStyle::Airbrush,
PresetStyle::Crayon => BrushStyle::Crayon,
PresetStyle::WetPaint => BrushStyle::WetPaint,
PresetStyle::InkPen => BrushStyle::InkPen,
PresetStyle::Clouds => BrushStyle::Clouds,
PresetStyle::Dirt => BrushStyle::Dirt,
PresetStyle::Tree => BrushStyle::Tree,
PresetStyle::Bristle => BrushStyle::Bristle,
PresetStyle::Mixer => BrushStyle::Mixer,
PresetStyle::Blender => BrushStyle::Blender,
PresetStyle::Bitmap => BrushStyle::Bitmap,
}
}
/// Convert a display label back to a PaneType for dock profile loading.
fn pane_type_from_label(label: &str) -> Option<crate::dock::state::PaneType> {
use crate::dock::state::PaneType;
@@ -1605,9 +1698,9 @@ impl HcieIcedApp {
active_dialog: ActiveDialog::None,
active_tool_slot: crate::sidebar::slot_for_tool(hcie_engine_api::Tool::Brush)
.unwrap_or(0),
sub_tools_open: false,
open_subtool_slot: None,
sidebar_expanded: settings.panel_layout.sidebar_expanded,
slot_last_used: vec![0; 20],
slot_last_used: vec![0; 21],
_dialog_input: String::new(),
_dialog_input2: String::new(),
_show_performance: false,
@@ -1692,7 +1785,10 @@ impl HcieIcedApp {
dock_profile_rename_buf: String::new(),
show_dock_profile_dialog: false,
_language: i18n::Language::default(),
brush_category: crate::panels::brushes::BrushCategory::All,
colors_dirty: false,
color_dragging: false,
pending_drag_color: None,
vector_shapes_snapshot: None,
cached_custom_shapes: Vec::new(),
vector_drag_last: None,
@@ -2109,10 +2205,19 @@ impl HcieIcedApp {
}
/// Add a color to the recent colors list (most recent first, max 40).
///
/// Immediately persists to disk so committed colors survive a crash or non-graceful
/// shutdown. Color-wheel movement never calls this method; the final sampled color is
/// committed once by `ColorDragEnded`.
fn add_recent_color(&mut self, color: [u8; 4]) {
self.recent_colors.retain(|c| *c != color);
self.recent_colors.insert(0, color);
self.recent_colors.truncate(40);
// Write immediately unless we are in the middle of a colour-wheel
// drag (where callers are responsible for the final flush).
if !self.color_dragging {
save_colors(self.fg_color, self.bg_color, &self.recent_colors);
}
}
/// Refresh the composite buffer using incremental dirty-region compositing.
@@ -2287,7 +2392,17 @@ impl HcieIcedApp {
/// Called before starting a stroke to ensure the engine uses
/// the current brush size, opacity, hardness, and style.
fn apply_brush_params(&mut self) {
let mut tip = build_brush_tip(&self.tool_state);
let mut tip = self
.tool_state
.active_brush_preset
.as_ref()
.map(|preset| protocol_tip_from_preset(&preset.tip))
.unwrap_or_else(|| build_brush_tip(&self.tool_state));
tip.style = self.tool_state.brush_style;
tip.size = self.tool_state.brush_size;
tip.opacity = self.tool_state.brush_opacity;
tip.hardness = self.tool_state.brush_hardness;
tip.spacing = self.tool_state.brush_spacing;
match self.tool_state.active_tool {
Tool::Pen => {
tip.style = BrushStyle::Round;
@@ -2354,7 +2469,6 @@ impl HcieIcedApp {
Message::ToolSelected(tool) => {
log::info!("Tool selected: {:?}", tool);
self.tool_state.active_tool = tool;
self.sub_tools_open = false;
if tool == Tool::Brush {
self.dock.reopen_pane(crate::dock::state::PaneType::Brushes);
}
@@ -2386,6 +2500,7 @@ impl HcieIcedApp {
}
Message::ToolSlotClicked(slot_idx, primary_tool) => {
let same_slot = self.active_tool_slot == slot_idx;
self.active_tool_slot = slot_idx;
let tool = crate::sidebar::last_used_tool(slot_idx, &self.slot_last_used);
let selected_tool = if crate::sidebar::tool_slots().get(slot_idx).is_some() {
@@ -2395,18 +2510,30 @@ impl HcieIcedApp {
};
self.tool_state.active_tool = selected_tool;
if selected_tool == Tool::Brush {
self.dock
.reopen_pane(crate::dock::state::PaneType::Brushes);
self.dock.reopen_pane(crate::dock::state::PaneType::Brushes);
}
let supports_popup = crate::sidebar::tool_slots()
.get(slot_idx)
.is_some_and(|slot| slot.tools.len() > 1);
if same_slot && supports_popup {
self.open_subtool_slot = if self.open_subtool_slot == Some(slot_idx) {
None
} else {
Some(slot_idx)
};
}
if matches!(selected_tool, Tool::CustomShape(_)) {
self.dock
.reopen_pane(crate::dock::state::PaneType::CustomShapes);
}
self.sub_tools_open = false;
}
Message::SubtoolsToggle(slot_idx) => {
let supports_popup = crate::sidebar::tool_slots()
.get(slot_idx)
.is_some_and(|slot| slot.tools.len() > 1);
if supports_popup {
let opening = !(self.sub_tools_open && self.active_tool_slot == slot_idx);
self.sub_tools_open = opening;
let opening = self.open_subtool_slot != Some(slot_idx);
self.open_subtool_slot = opening.then_some(slot_idx);
self.active_tool_slot = slot_idx;
if opening && !self.sidebar_expanded {
self.sidebar_expanded = true;
@@ -2414,13 +2541,14 @@ impl HcieIcedApp {
self.settings.panel_layout.sidebar_width = 72.0;
let _ = self.settings.save();
}
// If the user is opening the tool slot popup, also reveal the
// Brushes panel when the slot's current tool is the brush.
if opening {
let tool = crate::sidebar::last_used_tool(slot_idx, &self.slot_last_used);
if tool == Tool::Brush {
self.dock.reopen_pane(crate::dock::state::PaneType::Brushes);
}
if matches!(tool, Tool::CustomShape(_)) {
self.dock
.reopen_pane(crate::dock::state::PaneType::Brushes);
.reopen_pane(crate::dock::state::PaneType::CustomShapes);
}
}
}
@@ -2429,25 +2557,45 @@ impl HcieIcedApp {
if self.dock.active_auto_hide.take().is_some() {
return Task::none();
}
if self.sub_tools_open {
self.sub_tools_open = false;
} else {
self.active_menu = None;
}
self.active_menu = None;
}
Message::FgColorChanged(color) => {
self.fg_color = color;
self.active_document_mut().engine.set_color(color);
self.add_recent_color(color);
self.colors_dirty = true;
crate::shape_sync::sync_shape_from_tool(self);
self.refresh_composite_if_needed();
if self.color_dragging {
self.pending_drag_color = Some(color);
} else {
self.add_recent_color(color);
self.colors_dirty = true;
crate::shape_sync::sync_shape_from_tool(self);
self.refresh_composite_if_needed();
}
}
Message::BgColorChanged(color) => {
self.bg_color = color;
self.add_recent_color(color);
self.colors_dirty = true;
if !self.color_dragging {
crate::shape_sync::sync_shape_from_tool(self);
self.refresh_composite_if_needed();
}
}
Message::ColorDragStarted(initial_color) => {
self.color_dragging = true;
self.pending_drag_color = Some(initial_color);
self.fg_color = initial_color;
self.active_document_mut().engine.set_color(initial_color);
}
Message::ColorDragEnded => {
self.color_dragging = false;
if let Some(final_color) = self.pending_drag_color.take() {
self.add_recent_color(final_color);
}
self.colors_dirty = false;
crate::shape_sync::sync_shape_from_tool(self);
self.refresh_composite_if_needed();
}
@@ -2674,6 +2822,8 @@ impl HcieIcedApp {
];
self.fg_color = color;
self.documents[self.active_doc].engine.set_color(color);
self.add_recent_color(color);
self.colors_dirty = true;
}
}
}
@@ -2940,6 +3090,8 @@ impl HcieIcedApp {
];
self.fg_color = color;
self.documents[self.active_doc].engine.set_color(color);
self.add_recent_color(color);
self.colors_dirty = true;
}
}
} else if self.tool_state.is_resizing_brush {
@@ -4479,6 +4631,21 @@ impl HcieIcedApp {
Message::BrushStyleSelected(style) => {
log::info!("Brush style selected: {:?}", style);
self.tool_state.brush_style = style;
self.tool_state.active_brush_preset = None;
}
Message::BrushPresetSelected(preset) => {
log::info!("Brush preset selected: {}", preset.name);
self.tool_state.brush_style = protocol_style_from_preset(preset.style);
self.tool_state.brush_size = preset.tip.size;
self.tool_state.brush_opacity = preset.tip.opacity;
self.tool_state.brush_hardness = preset.tip.hardness;
self.tool_state.brush_spacing = preset.tip.spacing;
self.tool_state.active_brush_preset = Some(preset);
self.settings.update_from_tool_state(&self.tool_state);
let _ = self.settings.save();
}
Message::BrushCategorySelected(category) => {
self.brush_category = category;
}
Message::BrushSizeChanged(size) => {
self.tool_state.brush_size = size;
@@ -5576,22 +5743,22 @@ impl HcieIcedApp {
// Use cached catalog snapshot if available; otherwise read once from the
// active document's shape catalog. `cached_custom_shapes` is populated/updated
// by `RefreshCustomShapes` so drawing remains consistent with the panel.
let custom_shapes: Vec<(String, String)> = if self.cached_custom_shapes.is_empty()
{
self.documents
.get(self.active_doc)
.map(|d| {
d.engine
.shape_catalog
.all()
.iter()
.map(|e| (e.kind.clone(), e.svg.clone()))
.collect()
})
.unwrap_or_default()
} else {
self.cached_custom_shapes.clone()
};
let custom_shapes: Vec<(String, String)> =
if self.cached_custom_shapes.is_empty() {
self.documents
.get(self.active_doc)
.map(|d| {
d.engine
.shape_catalog
.all()
.iter()
.map(|e| (e.kind.clone(), e.svg.clone()))
.collect()
})
.unwrap_or_default()
} else {
self.cached_custom_shapes.clone()
};
let engine = &mut self.documents[self.active_doc].engine;
let color = self.fg_color;
@@ -6298,9 +6465,11 @@ impl HcieIcedApp {
.active_vector_shapes()
.map_or(0, |s| s.len());
if idx + 1 < count {
self.documents[self.active_doc]
.engine
.reorder_vector_shape(layer_id, idx, idx + 1);
self.documents[self.active_doc].engine.reorder_vector_shape(
layer_id,
idx,
idx + 1,
);
self.documents[self.active_doc].selected_vector_shape = Some(idx + 1);
self.documents[self.active_doc].modified = true;
self.documents[self.active_doc]
@@ -6316,9 +6485,11 @@ impl HcieIcedApp {
if let Some(idx) = self.documents[self.active_doc].selected_vector_shape {
if idx > 0 {
let layer_id = self.documents[self.active_doc].engine.active_layer_id();
self.documents[self.active_doc]
.engine
.reorder_vector_shape(layer_id, idx, idx - 1);
self.documents[self.active_doc].engine.reorder_vector_shape(
layer_id,
idx,
idx - 1,
);
self.documents[self.active_doc].selected_vector_shape = Some(idx - 1);
self.documents[self.active_doc].modified = true;
self.documents[self.active_doc]
@@ -6601,11 +6772,13 @@ impl HcieIcedApp {
.engine
.set_vector_shape_fill_color(layer_id, idx, c);
self.documents[self.active_doc].modified = true;
crate::shape_sync::sync_tool_from_shape(self);
self.documents[self.active_doc]
.engine
.mark_composite_dirty();
self.refresh_composite_if_needed();
if !self.color_dragging {
crate::shape_sync::sync_tool_from_shape(self);
self.documents[self.active_doc]
.engine
.mark_composite_dirty();
self.refresh_composite_if_needed();
}
}
}
Message::GeometryStrokeColorChanged(c) => {
@@ -6615,11 +6788,13 @@ impl HcieIcedApp {
.engine
.set_vector_shape_color(layer_id, idx, c);
self.documents[self.active_doc].modified = true;
crate::shape_sync::sync_tool_from_shape(self);
self.documents[self.active_doc]
.engine
.mark_composite_dirty();
self.refresh_composite_if_needed();
if !self.color_dragging {
crate::shape_sync::sync_tool_from_shape(self);
self.documents[self.active_doc]
.engine
.mark_composite_dirty();
self.refresh_composite_if_needed();
}
}
}
Message::GeometryOpacityChanged(v) => {
@@ -7962,7 +8137,6 @@ impl HcieIcedApp {
// ── Menu ─────────────────────────────────────
Message::MenuOpen(idx) => {
self.sub_tools_open = false;
if self.active_menu == Some(idx) {
self.active_menu = None;
} else {
@@ -8929,11 +9103,8 @@ impl HcieIcedApp {
}
// Mirrors egui's Escape handling. Priority: viewer > crop >
// transform > vector selection > selection > dialog > viewer fallback.
match overlay_escape_target(self.sub_tools_open, self.active_menu.is_some()) {
Some(OverlayEscapeTarget::Subtools) => {
self.sub_tools_open = false;
return Task::none();
}
match overlay_escape_target(false, self.active_menu.is_some()) {
Some(OverlayEscapeTarget::Subtools) => unreachable!(),
Some(OverlayEscapeTarget::Menu) => {
self.active_menu = None;
return Task::none();
@@ -8944,7 +9115,7 @@ impl HcieIcedApp {
return self.update(Message::ViewerToggle);
} else if matches!(self.active_dialog, ActiveDialog::SvgEditor) {
return self.update(Message::SvgEditorCancel);
} else if self.sub_tools_open || self.active_menu.is_some() {
} else if self.active_menu.is_some() {
return Task::none();
} else if self.documents[self.active_doc].crop_state.active {
return self.update(Message::CropCancel);
@@ -9059,8 +9230,7 @@ impl HcieIcedApp {
&self.fg_color,
&self.bg_color,
colors,
self.active_tool_slot,
self.sub_tools_open,
self.open_subtool_slot,
&self.slot_last_used,
self.sidebar_expanded,
);
@@ -9226,6 +9396,7 @@ impl HcieIcedApp {
self.theme_state.colors(),
Some(&self.dock),
self.settings.window.width,
self.settings.window.height,
) {
stack = stack.push(menu_overlay);
}