a2846d8b3a
- 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.
539 lines
19 KiB
Rust
539 lines
19 KiB
Rust
//! C FFI layer for HCIE Engine.
|
|
//!
|
|
//! Exposes `extern "C"` functions that the Qt6 GUI (or any foreign language)
|
|
//! can call through a C-compatible ABI. Each function wraps a single Engine
|
|
//! method call.
|
|
//!
|
|
//! RULE: This module is LOCKED after creation. Do not modify without unlock.sh.
|
|
|
|
use std::ffi::CStr;
|
|
use std::os::raw::c_char;
|
|
|
|
use crate::Engine;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Handle type
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/// Opaque handle wrapping a boxed Engine. The GUI stores this pointer and
|
|
/// passes it to every FFI call.
|
|
#[repr(C)]
|
|
pub struct EngineHandle {
|
|
engine: Box<Engine>,
|
|
}
|
|
|
|
/// Helper to extract a safe mutable reference from the opaque handle.
|
|
///
|
|
/// # Safety
|
|
/// The caller must ensure `handle` is a valid pointer obtained from
|
|
/// `hcie_engine_create`.
|
|
unsafe fn get_engine<'a>(handle: *mut EngineHandle) -> Option<&'a mut Engine> {
|
|
if handle.is_null() {
|
|
return None;
|
|
}
|
|
Some(&mut (*handle).engine)
|
|
}
|
|
|
|
/// Helper to extract the engine and execute a closure, returning the closure's result.
|
|
///
|
|
/// # Safety
|
|
/// Same invariants as `get_engine`.
|
|
unsafe fn with_engine<F, R>(handle: *mut EngineHandle, f: F) -> Option<R>
|
|
where
|
|
F: FnOnce(&mut Engine) -> R,
|
|
{
|
|
get_engine(handle).map(f)
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Document lifecycle
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/// Create a new engine instance with a transparent canvas of the given size.
|
|
///
|
|
/// Returns a non-null handle on success, null on allocation failure.
|
|
#[no_mangle]
|
|
pub extern "C" fn hcie_engine_create(w: u32, h: u32) -> *mut EngineHandle {
|
|
let engine = Engine::new(w, h);
|
|
let handle = Box::new(EngineHandle {
|
|
engine: Box::new(engine),
|
|
});
|
|
Box::into_raw(handle)
|
|
}
|
|
|
|
/// Destroy an engine instance and free its memory.
|
|
///
|
|
/// # Safety
|
|
/// `handle` must be a valid pointer from `hcie_engine_create` that has not
|
|
/// already been destroyed. Passing null is a no-op.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_destroy(handle: *mut EngineHandle) {
|
|
if !handle.is_null() {
|
|
drop(Box::from_raw(handle));
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Canvas queries
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/// Get canvas width in pixels. Returns 0 if handle is null.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_canvas_width(handle: *mut EngineHandle) -> u32 {
|
|
with_engine(handle, |e| e.canvas_width()).unwrap_or(0)
|
|
}
|
|
|
|
/// Get canvas height in pixels. Returns 0 if handle is null.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_canvas_height(handle: *mut EngineHandle) -> u32 {
|
|
with_engine(handle, |e| e.canvas_height()).unwrap_or(0)
|
|
}
|
|
|
|
/// Copy the composite RGBA pixel buffer into `out_buf`.
|
|
///
|
|
/// `out_len` must be at least `w * h * 4` bytes. Returns the number of bytes
|
|
/// written, or 0 on failure.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_composite_pixels(
|
|
handle: *mut EngineHandle,
|
|
out_buf: *mut u8,
|
|
out_len: usize,
|
|
) -> i32 {
|
|
with_engine(handle, |e| {
|
|
let pixels = e.get_composite_pixels();
|
|
let copy_len = pixels.len().min(out_len);
|
|
std::ptr::copy_nonoverlapping(pixels.as_ptr(), out_buf, copy_len);
|
|
copy_len as i32
|
|
})
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Layer management
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/// Add a new raster layer with the given name. Returns the layer ID.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_add_layer(
|
|
handle: *mut EngineHandle,
|
|
name: *const c_char,
|
|
) -> u64 {
|
|
let name_str = if name.is_null() {
|
|
"Layer".to_string()
|
|
} else {
|
|
CStr::from_ptr(name).to_string_lossy().into_owned()
|
|
};
|
|
with_engine(handle, |e| e.add_layer(&name_str)).unwrap_or(0)
|
|
}
|
|
|
|
/// Delete a layer by ID.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_delete_layer(handle: *mut EngineHandle, id: u64) {
|
|
with_engine(handle, |e| e.delete_layer(id));
|
|
}
|
|
|
|
/// Set the active (selected) layer by ID.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_set_active_layer(handle: *mut EngineHandle, id: u64) {
|
|
with_engine(handle, |e| e.set_active_layer(id));
|
|
}
|
|
|
|
/// Get the active layer ID. Returns 0 if no active layer.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_active_layer_id(handle: *mut EngineHandle) -> u64 {
|
|
with_engine(handle, |e| e.active_layer_id()).unwrap_or(0)
|
|
}
|
|
|
|
/// Get the number of layers.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_layer_count(handle: *mut EngineHandle) -> usize {
|
|
with_engine(handle, |e| e.get_layer_count()).unwrap_or(0)
|
|
}
|
|
|
|
/// Set layer visibility.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_set_layer_visible(
|
|
handle: *mut EngineHandle,
|
|
id: u64,
|
|
visible: bool,
|
|
) {
|
|
with_engine(handle, |e| e.set_layer_visible(id, visible));
|
|
}
|
|
|
|
/// Set layer opacity (0.0 - 1.0).
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_set_layer_opacity(
|
|
handle: *mut EngineHandle,
|
|
id: u64,
|
|
opacity: f32,
|
|
) {
|
|
with_engine(handle, |e| e.set_layer_opacity(id, opacity));
|
|
}
|
|
|
|
/// Set layer blend mode (encoded as integer index).
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_set_layer_blend_mode(
|
|
handle: *mut EngineHandle,
|
|
id: u64,
|
|
mode: i32,
|
|
) {
|
|
with_engine(handle, |e| {
|
|
let modes = crate::BlendMode::ALL;
|
|
if (mode as usize) < modes.len() {
|
|
e.set_layer_blend_mode(id, modes[mode as usize]);
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Set layer locked state.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_set_layer_locked(
|
|
handle: *mut EngineHandle,
|
|
id: u64,
|
|
locked: bool,
|
|
) {
|
|
with_engine(handle, |e| e.set_layer_locked(id, locked));
|
|
}
|
|
|
|
/// Move layer to a new position in the stack.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_move_layer(
|
|
handle: *mut EngineHandle,
|
|
id: u64,
|
|
new_index: i32,
|
|
) {
|
|
with_engine(handle, |e| e.move_layer(id, new_index as usize));
|
|
}
|
|
|
|
/// Merge the active layer down into the layer below.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_merge_down(handle: *mut EngineHandle) {
|
|
with_engine(handle, |e| {
|
|
e.merge_down();
|
|
});
|
|
}
|
|
|
|
/// Flatten all layers into a single background.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_flatten_image(handle: *mut EngineHandle) {
|
|
with_engine(handle, |e| {
|
|
e.flatten_image();
|
|
});
|
|
}
|
|
|
|
/// Clear the active layer's pixels (make transparent).
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_clear_active_layer(handle: *mut EngineHandle) {
|
|
with_engine(handle, |e| {
|
|
e.clear_active_layer_pixels_undoable();
|
|
});
|
|
}
|
|
|
|
/// Rename a layer.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_rename_layer(
|
|
handle: *mut EngineHandle,
|
|
id: u64,
|
|
name: *const c_char,
|
|
) {
|
|
if name.is_null() {
|
|
return;
|
|
}
|
|
let name_str = CStr::from_ptr(name).to_string_lossy().into_owned();
|
|
with_engine(handle, |e| {
|
|
e.rename_layer(id, &name_str);
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Drawing (stroke API)
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/// Begin a new brush stroke at the given canvas position.
|
|
///
|
|
/// This snapshots the layer pixels for undo and initializes stroke caches.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_begin_stroke(
|
|
handle: *mut EngineHandle,
|
|
x: f32,
|
|
y: f32,
|
|
_pressure: f32,
|
|
) {
|
|
with_engine(handle, |e| {
|
|
let layer_id = e.active_layer_id();
|
|
e.begin_stroke(layer_id, x, y);
|
|
});
|
|
}
|
|
|
|
/// Continue a brush stroke to the given position with pressure.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_stroke_to(
|
|
handle: *mut EngineHandle,
|
|
x: f32,
|
|
y: f32,
|
|
pressure: f32,
|
|
) {
|
|
with_engine(handle, |e| {
|
|
let layer_id = e.active_layer_id();
|
|
e.stroke_to(layer_id, x, y, pressure);
|
|
});
|
|
}
|
|
|
|
/// End the current brush stroke and commit an undo snapshot.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_end_stroke(handle: *mut EngineHandle) {
|
|
with_engine(handle, |e| {
|
|
let layer_id = e.active_layer_id();
|
|
e.end_stroke(layer_id);
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Selection
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_select_all(handle: *mut EngineHandle) {
|
|
with_engine(handle, |e| {
|
|
e.selection_all();
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_deselect(handle: *mut EngineHandle) {
|
|
with_engine(handle, |e| {
|
|
e.selection_clear();
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_invert_selection(handle: *mut EngineHandle) {
|
|
with_engine(handle, |e| {
|
|
e.selection_invert();
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_grow_selection(handle: *mut EngineHandle, amount: u32) {
|
|
with_engine(handle, |e| {
|
|
e.selection_grow(amount);
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_shrink_selection(handle: *mut EngineHandle, amount: u32) {
|
|
with_engine(handle, |e| {
|
|
e.selection_shrink(amount);
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_feather_selection(handle: *mut EngineHandle, amount: u32) {
|
|
with_engine(handle, |e| {
|
|
e.selection_feather(amount as f32);
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_erode_selection(handle: *mut EngineHandle, _amount: u32) {
|
|
// Erode is shrink + feather combined
|
|
with_engine(handle, |e| {
|
|
e.selection_shrink(_amount);
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_fade_selection(handle: *mut EngineHandle, amount: u32) {
|
|
with_engine(handle, |e| {
|
|
e.selection_feather(amount as f32);
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Filters
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/// Apply a named filter with JSON parameters.
|
|
///
|
|
/// `filter_name` and `params_json` must be null-terminated C strings.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_apply_filter(
|
|
handle: *mut EngineHandle,
|
|
filter_name: *const c_char,
|
|
params_json: *const c_char,
|
|
) {
|
|
if filter_name.is_null() {
|
|
return;
|
|
}
|
|
let name = CStr::from_ptr(filter_name).to_string_lossy().into_owned();
|
|
let params_str = if params_json.is_null() {
|
|
"{}".to_string()
|
|
} else {
|
|
CStr::from_ptr(params_json).to_string_lossy().into_owned()
|
|
};
|
|
let params: serde_json::Value =
|
|
serde_json::from_str(¶ms_str).unwrap_or(serde_json::json!({}));
|
|
|
|
// Internal bridge commands used by the Qt6 frontend to set brush / color
|
|
// through the single FFI entry point until dedicated functions are added.
|
|
with_engine(handle, |e| match name.as_str() {
|
|
"__internal_set_brush" => {
|
|
let mut tip = e.current_tip.clone();
|
|
if let Some(sz) = params["size"].as_f64() {
|
|
tip.size = sz as f32;
|
|
}
|
|
if let Some(o) = params["opacity"].as_f64() {
|
|
tip.opacity = o as f32;
|
|
}
|
|
e.set_brush_tip(tip);
|
|
}
|
|
"__internal_set_color" => {
|
|
if let Some(hex) = params["hex"].as_str() {
|
|
let hex = hex.trim_start_matches('#');
|
|
if hex.len() >= 6 {
|
|
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
|
|
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
|
|
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
|
|
let a = if hex.len() >= 8 {
|
|
u8::from_str_radix(&hex[6..8], 16).unwrap_or(255)
|
|
} else {
|
|
255
|
|
};
|
|
e.set_color([r, g, b, a]);
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
e.apply_filter(&name, params);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// History (undo/redo)
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_undo(handle: *mut EngineHandle) {
|
|
with_engine(handle, |e| {
|
|
e.undo();
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_redo(handle: *mut EngineHandle) {
|
|
with_engine(handle, |e| {
|
|
e.redo();
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_can_undo(handle: *mut EngineHandle) -> bool {
|
|
with_engine(handle, |e| e.can_undo()).unwrap_or(false)
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_can_redo(handle: *mut EngineHandle) -> bool {
|
|
with_engine(handle, |e| e.can_redo()).unwrap_or(false)
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_history_current(handle: *mut EngineHandle) -> i32 {
|
|
with_engine(handle, |e| e.history_current()).unwrap_or(-1)
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_history_len(handle: *mut EngineHandle) -> usize {
|
|
with_engine(handle, |e| e.history_len()).unwrap_or(0)
|
|
}
|
|
|
|
/// Jump to a specific history step by index. Pass -1 for original image.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_jump_to_history(handle: *mut EngineHandle, idx: i32) {
|
|
with_engine(handle, |e| {
|
|
e.jump_to_history(idx);
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Transform
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_rotate_canvas(handle: *mut EngineHandle, degrees: f32) {
|
|
with_engine(handle, |e| {
|
|
let layer_id = e.active_layer_id();
|
|
e.rotate_layer(layer_id, degrees);
|
|
});
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_flip_canvas(handle: *mut EngineHandle, horizontal: bool) {
|
|
with_engine(handle, |e| {
|
|
let layer_id = e.active_layer_id();
|
|
if horizontal {
|
|
e.flip_layer_horizontal(layer_id);
|
|
} else {
|
|
e.flip_layer_vertical(layer_id);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Tool
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/// Set the active drawing tool by numeric ID.
|
|
///
|
|
/// Maps to `hcie_protocol::tools::Tool` discriminant.
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_set_active_tool(handle: *mut EngineHandle, tool_id: i32) {
|
|
with_engine(handle, |e| {
|
|
use crate::Tool;
|
|
let tool = match tool_id {
|
|
0 => Tool::Eyedropper,
|
|
1 => Tool::Pen,
|
|
2 => Tool::Brush,
|
|
3 => Tool::Eraser,
|
|
4 => Tool::Spray,
|
|
5 => Tool::FloodFill,
|
|
6 => Tool::MagicWand,
|
|
7 => Tool::Select,
|
|
8 => Tool::Lasso,
|
|
9 => Tool::PolygonSelect,
|
|
10 => Tool::Move,
|
|
11 => Tool::Crop,
|
|
12 => Tool::Text,
|
|
13 => Tool::Gradient,
|
|
14 => Tool::VectorSelect,
|
|
15 => Tool::VectorLine,
|
|
16 => Tool::VectorRect,
|
|
17 => Tool::VectorCircle,
|
|
18 => Tool::RedEyeRemoval,
|
|
19 => Tool::SpotRemoval,
|
|
20 => Tool::SmartPatch,
|
|
21 => Tool::AiObjectRemoval,
|
|
22 => Tool::SmartSelect,
|
|
23 => Tool::VisionSelect,
|
|
_ => Tool::Brush,
|
|
};
|
|
e.set_tool(tool);
|
|
});
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// Modified flag
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_is_modified(handle: *mut EngineHandle) -> bool {
|
|
with_engine(handle, |e| e.is_modified()).unwrap_or(false)
|
|
}
|
|
|
|
#[no_mangle]
|
|
pub unsafe extern "C" fn hcie_engine_set_modified(handle: *mut EngineHandle, modified: bool) {
|
|
with_engine(handle, |e| {
|
|
e.set_modified(modified);
|
|
});
|
|
}
|