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:
2026-06-24 20:03:16 +03:00
parent e841c082b3
commit 798e5ba14a
7 changed files with 123 additions and 59 deletions
+68
View File
@@ -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(())
}