// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
//! Shared text layout/rasterization backend hooks (C++
//! `src/node/src/generator/text/textbackend.h`, header-only — no class,
//! just POD structs and global function-pointer hooks in `olive::`).
//!
//! FONT/RASTER BACKEND DEPENDENCY — DELIBERATELY UNDECIDED:
//! The C++ text nodes do NOT link any font or raster library directly.
//! Text layout and rasterization were historically done with Qt's rich
//! text stack (`QTextDocument` / `QTextOption` /
//! `QAbstractTextDocumentLayout`) rasterized via `QPainter` into a
//! `QImage` (`Format_Grayscale8` for v1/v2 coverage buffers,
//! `Format_RGBA8888_Premultiplied` for v3). That Qt dependency has
//! already been removed from oaknode: the C++ now carries only the POD
//! job descriptions below plus two installable backend hooks
//! (`g_text_measure_backend` / `g_text_render_backend`) that the
//! facade/app layer fills in at runtime. No Rust font/shaping/raster
//! crate (freetype-rs, rusttype, cosmic-text, swash, etc.) is chosen
//! here on purpose; the backend decision belongs to the facade layer
//! that will install these hooks.
/// POD description of a text layout/rasterization job (C++
/// `TextLayoutRequest`). Replaces the `QTextDocument`/`QTextOption`
/// usage in the text generator nodes; this module carries only the
/// data.
pub struct TextLayoutRequest {
/// Plain text or markup, per `mode`.
pub text: String,
/// Layout mode (C++ `TextLayoutRequest::Mode`).
pub mode: TextLayoutMode,
/// Default font family (empty = backend default).
pub font_family: String,
/// Default font size in points (0 = default).
pub font_size_pt: f64,
/// Paint device resolution (0 = backend default).
pub dots_per_meter: i32,
/// Wrap width (C++ `QTextDocument::setTextWidth()`).
pub wrap_width: f64,
/// Center horizontally by default (C++ default
/// `QTextOption(Qt::AlignCenter)`).
pub center_horizontally: bool,
// Note: the backends always paint with white as the default text
// color (formerly PaintContext palette QPalette::Text = Qt::white);
// HTML markup may override it per span.
}
/// Layout mode (C++ `TextLayoutRequest::Mode`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextLayoutMode {
/// Plain text (C++ `QTextDocument::setPlainText()`).
PlainText,
/// HTML subset (C++ `QTextDocument::setHtml()`).
Html,
/// Olive's `Html::html_to_doc()` rich text.
OliveHtml,
}
/// Laid-out document size (C++ `TextLayoutSize`, formerly
/// `QTextDocument::size()`).
#[derive(Clone, Copy, Debug, Default)]
pub struct TextLayoutSize {
/// Document width in pixels.
pub width: f64,
/// Document height in pixels.
pub height: f64,
}
/// Target pixel buffer for text rasterization (C++
/// `TextRenderTarget`). `channel_count == 1` means 8-bit grayscale
/// coverage (formerly `QImage::Format_Grayscale8`, the node tints it
/// afterwards); `channel_count == 4` means 8-bit RGBA premultiplied
/// drawn over the existing (cleared) buffer content (formerly
/// `QImage::Format_RGBA8888_Premultiplied`).
pub struct TextRenderTarget<'a> {
/// Pixel buffer, `linesize_bytes * height` bytes.
pub data: &'a mut [u8],
/// Buffer width in pixels.
pub width: i32,
/// Buffer height in pixels.
pub height: i32,
/// Bytes per scanline.
pub linesize_bytes: i32,
/// Channel count: 1 = grayscale coverage, 4 = RGBA premultiplied.
pub channel_count: i32,
}
/// Draw transform, mirroring the original `QPainter` calls (C++
/// `TextRenderTransform`). The painter first applied
/// `scale(scale, scale)`, then translated to the base offset, set the
/// clip rect `(0, 0, clip_width, clip_height)` at that base offset, and
/// finally translated by the remaining draw offset before drawing. A
/// point `p` of the laid-out document therefore lands at
/// `((p.x + draw_offset_x) * scale, (p.y + draw_offset_y) * scale)`,
/// clipped to `((clip_offset_*) * scale, (clip_size) * scale)` relative
/// to the same origin. `clip_enabled == false` means no clip.
#[derive(Clone, Copy, Debug, Default)]
pub struct TextRenderTransform {
/// Uniform scale (C++ `QPainter::scale(scale, scale)`).
pub scale: f64,
/// Draw offset X, in pre-scale document coordinates.
pub draw_offset_x: f64,
/// Draw offset Y, in pre-scale document coordinates.
pub draw_offset_y: f64,
/// Whether the clip rect is active.
pub clip_enabled: bool,
/// Clip rect offset X, in pre-scale document coordinates.
pub clip_offset_x: f64,
/// Clip rect offset Y, in pre-scale document coordinates.
pub clip_offset_y: f64,
/// Clip rect width, in pre-scale document coordinates.
pub clip_width: f64,
/// Clip rect height, in pre-scale document coordinates.
pub clip_height: f64,
}
/// Measure hook (C++ `TextMeasureBackend`): lays out the request and
/// returns the document size (formerly `QTextDocument::size()`).
pub type TextMeasureBackend = fn(&TextLayoutRequest) -> TextLayoutSize;
/// Render hook (C++ `TextRenderBackend`): draws the request into the
/// target buffer (formerly `QAbstractTextDocumentLayout::draw()`).
pub type TextRenderBackend =
for<'a> fn(&TextLayoutRequest, &TextRenderTransform, TextRenderTarget<'a>);
/// Install the backend hooks (C++ `set_text_backends()`). When a hook
/// is `None` the caller falls back to a zero size / leaves the (already
/// cleared) buffer untouched — a documented behavior gap until the
/// facade installs a text engine. Setting a hook to `None` uninstalls
/// it (the C++ global is a plain function pointer, assignable any
/// number of times; a `Mutex` keeps the tests able to reset it).
pub fn set_text_backends(measure: Option, render: Option) {
*MEASURE.lock().unwrap() = measure;
*RENDER.lock().unwrap() = render;
}
/// Currently installed measure hook (C++ `text_measure_backend()`).
pub fn text_measure_backend() -> Option {
*MEASURE.lock().unwrap()
}
/// Currently installed render hook (C++ `text_render_backend()`).
pub fn text_render_backend() -> Option {
*RENDER.lock().unwrap()
}
/// Installed measure hook (C++ global `g_text_measure_backend`).
static MEASURE: std::sync::Mutex