use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use crate::sync::{new_channel, Sender, WakerShared};
use tiny_skia::Pixmap;
use crate::core::{NodeId, Tree};
use crate::theme::Theme;
use crate::event::{
CursorShape, Key, MenuAction, MenuItem, MouseButton, PointerEvent, PointerKind, WindowOp,
};
use crate::geometry::{Color, Point, Rect, Size};
use crate::platform::{self, AppHandler, WindowConfig};
use crate::render::{Canvas, Paint, SkiaCanvas};
use crate::text::{PlatformTextEngine, TextEngine};
use crate::ui::Element;
const MENU_ITEM_H: i32 = 30;
const MENU_PAD_X: i32 = 14;
const MENU_VPAD: i32 = 4;
const MENU_MIN_W: i32 = 130;
const MENU_FONT: f32 = 14.0;
const TOOLTIP_DELAY_MS: u64 = 500;
const TOOLTIP_FONT: f32 = 13.0;
const TOOLTIP_PAD_X: i32 = 8;
const TOOLTIP_PAD_Y: i32 = 4;
const TOOLTIP_CURSOR_DX: i32 = 12;
const TOOLTIP_CURSOR_DY: i32 = 20;
struct ContextMenu {
items: Vec<MenuItem>,
rect: Rect,
hover: Option<usize>,
target: NodeId,
}
impl ContextMenu {
fn item_y(&self, i: usize) -> i32 {
self.rect.y + MENU_VPAD + i as i32 * MENU_ITEM_H
}
fn item_at(&self, p: Point) -> Option<usize> {
if !self.rect.contains(p) {
return None;
}
let i = (p.y - self.rect.y - MENU_VPAD) / MENU_ITEM_H;
if i >= 0 && (i as usize) < self.items.len() {
Some(i as usize)
} else {
None
}
}
}
type RenderClosure = Box<dyn FnMut(&mut Pixmap, Size)>;
pub struct App {
cfg: WindowConfig,
render: Option<RenderClosure>,
content: Option<Element>,
theme: Option<Theme>,
pumps: Vec<Box<dyn FnMut()>>,
intervals: Vec<(Duration, Box<dyn FnMut()>)>,
waker_shared: Option<Arc<WakerShared>>,
}
impl App {
pub fn new(title: impl Into<String>, width: i32, height: i32) -> Self {
Self {
cfg: WindowConfig {
title: title.into(),
width,
height,
bg: Color::hex(0xF3F3F3),
centered: false,
resizable: true,
screenshot: None,
screenshot_scale: 1.0,
screenshot_rclick: None,
screenshot_click: None,
screenshot_hover: None,
tray: None,
frameless: false,
animations: None,
},
render: None,
content: None,
theme: None,
pumps: Vec::new(),
intervals: Vec::new(),
waker_shared: None,
}
}
pub fn bg(mut self, c: Color) -> Self {
self.cfg.bg = c;
self
}
pub fn resizable(mut self, v: bool) -> Self {
self.cfg.resizable = v;
self
}
pub fn animations(mut self, on: bool) -> Self {
self.cfg.animations = Some(on);
self
}
pub fn centered(mut self) -> Self {
self.cfg.centered = true;
self
}
pub fn theme(mut self, t: Theme) -> Self {
self.cfg.bg = t.palette.bg;
self.theme = Some(t);
self
}
pub fn screenshot(mut self, path: impl Into<PathBuf>) -> Self {
self.cfg.screenshot = Some(path.into());
self
}
pub fn screenshot_from_args(mut self) -> Self {
let args: Vec<String> = std::env::args().collect();
if let Some(i) = args.iter().position(|a| a == "--screenshot") {
if let Some(p) = args.get(i + 1) {
self.cfg.screenshot = Some(PathBuf::from(p));
}
}
if let Some(i) = args.iter().position(|a| a == "--scale") {
if let Some(v) = args.get(i + 1).and_then(|s| s.parse::<f32>().ok()) {
self.cfg.screenshot_scale = v;
}
}
if let Some(i) = args.iter().position(|a| a == "--rclick") {
if let (Some(x), Some(y)) = (
args.get(i + 1).and_then(|s| s.parse::<i32>().ok()),
args.get(i + 2).and_then(|s| s.parse::<i32>().ok()),
) {
self.cfg.screenshot_rclick = Some((x, y));
}
}
if let Some(i) = args.iter().position(|a| a == "--click") {
if let (Some(x), Some(y)) = (
args.get(i + 1).and_then(|s| s.parse::<i32>().ok()),
args.get(i + 2).and_then(|s| s.parse::<i32>().ok()),
) {
self.cfg.screenshot_click = Some((x, y));
}
}
if let Some(i) = args.iter().position(|a| a == "--hover") {
if let (Some(x), Some(y)) = (
args.get(i + 1).and_then(|s| s.parse::<i32>().ok()),
args.get(i + 2).and_then(|s| s.parse::<i32>().ok()),
) {
self.cfg.screenshot_hover = Some((x, y));
}
}
self
}
pub fn on_render(mut self, f: impl FnMut(&mut Pixmap, Size) + 'static) -> Self {
self.render = Some(Box::new(f));
self
}
pub fn content(mut self, root: Element) -> Self {
self.content = Some(root);
self
}
pub fn tray(mut self, tray: platform::Tray) -> Self {
self.cfg.tray = Some(tray);
self
}
pub fn frameless(mut self) -> Self {
self.cfg.frameless = true;
self
}
pub fn run(self) {
let theme = Rc::new(self.theme.unwrap_or_default());
let waker = self.waker_shared.clone();
let cfg = self.cfg;
let handler: Box<dyn AppHandler> = if let Some(f) = self.render {
Box::new(ClosureHandler { f })
} else if let Some(root) = self.content {
Box::new(UiHost::new(root, theme, cfg.bg, self.pumps, self.intervals))
} else {
Box::new(ClosureHandler { f: Box::new(|_, _| {}) })
};
platform::run(cfg, handler, waker);
}
#[cfg(test)]
fn into_handler_for_test(self) -> UiHost {
let theme = Rc::new(self.theme.unwrap_or_default());
UiHost::new(self.content.unwrap(), theme, self.cfg.bg, self.pumps, self.intervals)
}
fn shared_waker(&mut self) -> crate::sync::Waker {
self.waker_shared.get_or_insert_with(WakerShared::new).waker()
}
pub fn channel<Msg: Send + 'static>(
&mut self,
on_message: impl FnMut(Msg) + 'static,
) -> Sender<Msg> {
let waker = self.shared_waker();
let (tx, pump) = new_channel(waker, on_message);
self.pumps.push(pump);
tx
}
pub fn on_interval(mut self, every: Duration, cb: impl FnMut() + 'static) -> Self {
self.intervals.push((every, Box::new(cb)));
self
}
}
struct ClosureHandler {
f: RenderClosure,
}
impl AppHandler for ClosureHandler {
fn render(&mut self, pixmap: &mut Pixmap, size: Size) {
(self.f)(pixmap, size);
}
}
const FLING_FRICTION: f32 = 0.996;
const FLING_TRIGGER: f32 = 0.25;
const FLING_STOP: f32 = 0.02;
const FLING_STALL_MS: u64 = 100;
const BOUNCE_GAIN: f32 = 22.0;
const MAX_BOUNCE: f32 = 26.0;
const BOUNCE_DECAY: f32 = 0.98;
#[derive(Clone, Copy, PartialEq)]
enum FlingPhase {
Glide,
Bounce,
}
struct Fling {
node: NodeId,
phase: FlingPhase,
vel: f32,
over: f32,
residual: f32,
last_ms: Option<u64>,
}
struct UiHost {
tree: Tree,
engine: PlatformTextEngine,
hover: Option<NodeId>,
capture: Option<NodeId>,
focus: Option<NodeId>,
focus_order: Vec<NodeId>,
close: bool,
scale: f32,
focus_visible: bool,
menu: Option<ContextMenu>,
logical_size: Size,
theme: Rc<Theme>,
start: std::time::Instant,
pan_residual: f32,
fling: Option<Fling>,
pending_window_op: Option<WindowOp>,
hover_pos: Point,
hover_since_ms: u64,
tooltip_suppressed: bool,
bg: Color,
back: Option<Pixmap>,
pending_damage: Option<Rect>,
needs_full: bool,
swallow_up: bool,
pumps: Vec<Box<dyn FnMut()>>,
interval_cbs: Vec<Box<dyn FnMut()>>,
interval_durs: Vec<std::time::Duration>,
}
const DAMAGE_MARGIN: i32 = 2;
impl UiHost {
fn new(
root: Element,
theme: Rc<Theme>,
bg: Color,
pumps: Vec<Box<dyn FnMut()>>,
intervals: Vec<(std::time::Duration, Box<dyn FnMut()>)>,
) -> Self {
crate::theme::set_current(theme.clone());
let mut tree = Tree::new();
tree.root = Some(root.build(&mut tree));
tree.clipboard = Some(Box::new(crate::platform::Clipboard));
let (interval_durs, interval_cbs): (Vec<_>, Vec<_>) =
intervals.into_iter().unzip();
Self {
tree,
engine: PlatformTextEngine::new(),
hover: None,
capture: None,
focus: None,
focus_order: Vec::new(),
close: false,
scale: 1.0,
focus_visible: false,
menu: None,
logical_size: Size::new(0, 0),
theme,
start: std::time::Instant::now(),
pan_residual: 0.0,
fling: None,
pending_window_op: None,
hover_pos: Point::new(0, 0),
hover_since_ms: 0,
tooltip_suppressed: false,
bg,
back: None,
pending_damage: None,
needs_full: true,
swallow_up: false,
pumps,
interval_cbs,
interval_durs,
}
}
fn clear_fling(&mut self) -> bool {
match self.fling.take() {
Some(f) => {
self.tree.set_over_scroll(f.node, 0);
true
}
None => false,
}
}
fn step_fling(&mut self, now_ms: u64) {
let Some(f) = self.fling.as_ref() else { return };
let (node, phase, last) = (f.node, f.phase, f.last_ms);
let dt = match last {
None => 16,
Some(prev) => {
let raw = now_ms.saturating_sub(prev);
if raw > FLING_STALL_MS {
self.tree.set_over_scroll(node, 0);
self.fling = None;
return;
}
raw.min(64)
}
} as f32;
match phase {
FlingPhase::Glide => {
let f = self.fling.as_mut().unwrap();
f.last_ms = Some(now_ms);
f.vel *= FLING_FRICTION.powf(dt);
let advance = f.vel * dt + f.residual;
let delta = advance.trunc() as i32;
f.residual = advance - delta as f32;
let vel = f.vel;
let hit = match self.tree.scroll_range(node) {
Some((cur, max)) => {
let next = cur + delta;
let clamped = next.clamp(0, max);
self.tree.set_scroll_y(node, clamped);
clamped != next
}
None => {
self.fling = None; return;
}
};
if hit {
let impulse = (-vel * BOUNCE_GAIN).clamp(-MAX_BOUNCE, MAX_BOUNCE);
if impulse.abs() < 1.0 {
self.tree.set_over_scroll(node, 0);
self.fling = None;
} else {
self.tree.set_over_scroll(node, impulse.round() as i32);
let f = self.fling.as_mut().unwrap();
f.phase = FlingPhase::Bounce;
f.over = impulse;
crate::anim::request_repaint();
}
} else if vel.abs() < FLING_STOP {
self.fling = None;
} else {
crate::anim::request_repaint();
}
}
FlingPhase::Bounce => {
let f = self.fling.as_mut().unwrap();
f.last_ms = Some(now_ms);
f.over *= BOUNCE_DECAY.powf(dt);
let over = f.over;
if over.abs() < 0.5 {
self.tree.set_over_scroll(node, 0);
self.fling = None;
} else {
self.tree.set_over_scroll(node, over.round() as i32);
crate::anim::request_repaint();
}
}
}
}
fn open_menu(&mut self, req: crate::event::MenuRequest, target: NodeId) {
let mut max_w = 0;
for it in &req.items {
let w = self.engine.measure(&it.label, None, MENU_FONT, None).w;
max_w = max_w.max(w);
}
let menu_w = (max_w + 2 * MENU_PAD_X).max(MENU_MIN_W).max(req.min_width);
let menu_h = req.items.len() as i32 * MENU_ITEM_H + 2 * MENU_VPAD;
let ws = self.logical_size;
let mut x = req.pos.x;
let mut y = req.pos.y;
if ws.w > 0 && x + menu_w > ws.w {
x = (ws.w - menu_w).max(0);
}
if ws.h > 0 && y + menu_h > ws.h {
y = (ws.h - menu_h).max(0);
}
self.menu = Some(ContextMenu {
items: req.items,
rect: Rect::new(x, y, menu_w, menu_h),
hover: None,
target,
});
}
fn handle_menu_pointer(&mut self, ev: PointerEvent) -> bool {
match ev.kind {
PointerKind::Move => {
let h = self.menu.as_ref().and_then(|m| m.item_at(ev.pos));
if let Some(m) = self.menu.as_mut() {
if m.hover != h {
m.hover = h;
return true;
}
}
false
}
PointerKind::Down => {
self.swallow_up = true;
let inside = self.menu.as_ref().is_some_and(|m| m.rect.contains(ev.pos));
if !inside {
self.menu = None; return true;
}
let hit = self.menu.as_ref().and_then(|m| {
m.item_at(ev.pos)
.filter(|&i| m.items[i].enabled)
.map(|i| (m.items[i].action.clone(), m.target))
});
if let Some((action, target)) = hit {
self.menu = None;
match action {
MenuAction::SendKey(k) => {
let res = self.tree.dispatch_key(k, Some(target));
if res.close {
self.close = true;
}
}
MenuAction::Run(f) => f(),
}
}
true }
_ => true, }
}
fn move_focus(&mut self, forward: bool) -> bool {
if self.focus_order.is_empty() {
return false;
}
let n = self.focus_order.len();
let cur = self.focus.and_then(|f| self.focus_order.iter().position(|&x| x == f));
let next = match cur {
Some(i) if forward => (i + 1) % n,
Some(i) => (i + n - 1) % n,
None if forward => 0,
None => n - 1,
};
let nf = Some(self.focus_order[next]);
let old = self.focus;
self.tree.set_focused(nf, old);
self.focus = nf;
true
}
}
impl AppHandler for UiHost {
fn render(&mut self, pixmap: &mut Pixmap, size: Size) {
for pump in self.pumps.iter_mut() {
pump();
}
crate::theme::set_current(self.theme.clone());
crate::anim::reset_request();
let now_ms = self.start.elapsed().as_millis() as u64;
crate::anim::set_clock_ms(now_ms);
self.step_fling(now_ms);
let s = self.scale;
let logical = Size::new(
(size.w as f32 / s).round().max(1.0) as i32,
(size.h as f32 / s).round().max(1.0) as i32,
);
self.logical_size = logical;
let back_ok = self
.back
.as_ref()
.map(|b| b.width() == size.w as u32 && b.height() == size.h as u32)
.unwrap_or(false);
let overlay = self.menu.is_some()
|| (!self.tooltip_suppressed
&& self.hover.and_then(|h| self.tree.node_tooltip(h)).is_some());
let damage = self.pending_damage.take();
let scale_ok = {
let q = s * 4.0;
(q - q.round()).abs() < 1e-3
};
let damage_small = damage
.map(|d| {
let win = self.logical_size.w as i64 * self.logical_size.h as i64;
win > 0 && (d.w as i64 * d.h as i64) * 2 <= win
})
.unwrap_or(false);
let do_full = self.needs_full || !back_ok || overlay || !scale_ok || !damage_small;
self.needs_full = false;
if !do_full {
self.render_partial(pixmap, size, s, damage.unwrap());
self.pending_damage = next_damage(&mut self.needs_full);
return;
}
self.tree.layout_root(logical, &mut self.engine);
self.focus_order = self.tree.focusable_order();
if let Some(f) = self.focus {
if !self.focus_order.contains(&f) {
self.tree.set_focused(None, Some(f));
self.focus = None;
}
}
self.tree.focus_ring_visible = self.focus_visible;
let mut canvas = SkiaCanvas::with_text(pixmap, &mut self.engine, s);
self.tree.paint(&mut canvas);
if let Some(menu) = self.menu.as_ref() {
let (pal, mt) = (&self.theme.palette, &self.theme.menu);
let r = menu.rect;
canvas.fill_round_rect(r.x as f32, r.y as f32, r.w as f32, r.h as f32, 8.0, &Paint::fill(mt.bg(pal)));
canvas.stroke_round_rect(r.x as f32, r.y as f32, r.w as f32, r.h as f32, 8.0, 1.0, &Paint::fill(mt.border(pal)));
for (i, it) in menu.items.iter().enumerate() {
let iy = menu.item_y(i);
if menu.hover == Some(i) && it.enabled {
canvas.fill_round_rect((r.x + 4) as f32, iy as f32, (r.w - 8) as f32, MENU_ITEM_H as f32, 5.0, &Paint::fill(mt.hover(pal)));
}
let color = if !it.enabled {
mt.text_disabled(pal)
} else if it.checked {
mt.accent(pal)
} else {
mt.text(pal)
};
let tr = Rect::new(r.x + MENU_PAD_X, iy, r.w - 2 * MENU_PAD_X, MENU_ITEM_H);
canvas.draw_text(&it.label, tr, color, crate::spec::Align::Start, None, MENU_FONT);
if it.checked {
let cr = Rect::new(r.x, iy, r.w - MENU_PAD_X, MENU_ITEM_H);
canvas.draw_text("\u{2713}", cr, mt.accent(pal), crate::spec::Align::End, None, MENU_FONT);
}
}
}
if self.menu.is_none() && !self.tooltip_suppressed {
if let Some(text) = self.hover.and_then(|h| self.tree.node_tooltip(h)) {
if now_ms.saturating_sub(self.hover_since_ms) < TOOLTIP_DELAY_MS {
crate::anim::request_repaint();
} else {
let (pal, tt) = (&self.theme.palette, &self.theme.tooltip);
let ts = canvas.measure_text(&text, None, TOOLTIP_FONT);
let (w, h) = (ts.w + 2 * TOOLTIP_PAD_X, ts.h + 2 * TOOLTIP_PAD_Y);
let ws = self.logical_size;
let mut x = self.hover_pos.x + TOOLTIP_CURSOR_DX;
let mut y = self.hover_pos.y + TOOLTIP_CURSOR_DY;
if ws.w > 0 && x + w > ws.w {
x = (ws.w - w).max(0);
}
if ws.h > 0 && y + h > ws.h {
y = (self.hover_pos.y - h - 4).max(0); }
let corner = tt.corner(&self.theme.metrics);
canvas.fill_round_rect(x as f32, y as f32, w as f32, h as f32, corner, &Paint::fill(tt.bg(pal)));
let tr = Rect::new(x + TOOLTIP_PAD_X, y, w - 2 * TOOLTIP_PAD_X, h);
canvas.draw_text(&text, tr, tt.text(pal), crate::spec::Align::Start, None, TOOLTIP_FONT);
}
}
}
drop(canvas);
self.seed_back(pixmap, size);
self.pending_damage = next_damage(&mut self.needs_full);
}
fn on_pointer(&mut self, mut ev: crate::event::PointerEvent) -> bool {
self.needs_full = true;
let s = self.scale;
ev.pos = Point::new(
(ev.pos.x as f32 / s).round() as i32,
(ev.pos.y as f32 / s).round() as i32,
);
if ev.kind == PointerKind::Down {
self.clear_fling();
}
if self.menu.is_some() {
return self.handle_menu_pointer(ev);
}
match ev.kind {
PointerKind::Up if self.swallow_up => {
self.swallow_up = false;
return false;
}
PointerKind::Down => self.swallow_up = false,
_ => {}
}
let old_hover = self.hover;
let mut hover = self.hover;
let mut capture = self.capture;
let res = self.tree.dispatch_pointer(ev, &mut hover, &mut capture);
self.hover = hover;
self.capture = capture;
self.hover_pos = ev.pos;
let now_ms = self.start.elapsed().as_millis() as u64;
if hover != old_hover {
self.hover_since_ms = now_ms;
self.tooltip_suppressed = false;
}
match ev.kind {
PointerKind::Down => self.tooltip_suppressed = true,
PointerKind::Move if self.tooltip_suppressed => {
self.tooltip_suppressed = false;
self.hover_since_ms = now_ms;
}
_ => {}
}
if let Some(f) = res.focus {
let old = self.focus;
self.tree.set_focused(Some(f), old);
self.focus = Some(f);
self.focus_visible = false;
}
if res.close {
self.close = true;
}
if let Some(req) = res.menu {
if let Some(target) = self.focus {
self.open_menu(req, target);
}
}
if let Some(url) = res.open_url {
platform::open_url(&url);
}
if res.window_op.is_some() {
self.pending_window_op = res.window_op;
}
res.repaint
}
fn on_key(&mut self, ev: crate::event::KeyEvent) -> bool {
self.needs_full = true;
if self.menu.is_some() {
if ev.key == Key::Escape {
self.menu = None;
}
return true;
}
if ev.key == Key::Tab {
self.focus_visible = true;
return self.move_focus(!ev.shift);
}
let res = self.tree.dispatch_key(ev, self.focus);
if res.close {
self.close = true;
}
if let Some(url) = res.open_url {
platform::open_url(&url);
}
if res.window_op.is_some() {
self.pending_window_op = res.window_op;
}
if !res.consumed && ev.key == Key::Escape {
self.close = true;
}
res.repaint
}
fn wants_close(&self) -> bool {
self.close
}
fn capture_active(&self) -> bool {
self.capture.is_some()
}
fn set_scale(&mut self, scale: f32) {
self.needs_full = true;
self.scale = scale;
self.engine.set_scale(scale);
}
fn wants_animation(&self) -> bool {
crate::anim::animation_requested()
}
fn intervals(&self) -> Vec<std::time::Duration> {
self.interval_durs.clone()
}
fn on_interval_fired(&mut self, idx: usize) -> bool {
if let Some(cb) = self.interval_cbs.get_mut(idx) {
cb();
true
} else {
false
}
}
fn on_drop_files(&mut self, pos: Point, paths: Vec<std::path::PathBuf>) -> bool {
self.needs_full = true;
let s = self.scale;
let p = Point::new((pos.x as f32 / s).round() as i32, (pos.y as f32 / s).round() as i32);
let res = self.tree.dispatch_files(p, paths);
if res.close {
self.close = true;
}
if let Some(url) = res.open_url {
platform::open_url(&url);
}
res.repaint
}
fn window_drag_at(&self, pos: Point) -> bool {
if self.menu.is_some() {
return false;
}
let s = self.scale;
let p = Point::new((pos.x as f32 / s).round() as i32, (pos.y as f32 / s).round() as i32);
self.tree.drag_hit_at(p)
}
fn interactive_at(&self, pos: Point) -> bool {
let s = self.scale;
let p = Point::new((pos.x as f32 / s).round() as i32, (pos.y as f32 / s).round() as i32);
self.tree.interactive_hit_at(p)
}
fn take_window_op(&mut self) -> Option<WindowOp> {
self.pending_window_op.take()
}
fn cursor(&self) -> CursorShape {
if self.menu.is_some() {
return CursorShape::Arrow;
}
match self.hover {
Some(h) if self.tree.node_enabled(h) => self.tree.cursor_at(h),
_ => CursorShape::Arrow,
}
}
fn on_pan(&mut self, pos: Point, dy: i32) -> bool {
self.needs_full = true; if self.menu.is_some() {
self.pan_residual = 0.0;
return false;
}
let s = self.scale;
let p = Point::new((pos.x as f32 / s).round() as i32, (pos.y as f32 / s).round() as i32);
let total = dy as f32 / s + self.pan_residual;
let dyl = total.trunc() as i32;
self.pan_residual = total - dyl as f32;
if dyl == 0 {
return false;
}
self.clear_fling();
self.tree.pan_scroll(p, dyl)
}
fn start_fling(&mut self, pos: Point, vy: f32) -> bool {
self.clear_fling();
if self.menu.is_some() {
return false;
}
if vy.abs() < FLING_TRIGGER {
return false;
}
let s = self.scale;
let p = Point::new((pos.x as f32 / s).round() as i32, (pos.y as f32 / s).round() as i32);
let Some(node) = self.tree.scroll_node_at(p) else {
return false;
};
let vel = -vy / s;
self.fling = Some(Fling {
node,
phase: FlingPhase::Glide,
vel,
over: 0.0,
residual: 0.0,
last_ms: None,
});
crate::anim::request_repaint();
true
}
fn cancel_fling(&mut self) -> bool {
self.clear_fling()
}
fn ime_caret(&self) -> Option<(i32, i32, i32)> {
let focus = self.focus?;
let (p, h) = self.tree.caret_of(focus)?;
let s = self.scale;
Some((
(p.x as f32 * s).round() as i32,
(p.y as f32 * s).round() as i32,
((h as f32 * s).round() as i32).max(1),
))
}
fn on_capture_lost(&mut self) -> bool {
self.needs_full = true;
if self.capture.is_none() {
return false;
}
let ev = PointerEvent::single(
PointerKind::Up,
Point::new(-1_000_000, -1_000_000),
MouseButton::Left,
);
let mut hover = self.hover;
let mut capture = self.capture;
let res = self.tree.dispatch_pointer(ev, &mut hover, &mut capture);
self.hover = hover;
self.capture = capture;
res.repaint
}
}
impl UiHost {
fn render_partial(&mut self, pixmap: &mut Pixmap, size: Size, s: f32, damage: Rect) {
let raw = damage.inflate(DAMAGE_MARGIN).intersect(&Rect::from_size(self.logical_size));
const GRID: i32 = 4;
let x0 = raw.x - raw.x.rem_euclid(GRID);
let y0 = raw.y - raw.y.rem_euclid(GRID);
let x1 = raw.right() + (GRID - raw.right().rem_euclid(GRID)) % GRID;
let y1 = raw.bottom() + (GRID - raw.bottom().rem_euclid(GRID)) % GRID;
let dmg = Rect::new(x0, y0, x1 - x0, y1 - y0).intersect(&Rect::from_size(self.logical_size));
let pdmg = dmg.scaled(s).intersect(&Rect::new(0, 0, size.w, size.h));
if pdmg.is_empty() {
self.blit_back_to(pixmap);
return;
}
let Some(mut sub) = Pixmap::new(pdmg.w as u32, pdmg.h as u32) else {
self.blit_back_to(pixmap);
return;
};
sub.fill(tiny_skia::Color::from_rgba8(self.bg.r, self.bg.g, self.bg.b, self.bg.a));
{
let mut canvas =
SkiaCanvas::with_text_offset(&mut sub, &mut self.engine, s, Point::new(dmg.x, dmg.y));
self.tree.paint(&mut canvas);
}
if let Some(back) = self.back.as_mut() {
blit(&sub, back, pdmg.x, pdmg.y);
}
self.blit_back_to(pixmap);
}
fn blit_back_to(&self, pixmap: &mut Pixmap) {
if let Some(back) = self.back.as_ref() {
if back.width() == pixmap.width() && back.height() == pixmap.height() {
pixmap.data_mut().copy_from_slice(back.data());
}
}
}
fn seed_back(&mut self, pixmap: &Pixmap, size: Size) {
let need_new = self
.back
.as_ref()
.map(|b| b.width() != size.w as u32 || b.height() != size.h as u32)
.unwrap_or(true);
if need_new {
self.back = Pixmap::new(size.w as u32, size.h as u32);
}
if let Some(back) = self.back.as_mut() {
back.data_mut().copy_from_slice(pixmap.data());
}
}
}
fn next_damage(needs_full: &mut bool) -> Option<Rect> {
match crate::anim::take_damage() {
crate::anim::Damage::Rect(r) => Some(r),
crate::anim::Damage::Full => {
*needs_full = true;
None
}
crate::anim::Damage::None => None,
}
}
fn blit(src: &Pixmap, dst: &mut Pixmap, x: i32, y: i32) {
let (sw, sh) = (src.width() as usize, src.height() as usize);
let (dw, dh) = (dst.width() as usize, dst.height() as usize);
let (x, y) = (x.max(0) as usize, y.max(0) as usize);
debug_assert!(x + sw <= dw && y + sh <= dh, "blit 越界:({x},{y})+{sw}x{sh} 超出 {dw}x{dh}");
let sd = src.data();
let dd = dst.data_mut();
for row in 0..sh {
let s0 = row * sw * 4;
let d0 = ((y + row) * dw + x) * 4;
dd[d0..d0 + sw * 4].copy_from_slice(&sd[s0..s0 + sw * 4]);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn channel_returns_sendable_sender() {
let mut app = App::new("t", 100, 100);
let tx = app.channel::<u32>(|_| {});
let h = std::thread::spawn(move || tx.send(5));
assert!(h.join().unwrap().is_ok());
assert_eq!(app.pumps.len(), 1);
}
#[test]
fn on_interval_registers() {
let app = App::new("t", 100, 100)
.on_interval(std::time::Duration::from_millis(100), || {});
assert_eq!(app.intervals.len(), 1);
}
#[test]
fn render_drains_pending_messages() {
use crate::platform::AppHandler;
use tiny_skia::Pixmap;
let got = std::rc::Rc::new(std::cell::Cell::new(0u32));
let g2 = got.clone();
let mut app = App::new("t", 50, 50);
let tx = app.channel::<u32>(move |m| g2.set(m));
app = app.content(Element::col());
tx.send(7).unwrap();
let mut handler = app.into_handler_for_test();
handler.set_scale(1.0);
let mut pm = Pixmap::new(50, 50).unwrap();
handler.render(&mut pm, Size::new(50, 50));
assert_eq!(got.get(), 7, "render 前排空 pump,消息写入状态");
}
}