A GOOD MILESTONE : Refactor title bar and toolbar typography, improve tooltip styles, and enhance selection transform functionality

- Updated typography constants for menu bar and tooltips to improve readability and consistency.
- Adjusted button padding and widths in the title bar for better layout.
- Replaced tooltip implementation with a new balloon tooltip style for improved visual feedback.
- Enhanced selection transform logic to support rotation and resizing, ensuring accurate hit testing and bounds calculations.
- Added tests for selection clearing and transformed bounds to ensure functionality and prevent regressions.
- Introduced a new module for managing selection transform dirty bounds during interactive previews.
This commit is contained in:
2026-07-18 20:31:31 +03:00
parent b2c88e5471
commit a2846d8b3a
16 changed files with 1361 additions and 526 deletions
+1 -1
View File
@@ -225,7 +225,7 @@ pub unsafe extern "C" fn hcie_engine_flatten_image(handle: *mut EngineHandle) {
#[no_mangle]
pub unsafe extern "C" fn hcie_engine_clear_active_layer(handle: *mut EngineHandle) {
with_engine(handle, |e| {
e.clear_active_layer_pixels();
e.clear_active_layer_pixels_undoable();
});
}
+31
View File
@@ -97,6 +97,11 @@ impl Engine {
self.below_cache_dirty = true;
}
/// Return the index of the currently active layer.
pub fn active_layer_index(&self) -> usize {
self.document.active_layer
}
/// Switch the active (selected) layer by ID.
pub fn set_active_layer(&mut self, id: u64) {
if let Some(idx) = self.document.layer_index_by_id(id) {
@@ -270,6 +275,32 @@ impl Engine {
self.below_cache_dirty = true;
}
/// Push a full-layer pixel snapshot for the currently active layer.
///
/// **Purpose:** Lets callers that already modified the active layer pixels
/// through `set_active_layer_pixels` still record an undo step without
/// needing access to the internal `Document` type.
///
/// **Arguments:**
/// * `before_pixels` — the layer RGBA buffer captured before the change.
/// * `layer_idx` — index of the layer to snapshot (typically the active layer).
/// * `description` — human-readable history entry label.
///
/// **Side Effects:** Appends a `DrawAction` to the document history.
pub fn push_active_layer_snapshot(
&mut self,
before_pixels: Vec<u8>,
layer_idx: usize,
description: &str,
) {
self.document.push_draw_snapshot(
layer_idx,
before_pixels,
None,
description.to_string(),
);
}
pub fn undo(&mut self) -> bool {
if self.document.can_undo() {
self.document.undo();
+56 -4
View File
@@ -3400,11 +3400,32 @@ impl Engine {
}
/// Clear pixels only within the active selection mask. If no selection, clears all.
/// Records a full-layer draw snapshot so the operation is undoable.
pub fn clear_selection_pixels(&mut self) {
let mask = self.cached_selection_mask.clone();
let mask = self.document.selection_mask.clone();
let active_idx = self.document.active_layer;
log::debug!(
"[Engine::clear_selection_pixels] active_layer={}, selection_active={}, document_mask_len={:?}, stroke_cache_mask_len={:?}",
active_idx,
self.document.selection_active,
mask.as_ref().map(Vec::len),
self.cached_selection_mask.as_ref().map(Vec::len),
);
if let Some(ref mask_data) = mask {
let w = self.document.canvas_width as usize;
let h = self.document.canvas_height as usize;
let selected_pixels = mask_data.iter().filter(|coverage| **coverage > 0).count();
log::debug!(
"[Engine::clear_selection_pixels] clearing selected region: canvas={}x{}, selected_pixels={}",
w,
h,
selected_pixels,
);
let before_pixels = self
.document
.active_layer()
.map(|layer| layer.pixels.clone())
.unwrap_or_default();
if let Some(layer) = self.document.active_layer_mut() {
for y in 0..h {
for x in 0..w {
@@ -3418,14 +3439,45 @@ impl Engine {
}
}
layer.dirty = true;
self.document.composite_dirty = true;
self.document.modified = true;
}
self.document.push_draw_snapshot(
active_idx,
before_pixels,
None,
"Clear Selection".to_string(),
);
self.document.composite_dirty = true;
self.document.modified = true;
} else {
self.clear_active_layer_pixels();
log::debug!(
"[Engine::clear_selection_pixels] no active document mask; clearing the entire active layer"
);
self.clear_active_layer_pixels_undoable();
}
}
/// Clears the entire active layer and records an undo snapshot.
pub fn clear_active_layer_pixels_undoable(&mut self) {
let active_idx = self.document.active_layer;
let before_pixels = self
.document
.active_layer()
.map(|layer| layer.pixels.clone())
.unwrap_or_default();
if let Some(layer) = self.document.active_layer_mut() {
layer.pixels.fill(0);
layer.dirty = true;
}
self.document.push_draw_snapshot(
active_idx,
before_pixels,
None,
"Clear Layer".to_string(),
);
self.document.composite_dirty = true;
self.document.modified = true;
}
pub fn set_active_layer_pixels(&mut self, pixels: Vec<u8>) {
if let Some(layer) = self.document.active_layer_mut() {
layer.pixels = pixels;
+54
View File
@@ -0,0 +1,54 @@
//! Selection pixel-clearing regression tests.
//!
//! **Purpose:** Verifies that destructive selection operations use the active
//! document selection mask rather than the brush stroke's temporary mask cache.
//!
//! **Logic & Workflow:** Each test creates an opaque raster layer through the
//! public engine API, installs a deterministic selection mask, clears selected
//! pixels, and compares alpha values inside and outside that mask.
//!
//! **Side Effects / Dependencies:** Uses only in-memory `hcie-engine-api` state;
//! no files, network services, GUI runtime, or engine internals are accessed.
use hcie_engine_api::Engine;
/// Confirms that selection clearing removes internal pixels and preserves all
/// pixels external to the selection.
///
/// **Purpose:** Prevents a regression where `clear_selection_pixels` consulted
/// an empty stroke cache and consequently cleared the complete active layer.
///
/// **Logic & Workflow:** Creates a 3×3 opaque layer, selects only its center
/// pixel, invokes selection clearing, then checks every resulting alpha value.
///
/// **Arguments & Returns:** Takes no arguments and returns nothing; assertion
/// failures report whether an internal or external pixel was handled wrongly.
///
/// **Side Effects / Dependencies:** Mutates only the local in-memory engine and
/// records the normal undo snapshot generated by the clear operation.
#[test]
fn clear_selection_pixels_clears_inside_and_preserves_outside() {
let mut engine = Engine::new(3, 3);
engine.set_active_layer_pixels(vec![255; 3 * 3 * 4]);
let mut mask = vec![0; 3 * 3];
mask[4] = 255;
engine.set_selection_mask(mask);
engine.clear_selection_pixels();
let pixels = engine
.get_active_layer_pixels()
.expect("a new engine must have an active raster layer");
for pixel_index in 0..9 {
let alpha = pixels[pixel_index * 4 + 3];
if pixel_index == 4 {
assert_eq!(alpha, 0, "selected center pixel must be cleared");
} else {
assert_eq!(
alpha, 255,
"unselected pixel {pixel_index} must be preserved"
);
}
}
}