Files
oak-editor/examples/screenshot.rs
T
Mike-Solar f0517fe6af feat(app): UI walkthrough pass — faithful screenshots, dock labels, timeline clips, status bar, meter
- screenshot example inits i18n and captures both zh-CN and en-US
  (docs/screenshot-window{,-en}.png)
- dock tabs size to content (no more 64px truncation); viewer/panel
  titles follow the design (素材查看器·name etc.)
- mock project carries V1/V2 video + A1/A2 audio clips rendered as
  rounded green bars; timeline clip geometry/rounded corners match the
  design; status bar visible with full content; audio meter strip
  docked at the program viewer's right edge; project explorer rows
  have icons
- tests/waveform_e2e.rs: waveform cache extracts real peaks and hits
  cache on re-query (P4 acceptance)
2026-08-13 23:37:57 +08:00

174 lines
6.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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/>.
//! Offscreen screenshot capture for the app window.
//!
//! Renders the full [`OakApp`] shell at 1600×900 (2× = 3200×1866 px) in an
//! offscreen macOS window and writes the PNGs to
//! `docs/screenshot-window.png` (zh-CN) and `docs/screenshot-window-en.png`
//! (en-US), using the same [`VisualTestAppContext`] machinery the gpui visual
//! tests use. The window is created at `(-10000, -10000)` so nothing
//! flickers on screen.
//!
//! Unlike the real app's startup ([`oakapp::app::run`]) the example must
//! initialize the i18n layer itself, or every menu renders in the en-US
//! fallback while the panels fall back to their built-in Chinese defaults.
//! The active language is captured first and restored at the end, so the
//! persisted preference is untouched.
//!
//! Run it on the macOS main thread (examples run on the main thread, unlike
//! `#[test]` harness threads):
//!
//! ```text
//! cargo run --example screenshot # 1600×900 → both PNGs
//! cargo run --example screenshot -- 1100 900 # any size (same filenames)
//! ```
use gpui::{px, size, AnyWindowHandle, AppContext, Result, VisualTestAppContext};
use gpui_platform::current_platform;
use oakapp::app::OakApp;
use oakapp::i18n::{self, Language};
use oakapp::oakui::MockEngine;
const DEFAULT_WIDTH: f32 = 1600.0;
const DEFAULT_HEIGHT: f32 = 900.0;
const OUT_ZH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/docs/screenshot-window.png");
const OUT_EN: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/docs/screenshot-window-en.png");
/// Logical y of the timeline toolbar row, which sits at the top of the
/// bottom dock panel in the default layout: the dock starts at y 27.5 (the
/// menu bar height), the viewers get 60% of the remaining height, and a 6px
/// split handle separates them from the timeline. The toolbar is its 31px
/// first row. The assertion scans a small band around it so minor layout
/// drift does not false-negative.
const TOOLBAR_Y: f32 = 541.0;
const TOOLBAR_BAND: f32 = 44.0;
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let width = args
.first()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_WIDTH);
let height = args
.get(1)
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_HEIGHT);
let mut cx = VisualTestAppContext::new(current_platform(false));
cx.update(|app| app.init_colors());
// Initialize the UI language like the real app's startup would: zh-CN
// for the primary screenshot, then en-US for the English one. The
// original persisted language is restored at the end.
let original = i18n::language();
i18n::set_language(Language::ZhCN);
{
let handle = open_shell(&mut cx, width, height);
let image = cx.capture_screenshot(handle)?;
std::fs::create_dir_all(std::path::Path::new(OUT_ZH).parent().unwrap())?;
image.save(OUT_ZH)?;
println!("wrote {OUT_ZH} ({}×{})", image.width(), image.height());
assert_toolbar(&image, "zh-CN");
}
i18n::set_language(Language::EnUs);
{
let handle = open_shell(&mut cx, width, height);
let image = cx.capture_screenshot(handle)?;
std::fs::create_dir_all(std::path::Path::new(OUT_EN).parent().unwrap())?;
image.save(OUT_EN)?;
println!("wrote {OUT_EN} ({}×{})", image.width(), image.height());
assert_toolbar(&image, "en-US");
}
i18n::set_language(original);
Ok(())
}
/// Opens the app shell offscreen and draws 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.
fn open_shell(
cx: &mut VisualTestAppContext,
width: f32,
height: f32,
) -> AnyWindowHandle {
let window = cx
.open_offscreen_window(size(px(width), px(height)), |window, cx| {
// Compact pro-app text metrics, matching the real app's startup
// (`src/app.rs run_with` sets rem 14px; gpui's default is 16px).
window.set_rem_size(px(14.0));
cx.new(|cx| OakApp::<MockEngine>::new(window, None, cx))
})
.expect("offscreen window opens");
let handle: AnyWindowHandle = window.into();
for _ in 0..16 {
cx.run_until_parked();
cx.update_window(handle, |_root, window, app| {
let _ = window.draw(app);
})
.expect("window still open");
}
cx.run_until_parked();
for _ in 0..16 {
cx.run_until_parked();
cx.update_window(handle, |_root, window, app| {
let _ = window.draw(app);
})
.expect("window still open");
}
cx.run_until_parked();
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 tool cells for bright glyph pixels, so a broken icon load fails
/// the capture loudly instead of shipping an empty toolbar.
fn assert_toolbar(image: &image::RgbaImage, language: &str) {
// The image is 2× the logical size; convert logical → pixel y. TOOLBAR_Y
// is measured from the window's top edge.
let mut rendered = 0usize;
for (index, cell_x) in [12u32, 44, 76, 108, 140, 172, 204, 236].iter().enumerate() {
let mut bright = 0u32;
for dy in 0..(TOOLBAR_BAND as i32 * 2) {
for dx in 0..40i32 {
let x = (*cell_x as i32 + dx) as u32;
let y = ((TOOLBAR_Y as i32 - TOOLBAR_BAND as i32 / 2) * 2 + 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] {language} toolbar tool {index} bright pixels: {bright}");
if bright > 20 {
rendered += 1;
}
}
assert!(
rendered >= 6,
"{language} timeline toolbar icons did not render (only {rendered}/8 tool cells had pixels)"
);
}