feat(app): link liboakengine.dylib via frozen C ABI; UI fixes

- App no longer depends on the oakengine rlib: build.rs links the
  built liboakengine.dylib (+rpath, -export_dynamic, IOSurface) and
  src/oakui/ffi.rs declares the pure-C surface; RealEngine calls only
  the frozen oakengine_* C ABI
- host_syms.rs provides the oakcore_*/fb_* host symbols the dylib
  imports via dynamic lookup
- Fix Preferences dialog crash (spawn_modal reentrancy) with a
  regression test
- Timeline toolbar and viewer transport render C++-era icons (16px
  grid, dark/light themes) with localized tooltips
- i18n: complete en-US table, add untranslated-key detection test
- New dialogs module (preferences, export, progress)
This commit is contained in:
2026-08-11 00:25:20 +08:00
parent 3110e5cc3a
commit 063837446d
59 changed files with 4998 additions and 517 deletions
+49 -5
View File
@@ -33,6 +33,7 @@
use gpui::{px, size, AnyWindowHandle, AppContext, Result, VisualTestAppContext};
use gpui_platform::current_platform;
use oakapp::app::OakApp;
use oakapp::oakui::MockEngine;
const DEFAULT_WIDTH: f32 = 1600.0;
const DEFAULT_HEIGHT: f32 = 900.0;
@@ -53,14 +54,24 @@ fn main() -> Result<()> {
cx.update(|app| app.init_colors());
let window = cx.open_offscreen_window(size(px(width), px(height)), |window, cx| {
cx.new(|cx| OakApp::new(window, cx))
cx.new(|cx| OakApp::<MockEngine>::new(window, None, cx))
})?;
let handle: AnyWindowHandle = window.into();
// Draw a few frames so the layout settles: the node editor fits its graph
// once the canvas size is known and the viewers upload their first CPU
// frame, both of which happen on the frame after the initial render.
for _ in 0..4 {
// Draw enough frames for the layout to settle and the async toolbar-icon
// assets to decode: the node editor fits its graph once the canvas size
// is known, the viewers upload their first CPU frame, and the PNG
// toolbar icons load through the background executor on the frame after
// the asset future resolves.
for _ in 0..16 {
cx.run_until_parked();
cx.update_window(handle, |_root, window, app| {
let _ = window.draw(app);
})?;
}
cx.run_until_parked();
for _ in 0..16 {
cx.run_until_parked();
cx.update_window(handle, |_root, window, app| {
let _ = window.draw(app);
@@ -69,6 +80,39 @@ fn main() -> Result<()> {
cx.run_until_parked();
let image = cx.capture_screenshot(handle)?;
// The timeline toolbar's tool icons (16px at 2× = 32px on 48px pitch)
// must render: the toolbar is the 31px row at the top of the bottom dock
// panel. Scan the bottom strip for the 8 tool cells and require most of
// them to contain bright glyph pixels, so a broken icon load fails the
// capture loudly instead of shipping an empty toolbar.
let th = height * 2.0;
let mut rendered = 0usize;
for (index, cell_x) in [24u32, 88, 152, 216, 280, 344, 408, 472].iter().enumerate() {
let mut bright = 0u32;
for dy in 0..80i32 {
for dx in 0..32i32 {
let x = (*cell_x as i32 + dx) as u32;
let y = (th as i32 - 320 + dy).max(0) as u32;
if x >= image.width() || y >= image.height() {
continue;
}
let p = image.get_pixel(x, y);
if p[0] > 150 && p[1] > 150 && p[2] > 150 {
bright += 1;
}
}
}
println!("[screenshot] toolbar tool {index} bright pixels: {bright}");
if bright > 20 {
rendered += 1;
}
}
assert!(
rendered >= 6,
"timeline toolbar icons did not render (only {rendered}/8 tool cells had pixels)"
);
std::fs::create_dir_all(std::path::Path::new(OUT).parent().unwrap())?;
image.save(OUT)?;
println!("wrote {OUT} ({}×{})", image.width(), image.height());