mirror of
https://github.com/penpot/penpot.git
synced 2026-11-07 10:30:40 +00:00
Retain legal line starts at the original wrap width before allowing an indivisible punctuation or annotation unit to overflow together. Share the layout plan and alignment offset across painting, editing and exports. Cover mixed styles, nonbreaking controls, graphemes, annotations, alignment, strokes and resize. All 666 renderer tests and both WASM builds pass. AI-assisted-by: GPT-6
269 lines
7.5 KiB
Rust
269 lines
7.5 KiB
Rust
use macros::wasm_error;
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
use crate::emscripten::init_gl;
|
|
|
|
use crate::mem;
|
|
use crate::render::{gpu_state::GpuState, RenderResources, RenderState};
|
|
use crate::state::{State, TextEditorState, UIState};
|
|
|
|
static mut DESIGN_STATE: *mut State = std::ptr::null_mut();
|
|
static mut GPU_STATE: *mut GpuState = std::ptr::null_mut();
|
|
static mut RENDER_STATE: *mut RenderState = std::ptr::null_mut();
|
|
static mut UI_STATE: *mut UIState = std::ptr::null_mut();
|
|
static mut TEXT_EDITOR_STATE: *mut TextEditorState = std::ptr::null_mut();
|
|
static mut RENDER_RESOURCES: *mut RenderResources = std::ptr::null_mut();
|
|
|
|
pub(crate) fn get_design_state() -> &'static mut State {
|
|
unsafe {
|
|
debug_assert!(!DESIGN_STATE.is_null(), "Design State is null");
|
|
&mut *DESIGN_STATE
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn get_gpu_state() -> &'static mut GpuState {
|
|
unsafe {
|
|
assert!(
|
|
!GPU_STATE.is_null(),
|
|
"GPU State is null (headless instance?)"
|
|
);
|
|
&mut *GPU_STATE
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn get_render_state() -> &'static mut RenderState {
|
|
unsafe {
|
|
debug_assert!(!RENDER_STATE.is_null(), "Render State is null");
|
|
&mut *RENDER_STATE
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn current_browser() -> u8 {
|
|
unsafe {
|
|
if DESIGN_STATE.is_null() {
|
|
0
|
|
} else {
|
|
(*DESIGN_STATE).current_browser
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn has_render_state() -> bool {
|
|
unsafe { !RENDER_STATE.is_null() }
|
|
}
|
|
|
|
/// The render resources in use: in tests, the ones this thread installed with
|
|
/// [`TestRenderResourcesGuard`], else the global ones.
|
|
#[inline(always)]
|
|
fn render_resources() -> *mut RenderResources {
|
|
#[cfg(test)]
|
|
{
|
|
let installed = TEST_RENDER_RESOURCES.with(std::cell::Cell::get);
|
|
if !installed.is_null() {
|
|
return installed;
|
|
}
|
|
}
|
|
unsafe { RENDER_RESOURCES }
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn has_render_resources() -> bool {
|
|
!render_resources().is_null()
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn get_resources() -> &'static mut RenderResources {
|
|
let resources = render_resources();
|
|
debug_assert!(!resources.is_null(), "Render Resources is null");
|
|
unsafe { &mut *resources }
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn get_text_editor_state() -> &'static mut TextEditorState {
|
|
unsafe {
|
|
debug_assert!(!TEXT_EDITOR_STATE.is_null(), "Text Editor state is null");
|
|
&mut *TEXT_EDITOR_STATE
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub(crate) fn get_ui_state() -> &'static mut UIState {
|
|
unsafe {
|
|
debug_assert!(!UI_STATE.is_null(), "UI State is null");
|
|
&mut *UI_STATE
|
|
}
|
|
}
|
|
|
|
// FIXME: These with_state* macros should be using our CriticalError instead of expect.
|
|
// But to do that, we need to not use them at domain-level (i.e. in business logic), just
|
|
// in the context of the wasm call.
|
|
#[macro_export]
|
|
macro_rules! with_state {
|
|
($state:ident, $block:block) => {{
|
|
use $crate::globals::get_design_state;
|
|
let $state = get_design_state();
|
|
$block
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! with_current_shape_mut {
|
|
($state:ident, |$shape:ident: &mut Shape| $block:block) => {
|
|
use $crate::globals::get_design_state;
|
|
let $state = get_design_state();
|
|
$state.touch_current();
|
|
if let Some($shape) = $state.current_shape_mut() {
|
|
$block
|
|
}
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! with_current_shape {
|
|
($state:ident, |$shape:ident: &Shape| $block:block) => {
|
|
use $crate::globals::get_design_state;
|
|
let $state = get_design_state();
|
|
if let Some($shape) = $state.current_shape() {
|
|
$block
|
|
}
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
thread_local! {
|
|
/// Render resources a test installed for its own thread. Tests run on
|
|
/// parallel threads, and Skia font collections are not thread-safe, so
|
|
/// another test must never reach them.
|
|
static TEST_RENDER_RESOURCES: std::cell::Cell<*mut RenderResources> =
|
|
const { std::cell::Cell::new(std::ptr::null_mut()) };
|
|
}
|
|
|
|
/// Scoped override of the render resources for the current test thread.
|
|
#[cfg(test)]
|
|
pub(crate) struct TestRenderResourcesGuard {
|
|
prev: *mut RenderResources,
|
|
_lock: std::sync::MutexGuard<'static, ()>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
static TEST_RENDER_RESOURCES_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
|
|
|
#[cfg(test)]
|
|
impl TestRenderResourcesGuard {
|
|
pub(crate) fn install(resources: &mut RenderResources) -> Self {
|
|
let lock = TEST_RENDER_RESOURCES_LOCK
|
|
.lock()
|
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
let prev = TEST_RENDER_RESOURCES.replace(resources as *mut _);
|
|
Self { prev, _lock: lock }
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
impl Drop for TestRenderResourcesGuard {
|
|
fn drop(&mut self) {
|
|
TEST_RENDER_RESOURCES.set(self.prev);
|
|
}
|
|
}
|
|
|
|
/// Initializes GPUState.
|
|
fn gpu_init() {
|
|
unsafe {
|
|
let gpu_state = GpuState::try_new().expect("Cannot initialize GPU State");
|
|
GPU_STATE = Box::into_raw(Box::new(gpu_state));
|
|
}
|
|
}
|
|
|
|
/// Initializes RenderState.
|
|
fn render_init(width: i32, height: i32) {
|
|
unsafe {
|
|
let render_state =
|
|
RenderState::try_new(width, height).expect("Cannot initialize RenderState");
|
|
RENDER_STATE = Box::into_raw(Box::new(render_state));
|
|
}
|
|
}
|
|
|
|
/// Initializes the interactive RenderResources (GPU image store).
|
|
fn resources_init() {
|
|
unsafe {
|
|
let resources = RenderResources::try_new().expect("Cannot initialize RenderResources");
|
|
RENDER_RESOURCES = Box::into_raw(Box::new(resources));
|
|
}
|
|
}
|
|
|
|
/// Initializes GPU-free RenderResources for the headless export path.
|
|
fn resources_init_headless() {
|
|
unsafe {
|
|
let resources = RenderResources::try_new_headless()
|
|
.expect("Cannot initialize headless RenderResources");
|
|
RENDER_RESOURCES = Box::into_raw(Box::new(resources));
|
|
}
|
|
}
|
|
|
|
/// Initializes DesignState.
|
|
pub(crate) fn design_init() {
|
|
unsafe {
|
|
let design_state = State::new();
|
|
DESIGN_STATE = Box::into_raw(Box::new(design_state));
|
|
}
|
|
}
|
|
|
|
/// Initializes TextEditorState.
|
|
fn text_editor_init() {
|
|
unsafe {
|
|
let text_editor_state = TextEditorState::new();
|
|
TEXT_EDITOR_STATE = Box::into_raw(Box::new(text_editor_state));
|
|
}
|
|
}
|
|
|
|
/// Initializes UIState.
|
|
fn ui_init() {
|
|
unsafe {
|
|
let ui_state = UIState::new();
|
|
UI_STATE = Box::into_raw(Box::new(ui_state));
|
|
}
|
|
}
|
|
|
|
#[no_mangle]
|
|
#[wasm_error]
|
|
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
|
#[cfg(target_arch = "wasm32")]
|
|
init_gl!();
|
|
gpu_init();
|
|
resources_init();
|
|
render_init(width, height);
|
|
text_editor_init();
|
|
design_init();
|
|
ui_init();
|
|
Ok(())
|
|
}
|
|
|
|
/// Boots the engine for headless export with no GPU/WebGL context: only
|
|
/// `RenderResources` (GPU-free) + the design state. The export (raster/PDF)
|
|
/// paths render onto their own surface; the interactive render loop is not
|
|
/// available on an instance initialized this way. `width`/`height` are kept for
|
|
/// API symmetry with `init` but unused — the export sizes from shape bounds.
|
|
#[no_mangle]
|
|
#[wasm_error]
|
|
pub extern "C" fn init_headless(_width: i32, _height: i32) -> Result<()> {
|
|
resources_init_headless();
|
|
design_init();
|
|
Ok(())
|
|
}
|
|
|
|
#[no_mangle]
|
|
#[wasm_error]
|
|
pub extern "C" fn clean_up() -> Result<()> {
|
|
// Cancel the current animation frame if it exists so
|
|
// it won't try to render without context
|
|
let render_state = get_render_state();
|
|
render_state.prepare_context_loss_cleanup();
|
|
unsafe { DESIGN_STATE = std::ptr::null_mut() }
|
|
mem::free_bytes()?;
|
|
Ok(())
|
|
}
|