Files
hcie-rust-v3.05/hcie-engine-api/src/ffi.rs
T
phantom a2846d8b3a 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.
2026-07-18 20:31:31 +03:00

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(&params_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);
});
}