refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit: - root Cargo.toml = oakapp bin + workspace; one cargo build produces oakapp, oak-cli, oak-worker, liboakengine.dylib - app/rust/src -> src/ (app at repo root, no rust/ nesting) - src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore; src/bindings/oakotio -> crates/oakotio; src/engine/rust -> crates/oakengine (keeps cdylib+staticlib+rlib) - public C headers include/<mod>/ -> crates/oakengine/include/<mod>/ - OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone) - legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/, app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets submodule, otio-install, all build-* output (~40GB) - oakstorage kept but excluded from the workspace (skeleton w/ todos); gpui excluded (own workspace) - verified: cargo build green, cargo test --workspace 1845/0 (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! 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<TextMeasureBackend>,
|
||||
render: Option<TextRenderBackend>,
|
||||
) {
|
||||
*MEASURE.lock().unwrap() = measure;
|
||||
*RENDER.lock().unwrap() = render;
|
||||
}
|
||||
|
||||
/// Currently installed measure hook (C++ `text_measure_backend()`).
|
||||
pub fn text_measure_backend() -> Option<TextMeasureBackend> {
|
||||
*MEASURE.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Currently installed render hook (C++ `text_render_backend()`).
|
||||
pub fn text_render_backend() -> Option<TextRenderBackend> {
|
||||
*RENDER.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Installed measure hook (C++ global `g_text_measure_backend`).
|
||||
static MEASURE: std::sync::Mutex<Option<TextMeasureBackend>> = std::sync::Mutex::new(None);
|
||||
|
||||
/// Installed render hook (C++ global `g_text_render_backend`).
|
||||
static RENDER: std::sync::Mutex<Option<TextRenderBackend>> = std::sync::Mutex::new(None);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// The two tests below share the process-global backend statics; a
|
||||
// lock serializes them so `backend_hooks_default_none` cannot observe
|
||||
// the hooks installed by `backend_hooks_install_and_query`.
|
||||
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
#[test]
|
||||
fn backend_hooks_default_none() {
|
||||
let _guard = LOCK.lock().unwrap();
|
||||
set_text_backends(None, None);
|
||||
assert_eq!(text_measure_backend(), None);
|
||||
assert_eq!(text_render_backend(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_hooks_install_and_query() {
|
||||
let _guard = LOCK.lock().unwrap();
|
||||
fn measure(_r: &TextLayoutRequest) -> TextLayoutSize {
|
||||
TextLayoutSize {
|
||||
width: 12.0,
|
||||
height: 34.0,
|
||||
}
|
||||
}
|
||||
fn render(_r: &TextLayoutRequest, _t: &TextRenderTransform, mut target: TextRenderTarget) {
|
||||
for b in target.data.iter_mut() {
|
||||
*b = 255;
|
||||
}
|
||||
}
|
||||
set_text_backends(Some(measure), Some(render));
|
||||
assert_eq!(text_measure_backend().unwrap()(&TextLayoutRequest {
|
||||
text: String::new(),
|
||||
mode: TextLayoutMode::PlainText,
|
||||
font_family: String::new(),
|
||||
font_size_pt: 0.0,
|
||||
dots_per_meter: 0,
|
||||
wrap_width: 0.0,
|
||||
center_horizontally: false,
|
||||
}).width, 12.0);
|
||||
assert_eq!(
|
||||
text_measure_backend().unwrap()(&TextLayoutRequest {
|
||||
text: String::new(),
|
||||
mode: TextLayoutMode::Html,
|
||||
font_family: String::new(),
|
||||
font_size_pt: 0.0,
|
||||
dots_per_meter: 0,
|
||||
wrap_width: 0.0,
|
||||
center_horizontally: false,
|
||||
})
|
||||
.height,
|
||||
34.0
|
||||
);
|
||||
let mut buf = vec![0u8; 4];
|
||||
text_render_backend().unwrap()(
|
||||
&TextLayoutRequest {
|
||||
text: String::new(),
|
||||
mode: TextLayoutMode::PlainText,
|
||||
font_family: String::new(),
|
||||
font_size_pt: 0.0,
|
||||
dots_per_meter: 0,
|
||||
wrap_width: 0.0,
|
||||
center_horizontally: false,
|
||||
},
|
||||
&TextRenderTransform::default(),
|
||||
TextRenderTarget {
|
||||
data: &mut buf,
|
||||
width: 2,
|
||||
height: 2,
|
||||
linesize_bytes: 2,
|
||||
channel_count: 1,
|
||||
},
|
||||
);
|
||||
assert_eq!(buf, vec![255u8; 4]);
|
||||
// Restore the uninstalled state so other tests observe the
|
||||
// no-backend fallback.
|
||||
set_text_backends(None, None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user