refactor(v0.2): вынести headless render в shturman-render (shell использует)
P6.1: общий хелпер render_to_png<C: ComponentHandle>(build, w, h, path) поверх Slint software-renderer (thread_local окно + set_platform once + draw + png). shturman-shell.render_screenshot теперь зовёт его; плумбинг-дубль удалён. png в shell → dev-deps (рендер в render-крейте, тест декодирует). Тесты зелёные. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
Generated
+11
@@ -3589,6 +3589,16 @@ dependencies = [
|
|||||||
"zbus 4.4.0",
|
"zbus 4.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shturman-render"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"png 0.17.16",
|
||||||
|
"slint",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shturman-sdk"
|
name = "shturman-sdk"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
@@ -3624,6 +3634,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"png 0.17.16",
|
"png 0.17.16",
|
||||||
"shturman-common",
|
"shturman-common",
|
||||||
|
"shturman-render",
|
||||||
"shturman-sdk",
|
"shturman-sdk",
|
||||||
"slint",
|
"slint",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ members = [
|
|||||||
"crates/core/shturman-firstboot",
|
"crates/core/shturman-firstboot",
|
||||||
"crates/core/shturman-settings",
|
"crates/core/shturman-settings",
|
||||||
"crates/core/shturman-power",
|
"crates/core/shturman-power",
|
||||||
|
"crates/apps/shturman-render",
|
||||||
"crates/apps/shturman-shell",
|
"crates/apps/shturman-shell",
|
||||||
"crates/tools/shturman-manifest-validator",
|
"crates/tools/shturman-manifest-validator",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "shturman-render"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
slint.workspace = true
|
||||||
|
png = "0.17"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//! Headless software-render Slint-компонента в PNG (без дисплея/композитора).
|
||||||
|
//! Общий для shturman-shell (первый кадр) и shturman-splash (Stage 0). Спека v0.2 §4.1.
|
||||||
|
|
||||||
|
use slint::platform::software_renderer::{MinimalSoftwareWindow, RepaintBufferType};
|
||||||
|
use slint::platform::{Platform, PlatformError, WindowAdapter};
|
||||||
|
use slint::ComponentHandle;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Once;
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static WINDOW: Rc<MinimalSoftwareWindow> =
|
||||||
|
MinimalSoftwareWindow::new(RepaintBufferType::ReusedBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SwPlatform;
|
||||||
|
impl Platform for SwPlatform {
|
||||||
|
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
|
||||||
|
Ok(WINDOW.with(|w| w.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Software-platform ставится один раз на процесс (Slint — глобально). В мультитестовом процессе
|
||||||
|
/// повтор терпим (`Once` + терпимый результат); рендер в проде зовётся раз на процесс (oneshot-сервис).
|
||||||
|
fn ensure_platform() {
|
||||||
|
static ONCE: Once = Once::new();
|
||||||
|
ONCE.call_once(|| {
|
||||||
|
let _ = slint::platform::set_platform(Box::new(SwPlatform));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Построить компонент (ВНУТРИ — после `set_platform`) и отрендерить его кадр в PNG.
|
||||||
|
/// `build` зовётся после установки software-platform (порядок обязателен для Slint).
|
||||||
|
pub fn render_to_png<C: ComponentHandle>(
|
||||||
|
build: impl FnOnce() -> anyhow::Result<C>,
|
||||||
|
w: u32,
|
||||||
|
h: u32,
|
||||||
|
path: &Path,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
ensure_platform();
|
||||||
|
let ui = build()?;
|
||||||
|
let window = WINDOW.with(|x| x.clone());
|
||||||
|
window.set_size(slint::PhysicalSize::new(w, h));
|
||||||
|
ui.show()?;
|
||||||
|
ui.window().request_redraw(); // форсим перерисовку (повторный рендер в том же потоке)
|
||||||
|
let mut buf = vec![slint::Rgb8Pixel { r: 0, g: 0, b: 0 }; (w * h) as usize];
|
||||||
|
let drawn = window.draw_if_needed(|r| {
|
||||||
|
r.render(buf.as_mut_slice(), w as usize);
|
||||||
|
});
|
||||||
|
ui.hide()?; // освободить окно для следующего рендера в том же потоке
|
||||||
|
if !drawn {
|
||||||
|
anyhow::bail!("software-renderer не отрисовал кадр");
|
||||||
|
}
|
||||||
|
write_png(path, w, h, &buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_png(path: &Path, w: u32, h: u32, buf: &[slint::Rgb8Pixel]) -> anyhow::Result<()> {
|
||||||
|
let mut enc = png::Encoder::new(std::io::BufWriter::new(std::fs::File::create(path)?), w, h);
|
||||||
|
enc.set_color(png::ColorType::Rgb);
|
||||||
|
enc.set_depth(png::BitDepth::Eight);
|
||||||
|
let mut writer = enc.write_header()?;
|
||||||
|
let mut data = Vec::with_capacity((w * h * 3) as usize);
|
||||||
|
for px in buf {
|
||||||
|
data.extend_from_slice(&[px.r, px.g, px.b]);
|
||||||
|
}
|
||||||
|
writer.write_image_data(&data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
//! Общий headless-рендер: произвольный Slint-компонент → непустой PNG (План 6 P6.1).
|
||||||
|
|
||||||
|
use shturman_render::render_to_png;
|
||||||
|
|
||||||
|
slint::slint! {
|
||||||
|
export component Probe inherits Window {
|
||||||
|
width: 64px; height: 48px; background: #101418;
|
||||||
|
Rectangle { background: #ffffff; width: 20px; height: 20px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renders_component_to_nonempty_png() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("probe.png");
|
||||||
|
render_to_png(|| Ok(Probe::new()?), 64, 48, &path).expect("render");
|
||||||
|
|
||||||
|
let dec = png::Decoder::new(std::fs::File::open(&path).unwrap());
|
||||||
|
let mut r = dec.read_info().unwrap();
|
||||||
|
assert_eq!((r.info().width, r.info().height), (64, 48));
|
||||||
|
let mut buf = vec![0u8; r.output_buffer_size()];
|
||||||
|
let info = r.next_frame(&mut buf).unwrap();
|
||||||
|
let px = &buf[..info.buffer_size()];
|
||||||
|
assert!(px.iter().any(|&b| b != px[0]), "кадр одноцветный");
|
||||||
|
}
|
||||||
@@ -7,12 +7,13 @@ license.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
shturman-sdk = { path = "../../shturman-sdk" }
|
shturman-sdk = { path = "../../shturman-sdk" }
|
||||||
shturman-common = { path = "../../shturman-common" }
|
shturman-common = { path = "../../shturman-common" }
|
||||||
|
shturman-render = { path = "../shturman-render" }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
slint.workspace = true
|
slint.workspace = true
|
||||||
# PNG-кодек для headless software-render кадра (E2E-ассерт «кадр не пустой», спека §6).
|
|
||||||
png = "0.17"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
|
# PNG-декодер для проверки кадра в tests/screenshot.rs (рендер — в shturman-render).
|
||||||
|
png = "0.17"
|
||||||
|
|||||||
@@ -6,12 +6,8 @@
|
|||||||
mod theme;
|
mod theme;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Once;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use slint::platform::software_renderer::{MinimalSoftwareWindow, RepaintBufferType};
|
|
||||||
use slint::platform::{Platform, PlatformError, WindowAdapter};
|
|
||||||
use slint::ComponentHandle;
|
use slint::ComponentHandle;
|
||||||
|
|
||||||
slint::slint! {
|
slint::slint! {
|
||||||
@@ -167,64 +163,13 @@ pub fn run_interactive(initial: &Initial, hour: u8, clock: &str) -> anyhow::Resu
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- headless software-render (спека §6: основной автотест кадра, композитор не нужен) ---
|
// --- headless software-render первого кадра (спека §6) — через общий хелпер shturman-render ---
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static SCREENSHOT_WINDOW: Rc<MinimalSoftwareWindow> =
|
|
||||||
MinimalSoftwareWindow::new(RepaintBufferType::ReusedBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ScreenshotPlatform;
|
|
||||||
impl Platform for ScreenshotPlatform {
|
|
||||||
fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
|
|
||||||
Ok(SCREENSHOT_WINDOW.with(|w| w.clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Установить software-platform один раз на процесс. `--screenshot` зовётся в свежем процессе
|
|
||||||
/// первым делом; в мультитестовом процессе повтор терпим (`Once` + терпимый результат).
|
|
||||||
fn ensure_screenshot_platform() {
|
|
||||||
static ONCE: Once = Once::new();
|
|
||||||
ONCE.call_once(|| {
|
|
||||||
let _ = slint::platform::set_platform(Box::new(ScreenshotPlatform));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Headless software-render первого кадра в PNG (спека §6). Без дисплей-сервера/композитора.
|
/// Headless software-render первого кадра в PNG (спека §6). Без дисплей-сервера/композитора.
|
||||||
/// `hour` задаёт тему для `auto` (тест — детерминированно); `clock` берётся из текущего времени.
|
/// `hour` задаёт тему для `auto` (тест — детерминированно); `clock` берётся из текущего времени.
|
||||||
pub fn render_screenshot(initial: &Initial, hour: u8, path: &Path) -> anyhow::Result<()> {
|
pub fn render_screenshot(initial: &Initial, hour: u8, path: &Path) -> anyhow::Result<()> {
|
||||||
ensure_screenshot_platform();
|
|
||||||
let (_, clock) = utc_hh_mm();
|
let (_, clock) = utc_hh_mm();
|
||||||
let ui = build_ui(initial, hour, &clock)?;
|
shturman_render::render_to_png(|| build_ui(initial, hour, &clock), FRAME_W, FRAME_H, path)?;
|
||||||
|
|
||||||
let window = SCREENSHOT_WINDOW.with(|w| w.clone());
|
|
||||||
window.set_size(slint::PhysicalSize::new(FRAME_W, FRAME_H));
|
|
||||||
ui.show()?;
|
|
||||||
ui.window().request_redraw(); // форсим перерисовку (повторный рендер в том же потоке)
|
|
||||||
|
|
||||||
let mut buf = vec![slint::Rgb8Pixel { r: 0, g: 0, b: 0 }; (FRAME_W * FRAME_H) as usize];
|
|
||||||
let drawn = window.draw_if_needed(|renderer| {
|
|
||||||
renderer.render(buf.as_mut_slice(), FRAME_W as usize);
|
|
||||||
});
|
|
||||||
ui.hide()?; // освободить окно для следующего рендера в том же потоке
|
|
||||||
if !drawn {
|
|
||||||
anyhow::bail!("software-renderer не отрисовал кадр");
|
|
||||||
}
|
|
||||||
write_png(path, FRAME_W, FRAME_H, &buf)?;
|
|
||||||
tracing::info!(path = %path.display(), "кадр записан (software-render)");
|
tracing::info!(path = %path.display(), "кадр записан (software-render)");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_png(path: &Path, w: u32, h: u32, buf: &[slint::Rgb8Pixel]) -> anyhow::Result<()> {
|
|
||||||
let file = std::fs::File::create(path)?;
|
|
||||||
let mut enc = png::Encoder::new(std::io::BufWriter::new(file), w, h);
|
|
||||||
enc.set_color(png::ColorType::Rgb);
|
|
||||||
enc.set_depth(png::BitDepth::Eight);
|
|
||||||
let mut writer = enc.write_header()?;
|
|
||||||
let mut data = Vec::with_capacity((w * h * 3) as usize);
|
|
||||||
for px in buf {
|
|
||||||
data.extend_from_slice(&[px.r, px.g, px.b]);
|
|
||||||
}
|
|
||||||
writer.write_image_data(&data)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user