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