use std::collections::HashMap;
use std::num::NonZeroU32;
use std::rc::Rc;
use std::time::Instant;
use lieui_geom::{Point, Rect, Size};
use winit::application::ApplicationHandler;
use winit::event_loop::ControlFlow;
use winit::dpi::LogicalSize;
use winit::event::{ElementState, Ime, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy};
use winit::keyboard::{Key, NamedKey};
use winit::window::{Window, WindowAttributes, WindowId as OsWindowId};
use crate::app::{App, CloseAction, ExternalData};
use crate::event::{Event, EventKind, KeyCode, Modifiers, PointerButton, PointerId};
use crate::input::InputEvent;
use crate::reactive::Dirty;
use crate::track::Kind;
use crate::window::WindowId;
fn clipboard_set(text: &str) {
match arboard::Clipboard::new() {
Ok(mut cb) => {
if let Err(e) = cb.set_text(text.to_string()) {
eprintln!("[lieui] 写剪贴板失败:{e}");
}
}
Err(e) => eprintln!("[lieui] 打开剪贴板失败:{e}"),
}
}
fn clipboard_get() -> Option<String> {
arboard::Clipboard::new()
.ok()
.and_then(|mut cb| cb.get_text().ok())
}
#[derive(Debug)]
pub enum AppEvent {
Wake,
External {
window: WindowId,
data: ExternalData,
},
}
#[derive(Clone)]
pub struct RepaintHandle {
proxy: EventLoopProxy<AppEvent>,
}
impl RepaintHandle {
pub fn new(proxy: EventLoopProxy<AppEvent>) -> Self {
Self { proxy }
}
pub fn wake(&self) -> bool {
self.proxy.send_event(AppEvent::Wake).is_ok()
}
pub fn post_external(&self, window: WindowId, data: ExternalData) -> bool {
self.proxy
.send_event(AppEvent::External { window, data })
.is_ok()
}
}
impl std::fmt::Debug for RepaintHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("RepaintHandle")
}
}
pub fn pack_xrgb(src: &[vello_cpu::color::PremulRgba8], out: &mut [u32]) -> usize {
let n = src.len().min(out.len());
for i in 0..n {
let p = src[i];
let (r, g, b) = match p.a {
255 => (p.r, p.g, p.b),
0 => (0, 0, 0),
a => {
let un = |v: u8| {
let v = u32::from(v) * 255 + u32::from(a) / 2;
(v / u32::from(a)).min(255) as u8
};
(un(p.r), un(p.g), un(p.b))
}
};
out[i] = (u32::from(r) << 16) | (u32::from(g) << 8) | u32::from(b);
}
n
}
pub fn softbuffer_damage(
physical: Size,
damage: &[Rect],
damage_all: bool,
scale: f32,
) -> Vec<softbuffer::Rect> {
let scaled: Vec<Rect> = damage
.iter()
.map(|d| {
Rect::new(
d.x * scale,
d.y * scale,
d.width * scale,
d.height * scale,
)
})
.collect();
crate::render::damage_batches(physical, &scaled, damage_all)
.into_iter()
.map(|r| {
let nz = |v: f32| {
NonZeroU32::new((v.max(1.0).round() as u32).max(1)).unwrap_or(NonZeroU32::MIN)
};
softbuffer::Rect {
x: r.x.max(0.0).round() as u32,
y: r.y.max(0.0).round() as u32,
width: nz(r.width),
height: nz(r.height),
}
})
.collect()
}
struct WinSurface {
window: Rc<Window>,
surface: softbuffer::Surface<Rc<Window>, Rc<Window>>,
size: Option<(u32, u32)>,
}
struct Runner {
app: App,
context: Option<softbuffer::Context<Rc<Window>>>,
windows: HashMap<WindowId, WinSurface>,
cursor: Point,
modifiers: winit::keyboard::ModifiersState,
trace: bool,
exit_requested: bool,
}
impl Runner {
fn new(app: App) -> Self {
Self {
app,
context: None,
windows: HashMap::new(),
cursor: Point::zero(),
modifiers: winit::keyboard::ModifiersState::empty(),
trace: std::env::var_os("LIEUI_TRACE").is_some(),
exit_requested: false,
}
}
fn app_window_id(&self, os: OsWindowId) -> Option<WindowId> {
self.windows
.iter()
.find(|(_, w)| w.window.id() == os)
.map(|(id, _)| *id)
}
fn create_window(&mut self, el: &ActiveEventLoop, id: WindowId) {
if self.windows.contains_key(&id) {
return;
}
let Some(ctx) = self.app.window_ctx(id) else {
return;
};
let cfg = ctx.config().clone();
let mut attrs = WindowAttributes::default()
.with_title(cfg.title.clone())
.with_inner_size(LogicalSize::new(cfg.size.width as f64, cfg.size.height as f64))
.with_resizable(cfg.resizable)
.with_decorations(cfg.decorations);
if let Some(min) = cfg.min_size {
attrs = attrs.with_min_inner_size(LogicalSize::new(min.width as f64, min.height as f64));
}
if let Some(max) = cfg.max_size {
attrs = attrs.with_max_inner_size(LogicalSize::new(max.width as f64, max.height as f64));
}
let window = match el.create_window(attrs) {
Ok(w) => Rc::new(w),
Err(e) => {
eprintln!("[lieui] 创建窗口失败:{e}");
return;
}
};
window.set_ime_allowed(true);
let context = self.context.get_or_insert_with(|| {
softbuffer::Context::new(Rc::clone(&window)).expect("softbuffer context")
});
let surface = match softbuffer::Surface::new(context, Rc::clone(&window)) {
Ok(s) => s,
Err(e) => {
eprintln!("[lieui] 创建表面失败:{e}");
return;
}
};
let phys = window.inner_size();
let scale = window.scale_factor() as f32;
self.windows.insert(
id,
WinSurface {
window,
surface,
size: None,
},
);
let rt = self.app.runtime();
if let Some(ctx) = self.app.window_ctx_mut(id) {
ctx.set_scale_factor(&rt, scale);
let logical = Size::new(phys.width as f32 / scale, phys.height as f32 / scale);
ctx.set_size(&rt, logical);
}
}
fn destroy_window(&mut self, id: WindowId) {
self.windows.remove(&id);
if self.windows.is_empty() {
self.context = None;
}
}
fn tick(&mut self, el: &ActiveEventLoop, now: Instant) {
let (opened, closed) = self.app.drain_requests();
for id in opened {
self.create_window(el, id);
}
for id in closed {
self.destroy_window(id);
}
let ids: Vec<WindowId> = self.app.windows().iter().map(|w| w.id()).collect();
for id in ids {
let rt = self.app.runtime();
let stats = {
let Some(ctx) = self.app.window_ctx_mut(id) else {
continue;
};
ctx.animate(now);
ctx.tick(&rt, now);
ctx.frame(&rt)
};
if self.trace && !stats.is_idle() {
eprintln!(
"[lieui] {id:?} view={} align.patched={} layout[边界={} 移动={}] paint={}px batches={} present={}",
stats.view_ran,
stats.align.patched,
stats.layout.boundaries,
stats.layout.moved,
stats.rasterized_pixels(),
stats.render.raster.batches,
stats.present_pending,
);
}
if stats.present_pending {
self.present(id, &stats.damage, stats.damage_all);
}
}
if self.windows.is_empty() && self.app.windows().is_empty() {
self.exit_requested = true;
el.exit();
return;
}
let mut wakeup: Option<Instant> = None;
for id in self.app.windows().iter().map(|w| w.id()) {
if let Some(ctx) = self.app.window_ctx(id)
&& let Some(t) = ctx.next_wakeup()
{
wakeup = Some(match wakeup {
Some(prev) if prev <= t => prev,
_ => t,
});
}
}
el.set_control_flow(match wakeup {
Some(t) => ControlFlow::WaitUntil(t),
None => ControlFlow::Wait,
});
}
fn present(&mut self, id: WindowId, damage: &[Rect], damage_all: bool) {
let Some(ctx) = self.app.window_ctx(id) else {
return;
};
let physical = ctx.physical_size();
let scale = ctx.scale_factor();
let pixmap = ctx.pixmap();
let pw = u32::from(pixmap.width());
let ph = u32::from(pixmap.height());
let Some(ws) = self.windows.get_mut(&id) else {
return;
};
let (Some(nw), Some(nh)) = (NonZeroU32::new(pw), NonZeroU32::new(ph)) else {
return;
};
let mut resized = false;
if ws.size != Some((pw, ph)) {
if ws.surface.resize(nw, nh).is_ok() {
ws.size = Some((pw, ph));
resized = true;
} else {
return;
}
}
ws.window.pre_present_notify();
let mut buffer: softbuffer::Buffer<'_, Rc<Window>, Rc<Window>> =
match ws.surface.buffer_mut() {
Ok(b) => b,
Err(_) => return,
};
let full = resized || damage_all || buffer.age() == 0;
let batches = softbuffer_damage(physical, damage, full, scale);
let src = pixmap.data();
let stride = pw as usize;
{
let dst: &mut [u32] = &mut buffer;
if full {
pack_xrgb(src, dst);
} else {
for r in &batches {
let (y, h) = (r.y as usize, r.height.get() as usize);
for row in y..(y + h).min(ph as usize) {
let start = row * stride;
let end = (start + stride).min(src.len()).min(dst.len());
if start >= end {
continue;
}
let s = &src[start..end];
let d = &mut dst[start..end];
let _ = pack_xrgb(s, d);
}
}
}
}
if full {
let _ = buffer.present();
} else {
let _ = buffer.present_with_damage(&batches);
}
}
fn to_logical(&self, id: WindowId, p: winit::dpi::PhysicalPosition<f64>) -> Point {
let scale = self
.app
.window_ctx(id)
.map(|c| c.scale_factor())
.unwrap_or(1.0)
.max(0.001);
Point::new(p.x as f32 / scale, p.y as f32 / scale)
}
fn push_input(&mut self, id: WindowId, ev: InputEvent) {
let rt = self.app.runtime();
if let Some(ctx) = self.app.window_ctx_mut(id) {
ctx.pointer(&rt, ev);
}
}
fn button_of(b: MouseButton) -> PointerButton {
match b {
MouseButton::Left => PointerButton::Left,
MouseButton::Right => PointerButton::Right,
MouseButton::Middle => PointerButton::Middle,
MouseButton::Back => PointerButton::Other(3),
MouseButton::Forward => PointerButton::Other(4),
MouseButton::Other(n) => PointerButton::Other(n),
}
}
fn modifiers(m: winit::keyboard::ModifiersState) -> Modifiers {
let mut out = Modifiers::EMPTY;
if m.shift_key() {
out |= Modifiers::SHIFT;
}
if m.control_key() {
out |= Modifiers::CTRL;
}
if m.alt_key() {
out |= Modifiers::ALT;
}
if m.super_key() {
out |= Modifiers::META;
}
out
}
fn handle_clipboard(&mut self, id: WindowId, ev: &Event) -> bool {
let Event::Key {
code,
modifiers: mods,
..
} = ev
else {
return false;
};
if !mods.ctrl() {
return false;
}
let KeyCode::Char(c) = code else {
return false;
};
let lower = c.to_ascii_lowercase();
if !matches!(lower, 'c' | 'x' | 'v') {
return false;
}
let focus = match self.app.window_ctx(id).and_then(|w| w.track().focused) {
Some(f) => f,
None => return false,
};
let is_input = self
.app
.window_ctx(id)
.map(|w| matches!(w.track().get(focus).map(|n| &n.kind), Some(Kind::Input { .. })))
.unwrap_or(false);
if !is_input {
return false;
}
let copied = self
.app
.window_ctx(id)
.and_then(|w| w.track().input_selected_text(focus));
match lower {
'c' => {
if let Some(text) = copied {
clipboard_set(&text);
}
}
'x' => {
if let Some(text) = copied {
clipboard_set(&text);
if let Some(w) = self.app.window_ctx_mut(id) {
w.track_mut().input_backspace(focus);
}
}
}
'v' => {
if let Some(text) = clipboard_get()
&& let Some(w) = self.app.window_ctx_mut(id)
{
w.track_mut().input_insert(focus, &text);
}
}
_ => return false,
}
let rt = self.app.runtime();
rt.mark(id, Dirty::PAINT | Dirty::PRESENT);
if let Some(ws) = self.windows.get(&id) {
ws.window.request_redraw();
}
true
}
fn key_code(key: &Key) -> KeyCode {
match key {
Key::Named(n) => KeyCode::Named(match n {
NamedKey::Enter => crate::event::NamedKey::Enter,
NamedKey::Escape => crate::event::NamedKey::Escape,
NamedKey::Tab => crate::event::NamedKey::Tab,
NamedKey::Backspace => crate::event::NamedKey::Backspace,
NamedKey::Delete => crate::event::NamedKey::Delete,
NamedKey::Insert => crate::event::NamedKey::Insert,
NamedKey::Space => crate::event::NamedKey::Space,
NamedKey::ArrowLeft => crate::event::NamedKey::Left,
NamedKey::ArrowRight => crate::event::NamedKey::Right,
NamedKey::ArrowUp => crate::event::NamedKey::Up,
NamedKey::ArrowDown => crate::event::NamedKey::Down,
NamedKey::Home => crate::event::NamedKey::Home,
NamedKey::End => crate::event::NamedKey::End,
NamedKey::PageUp => crate::event::NamedKey::PageUp,
NamedKey::PageDown => crate::event::NamedKey::PageDown,
NamedKey::Shift => crate::event::NamedKey::Shift,
NamedKey::Control => crate::event::NamedKey::Control,
NamedKey::Alt => crate::event::NamedKey::Alt,
NamedKey::Meta => crate::event::NamedKey::Meta,
other => crate::event::NamedKey::Other(*other as u32),
}),
Key::Character(s) => s.chars().next().map(KeyCode::Char).unwrap_or(KeyCode::Named(
crate::event::NamedKey::Other(0),
)),
_ => KeyCode::Named(crate::event::NamedKey::Other(0)),
}
}
fn on_window_event(&mut self, _el: &ActiveEventLoop, os_id: OsWindowId, event: WindowEvent) {
let Some(id) = self.app_window_id(os_id) else {
return;
};
let rt = self.app.runtime();
match event {
WindowEvent::CloseRequested => {
let action = self
.app
.window_ctx_mut(id)
.map(|c| c.close_requested(&rt))
.unwrap_or(CloseAction::Close);
if action == CloseAction::Close {
self.app.close_window(id);
self.destroy_window(id);
}
}
WindowEvent::Resized(size) => {
let scale = self
.app
.window_ctx(id)
.map(|c| c.scale_factor())
.unwrap_or(1.0)
.max(0.001);
if let Some(ctx) = self.app.window_ctx_mut(id) {
ctx.set_size(
&rt,
Size::new(size.width as f32 / scale, size.height as f32 / scale),
);
}
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
if let Some(ctx) = self.app.window_ctx_mut(id) {
ctx.set_scale_factor(&rt, scale_factor as f32);
}
}
WindowEvent::RedrawRequested => {
let now = Instant::now();
let stats = {
let Some(ctx) = self.app.window_ctx_mut(id) else {
return;
};
ctx.tick(&rt, now);
ctx.frame(&rt)
};
if stats.present_pending {
self.present(id, &stats.damage, stats.damage_all);
}
}
WindowEvent::CursorMoved { position, .. } => {
let pos = self.to_logical(id, position);
self.cursor = pos;
self.push_input(
id,
InputEvent::Move {
pointer: PointerId(0),
pos,
},
);
}
WindowEvent::CursorLeft { .. } => {
self.push_input(id, InputEvent::Leave);
}
WindowEvent::MouseInput { state, button, .. } => {
let b = Self::button_of(button);
let pos = self.cursor;
let ev = match state {
ElementState::Pressed => InputEvent::Down {
pointer: PointerId(0),
pos,
button: b,
},
ElementState::Released => InputEvent::Up {
pointer: PointerId(0),
pos,
button: b,
},
};
self.push_input(id, ev);
}
WindowEvent::MouseWheel { delta, .. } => {
let d = match delta {
MouseScrollDelta::LineDelta(x, y) => (x * 48.0, y * 48.0),
MouseScrollDelta::PixelDelta(p) => {
let scale = self
.app
.window_ctx(id)
.map(|c| c.scale_factor())
.unwrap_or(1.0)
.max(0.001);
(p.x as f32 / scale, p.y as f32 / scale)
}
};
let pos = self.cursor;
self.push_input(
id,
InputEvent::Wheel {
pointer: PointerId(0),
pos,
delta: d,
},
);
}
WindowEvent::Focused(false) => {
self.push_input(id, InputEvent::Cancel { pointer: PointerId(0) });
}
WindowEvent::ModifiersChanged(m) => {
self.modifiers = m.state();
}
WindowEvent::KeyboardInput { event: key, .. } => {
let code = Self::key_code(&key.logical_key);
let kind = match key.state {
ElementState::Pressed => EventKind::KeyDown,
ElementState::Released => EventKind::KeyUp,
};
let mods = Self::modifiers(self.modifiers);
if key.state == ElementState::Pressed
&& code == KeyCode::Named(crate::event::NamedKey::Tab)
{
let forward = !self.modifiers.shift_key();
if let Some(ctx) = self.app.window_ctx_mut(id) {
ctx.tab(&rt, forward);
}
}
let mut ev = Event::key_with(kind, code, mods);
if let Event::Key {
text, repeat: r, ..
} = &mut ev
{
*r = key.repeat;
if kind == EventKind::KeyDown
&& let Key::Character(s) = &key.logical_key
{
*text = Some(s.to_string());
}
}
if kind == EventKind::KeyDown && self.handle_clipboard(id, &ev) {
return;
}
let path = self.focus_path(id);
self.dispatch(id, &path, &ev);
}
WindowEvent::Ime(ime) => match ime {
Ime::Commit(text) => {
for ch in text.chars() {
let path = self.focus_path(id);
self.dispatch(id, &path, &Event::char_received(ch));
}
}
Ime::Preedit(text, cursor) => {
let path = self.focus_path(id);
self.dispatch(
id,
&path,
&Event::ImePreedit {
text,
cursor: cursor.map(|(a, b)| (a as u32, b as u32)),
},
);
}
Ime::Enabled | Ime::Disabled => {}
},
_ => {}
}
}
fn focus_path(&self, id: WindowId) -> Vec<crate::track::NodeId> {
self.app
.window_ctx(id)
.map(|c| match c.track().focused {
Some(f) => crate::hit::path_to(c.track(), f),
None => c.content_root().map(|r| vec![r]).unwrap_or_default(),
})
.unwrap_or_default()
}
fn dispatch(&mut self, id: WindowId, path: &[crate::track::NodeId], ev: &Event) {
if path.is_empty() {
return;
}
let rt = self.app.runtime();
if let Some(ctx) = self.app.window_ctx_mut(id) {
ctx.dispatch(&rt, path, ev);
}
}
}
impl ApplicationHandler<AppEvent> for Runner {
fn resumed(&mut self, el: &ActiveEventLoop) {
let ids: Vec<WindowId> = self.app.windows().iter().map(|w| w.id()).collect();
for id in ids {
self.create_window(el, id);
}
self.tick(el, Instant::now());
}
fn user_event(&mut self, el: &ActiveEventLoop, event: AppEvent) {
match event {
AppEvent::Wake => {
self.tick(el, Instant::now());
}
AppEvent::External { window, data } => {
let rt = self.app.runtime();
if let Some(ctx) = self.app.window_ctx_mut(window) {
ctx.external(&rt, data);
}
self.tick(el, Instant::now());
}
}
let dirty: Vec<WindowId> = self.app.windows().iter().map(|w| w.id()).collect();
for id in dirty {
let rt = self.app.runtime();
if let Some(ctx) = self.app.window_ctx_mut(id) {
let d = rt.peek_dirty(id);
if d.contains(Dirty::PRESENT) || d.contains(Dirty::PAINT) || d.contains(Dirty::VIEW) {
if let Some(ws) = self.windows.get(&id) {
ws.window.request_redraw();
}
let _ = ctx;
}
}
}
}
fn window_event(&mut self, el: &ActiveEventLoop, os_id: OsWindowId, event: WindowEvent) {
self.on_window_event(el, os_id, event);
if let Some(id) = self.app_window_id(os_id) {
let rt = self.app.runtime();
let d = rt.peek_dirty(id);
if !d.is_empty()
&& let Some(ws) = self.windows.get(&id)
{
ws.window.request_redraw();
}
}
}
fn about_to_wait(&mut self, el: &ActiveEventLoop) {
self.tick(el, Instant::now());
}
}
pub fn run(
app: App,
on_ready: Option<Box<dyn FnOnce(RepaintHandle)>>,
) -> Result<(), Box<dyn std::error::Error>> {
let event_loop: EventLoop<AppEvent> = EventLoop::with_user_event().build()?;
if let Some(f) = on_ready {
f(RepaintHandle::new(event_loop.create_proxy()));
}
let mut runner = Runner::new(app);
event_loop.run_app(&mut runner)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use vello_cpu::color::PremulRgba8;
fn px(r: u8, g: u8, b: u8, a: u8) -> PremulRgba8 {
PremulRgba8::from_u8_array([r, g, b, a])
}
#[test]
fn pack_xrgb_passes_through_opaque_pixels() {
let src = [px(255, 128, 64, 255), px(0, 0, 0, 255)];
let mut out = [0u32; 2];
assert_eq!(pack_xrgb(&src, &mut out), 2);
assert_eq!(out[0], 0x00FF_8040);
assert_eq!(out[1], 0x0000_0000);
}
#[test]
fn pack_xrgb_unpremultiplies_and_ignores_alpha() {
let src = [px(128, 128, 128, 128)];
let mut out = [0u32; 1];
pack_xrgb(&src, &mut out);
assert_eq!(out[0], 0x00FF_FFFF);
let src = [px(0, 0, 0, 0)];
pack_xrgb(&src, &mut out);
assert_eq!(out[0], 0);
}
#[test]
fn pack_xrgb_stops_at_the_shorter_side() {
let src = [px(1, 2, 3, 255); 5];
let mut out = [0u32; 2];
assert_eq!(pack_xrgb(&src, &mut out), 2);
}
#[test]
fn softbuffer_damage_scales_and_clamps() {
let physical = Size::new(200.0, 100.0);
let d = softbuffer_damage(physical, &[Rect::new(10.0, 10.0, 20.0, 20.0)], false, 2.0);
assert_eq!(d.len(), 1);
assert_eq!(rect_of(&d[0]), (20, 20, 40, 40));
let d = softbuffer_damage(physical, &[Rect::new(500.0, 500.0, 10.0, 10.0)], false, 1.0);
assert!(d.is_empty());
let d = softbuffer_damage(physical, &[], true, 1.0);
assert_eq!(d.len(), 1);
assert_eq!(rect_of(&d[0]), (0, 0, 200, 100));
}
#[test]
fn softbuffer_damage_uses_physical_size_at_fractional_scale() {
let physical = Size::new(150.0, 75.0); let d = softbuffer_damage(physical, &[Rect::new(0.0, 0.0, 100.0, 50.0)], false, 1.5);
assert_eq!(d.len(), 1);
assert_eq!(rect_of(&d[0]), (0, 0, 150, 75));
}
fn rect_of(r: &softbuffer::Rect) -> (u32, u32, u32, u32) {
(r.x, r.y, r.width.get(), r.height.get())
}
}