use std::sync::Arc;
pub mod a11y;
use llimphi_hal::winit::application::ApplicationHandler;
use llimphi_hal::winit::dpi::{LogicalSize, PhysicalPosition};
use llimphi_hal::winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use llimphi_hal::winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
use llimphi_hal::winit::keyboard::ModifiersState;
use llimphi_hal::winit::window::{Window, WindowAttributes, WindowId};
use llimphi_hal::{Hal, Surface, WinitSurface};
pub use llimphi_hal::winit::keyboard::{Key, NamedKey};
use llimphi_layout::{ComputedLayout, LayoutTree};
use llimphi_raster::peniko::color::palette;
use llimphi_raster::{vello, Renderer};
pub use llimphi_hal;
pub use llimphi_layout;
pub use llimphi_raster;
pub use llimphi_text;
pub use llimphi_compositor;
pub use llimphi_compositor::*;
pub trait App: 'static {
type Model: 'static;
type Msg: Clone + Send + 'static;
fn init(handle: &Handle<Self::Msg>) -> Self::Model;
fn update(model: Self::Model, msg: Self::Msg, handle: &Handle<Self::Msg>) -> Self::Model;
fn view(model: &Self::Model) -> View<Self::Msg>;
fn on_key(_model: &Self::Model, _event: &KeyEvent) -> Option<Self::Msg> {
None
}
fn on_focus(_model: &Self::Model, _id: Option<u64>) -> Option<Self::Msg> {
None
}
fn ime_allowed() -> bool {
false
}
fn on_ime(_model: &Self::Model, _event: &ImeEvent) -> Option<Self::Msg> {
None
}
fn ime_cursor_area(_model: &Self::Model) -> Option<(f32, f32, f32, f32)> {
None
}
fn on_wheel(
_model: &Self::Model,
_delta: WheelDelta,
_cursor: (f32, f32),
_modifiers: Modifiers,
) -> Option<Self::Msg> {
None
}
fn view_overlay(_model: &Self::Model) -> Option<View<Self::Msg>> {
None
}
fn on_file_drop(_model: &Self::Model, _path: std::path::PathBuf) -> Option<Self::Msg> {
None
}
fn on_resize(_model: &Self::Model, _width: u32, _height: u32) -> Option<Self::Msg> {
None
}
fn on_scale_factor(_model: &Self::Model, _scale: f64) -> Option<Self::Msg> {
None
}
fn title() -> &'static str {
"llimphi"
}
fn window_title(_model: &Self::Model) -> Option<String> {
None
}
fn secondary_view(_model: &Self::Model, _key: u64) -> Option<View<Self::Msg>> {
None
}
fn secondary_title(_model: &Self::Model, _key: u64) -> Option<String> {
None
}
fn on_secondary_close(_model: &Self::Model, _key: u64) -> Option<Self::Msg> {
None
}
fn app_id() -> Option<&'static str> {
None
}
fn initial_size() -> (u32, u32) {
(960, 540)
}
}
pub enum UserEvent<Msg> {
Msg(Msg),
Quit,
OpenWindow {
key: u64,
title: String,
width: u32,
height: u32,
},
CloseWindow { key: u64 },
A11y(accesskit_winit::Event),
}
impl<Msg> From<accesskit_winit::Event> for UserEvent<Msg> {
fn from(e: accesskit_winit::Event) -> Self {
UserEvent::A11y(e)
}
}
pub struct Handle<Msg: Send + 'static> {
inner: HandleInner<Msg>,
}
enum HandleInner<Msg: Send + 'static> {
Real(EventLoopProxy<UserEvent<Msg>>),
Test,
Lifted(Arc<dyn Fn(Msg) + Send + Sync>),
}
impl<Msg: Send + 'static> Clone for Handle<Msg> {
fn clone(&self) -> Self {
Self {
inner: match &self.inner {
HandleInner::Real(p) => HandleInner::Real(p.clone()),
HandleInner::Test => HandleInner::Test,
HandleInner::Lifted(f) => HandleInner::Lifted(f.clone()),
},
}
}
}
impl<Msg: Send + 'static> Handle<Msg> {
pub fn for_test() -> Self {
Self {
inner: HandleInner::Test,
}
}
pub fn quit(&self) {
match &self.inner {
HandleInner::Real(p) => {
let _ = p.send_event(UserEvent::Quit);
}
HandleInner::Test => {}
HandleInner::Lifted(_) => {}
}
}
pub fn lift<Sub, F>(&self, lift: F) -> Handle<Sub>
where
Sub: Send + 'static,
F: Fn(Sub) -> Msg + Send + Sync + 'static,
{
let parent = self.clone();
Handle {
inner: HandleInner::Lifted(Arc::new(move |sub: Sub| parent.dispatch(lift(sub)))),
}
}
pub fn open_window(&self, key: u64, title: impl Into<String>, width: u32, height: u32) {
if let HandleInner::Real(p) = &self.inner {
let _ = p.send_event(UserEvent::OpenWindow {
key,
title: title.into(),
width,
height,
});
}
}
pub fn close_window(&self, key: u64) {
if let HandleInner::Real(p) = &self.inner {
let _ = p.send_event(UserEvent::CloseWindow { key });
}
}
pub fn dispatch(&self, msg: Msg) {
match &self.inner {
HandleInner::Real(p) => {
let _ = p.send_event(UserEvent::Msg(msg));
}
HandleInner::Test => {}
HandleInner::Lifted(f) => f(msg),
}
}
pub fn spawn<F>(&self, f: F)
where
F: FnOnce() -> Msg + Send + 'static,
{
match &self.inner {
HandleInner::Real(p) => {
let proxy = p.clone();
std::thread::spawn(move || {
let msg = f();
let _ = proxy.send_event(UserEvent::Msg(msg));
});
}
HandleInner::Test => {
std::thread::spawn(move || {
let _ = f();
});
}
HandleInner::Lifted(lift) => {
let lift = lift.clone();
std::thread::spawn(move || {
lift(f());
});
}
}
}
pub fn spawn_periodic<F>(&self, period: std::time::Duration, f: F)
where
F: Fn() -> Msg + Send + 'static,
{
match &self.inner {
HandleInner::Real(p) => {
let proxy = p.clone();
std::thread::spawn(move || loop {
std::thread::sleep(period);
if proxy.send_event(UserEvent::Msg(f())).is_err() {
break;
}
});
}
HandleInner::Test => {
let _ = f;
}
HandleInner::Lifted(lift) => {
let lift = lift.clone();
std::thread::spawn(move || loop {
std::thread::sleep(period);
lift(f());
});
}
}
}
}
#[derive(Debug, Clone)]
pub struct KeyEvent {
pub key: Key,
pub state: KeyState,
pub text: Option<String>,
pub modifiers: Modifiers,
pub repeat: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyState {
Pressed,
Released,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ImeEvent {
Enabled,
Preedit {
text: String,
cursor: Option<(usize, usize)>,
},
Commit(String),
Disabled,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Modifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub meta: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct WheelDelta {
pub x: f32,
pub y: f32,
}
impl From<ModifiersState> for Modifiers {
fn from(m: ModifiersState) -> Self {
Self {
shift: m.shift_key(),
ctrl: m.control_key(),
alt: m.alt_key(),
meta: m.super_key(),
}
}
}
mod eventloop;
struct Runtime<A: App> {
handle: Handle<A::Msg>,
state: Option<RuntimeState<A>>,
secondaries: Vec<SecondaryState<A>>,
}
struct SecondaryState<A: App> {
key: u64,
window: Arc<Window>,
surface: WinitSurface,
scene: vello::Scene,
typesetter: llimphi_text::Typesetter,
layout: LayoutTree,
cursor: PhysicalPosition<f64>,
modifiers: Modifiers,
last_render: Option<SecRenderCache<A::Msg>>,
hovered: Option<usize>,
drag: Option<DragState<A::Msg>>,
last_title: Option<String>,
}
struct SecRenderCache<Msg> {
mounted: Mounted<Msg>,
computed: ComputedLayout,
}
struct RuntimeState<A: App> {
window: Arc<Window>,
hal: Hal,
surface: WinitSurface,
renderer: Renderer,
scene: vello::Scene,
overlay_compositor: llimphi_hal::OverlayCompositor,
blur_compositor: llimphi_hal::BlurCompositor,
color_filter_compositor: llimphi_hal::ColorFilterCompositor,
model: Option<A::Model>,
cursor: PhysicalPosition<f64>,
modifiers: Modifiers,
typesetter: llimphi_text::Typesetter,
layout: LayoutTree,
overlay_layout: LayoutTree,
last_render: Option<RenderCache<A::Msg>>,
hovered: Option<usize>,
drag: Option<DragState<A::Msg>>,
focused: Option<u64>,
last_title: Option<String>,
anim_registry: llimphi_compositor::AnimRegistry,
size_anim_registry: llimphi_compositor::SizeAnimRegistry,
hero_registry: llimphi_compositor::HeroRegistry,
a11y_adapter: accesskit_winit::Adapter,
a11y_tree_id: accesskit::TreeId,
ripple_registry: llimphi_compositor::RippleRegistry,
last_tap: Option<(std::time::Instant, PhysicalPosition<f64>)>,
pending_long_press: Option<PendingLongPress<A::Msg>>,
retained: Option<RetainedScene>,
selection: Option<TextSelection>,
}
#[derive(Clone, Copy)]
struct RetainedScene {
w: u32,
h: u32,
animating: bool,
rippling: bool,
has_overlay: bool,
}
#[derive(Clone, Copy)]
struct TextSelection {
key: u64,
sel: llimphi_text::parley::Selection,
dragging: bool,
}
struct RenderCache<Msg> {
mounted: Mounted<Msg>,
computed: ComputedLayout,
hover_idx: Option<usize>,
drop_hover_idx: Option<usize>,
overlay: Option<OverlayCache<Msg>>,
}
struct OverlayCache<Msg> {
mounted: Mounted<Msg>,
computed: ComputedLayout,
hover_idx: Option<usize>,
}
enum DragHandlerKind<Msg> {
Delta(DragFn<Msg>),
DeltaAt(DragAtFn<Msg>, f32, f32),
Velocity(DragVelocityFn<Msg>),
}
enum GestureResolved<Msg> {
Direct(Msg),
At(ClickAtFn<Msg>, f32, f32, f32, f32),
}
impl<Msg: Clone> GestureResolved<Msg> {
fn invoke(&self) -> Option<Msg> {
match self {
GestureResolved::Direct(m) => Some(m.clone()),
GestureResolved::At(h, lx, ly, w, ht) => h(*lx, *ly, *w, *ht),
}
}
}
struct PendingLongPress<Msg> {
deadline: std::time::Instant,
origin: PhysicalPosition<f64>,
handler: GestureResolved<Msg>,
}
const LONG_PRESS_DELAY: std::time::Duration = std::time::Duration::from_millis(500);
const LONG_PRESS_MOVE_CANCEL: f64 = 8.0;
const DOUBLE_TAP_WINDOW: std::time::Duration = std::time::Duration::from_millis(400);
const DOUBLE_TAP_DIST: f64 = 16.0;
fn double_tap_qualifies(
last: Option<(std::time::Instant, PhysicalPosition<f64>)>,
now: std::time::Instant,
pos: PhysicalPosition<f64>,
) -> bool {
last.is_some_and(|(t, p)| {
now.duration_since(t) <= DOUBLE_TAP_WINDOW
&& ((p.x - pos.x).powi(2) + (p.y - pos.y).powi(2)).sqrt() <= DOUBLE_TAP_DIST
})
}
struct DragState<Msg> {
handler: DragHandlerKind<Msg>,
last_cursor: PhysicalPosition<f64>,
payload: Option<u64>,
samples: std::collections::VecDeque<(std::time::Instant, f64, f64)>,
}
const VELOCITY_WINDOW: std::time::Duration = std::time::Duration::from_millis(100);
const VELOCITY_MAX_SAMPLES: usize = 8;
fn compute_drag_velocity(
samples: &std::collections::VecDeque<(std::time::Instant, f64, f64)>,
now: std::time::Instant,
) -> (f32, f32) {
if samples.is_empty() {
return (0.0, 0.0);
}
let cutoff = now.checked_sub(VELOCITY_WINDOW).unwrap_or(now);
let recent: Vec<&(std::time::Instant, f64, f64)> =
samples.iter().filter(|(t, _, _)| *t >= cutoff).collect();
if recent.is_empty() {
return (0.0, 0.0);
}
let t0 = recent[0].0;
let dt = now.duration_since(t0).as_secs_f32();
if dt < 0.001 {
return (0.0, 0.0);
}
let sum_dx: f64 = recent.iter().map(|(_, dx, _)| *dx).sum();
let sum_dy: f64 = recent.iter().map(|(_, _, dy)| *dy).sum();
((sum_dx as f32) / dt, (sum_dy as f32) / dt)
}
pub fn run<A: App>() {
let event_loop = EventLoop::<UserEvent<A::Msg>>::with_user_event()
.build()
.expect("event loop");
event_loop.set_control_flow(ControlFlow::Wait);
let handle = Handle {
inner: HandleInner::Real(event_loop.create_proxy()),
};
let mut runtime: Runtime<A> = Runtime {
handle,
state: None,
secondaries: Vec::new(),
};
event_loop.run_app(&mut runtime).expect("run app");
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
#[test]
fn lift_aplica_la_funcion_de_elevacion() {
use std::sync::{Arc, Mutex};
let seen = Arc::new(Mutex::new(Vec::<i32>::new()));
let parent: Handle<i32> = Handle::for_test();
let sub: Handle<String> = {
let seen = seen.clone();
parent.lift(move |s: String| {
let n = s.len() as i32;
seen.lock().unwrap().push(n);
n
})
};
sub.dispatch("hola".to_string());
let _ = sub.clone(); assert_eq!(*seen.lock().unwrap(), vec![4]);
}
#[test]
fn velocidad_de_drag_promedia_dentro_de_la_ventana() {
use std::collections::VecDeque;
let now = Instant::now();
let mut samples: VecDeque<(Instant, f64, f64)> = VecDeque::new();
samples.push_back((now - Duration::from_millis(80), 4.0, 0.0));
samples.push_back((now - Duration::from_millis(60), 4.0, 0.0));
samples.push_back((now - Duration::from_millis(40), 4.0, 0.0));
samples.push_back((now - Duration::from_millis(20), 4.0, 0.0));
let (vx, vy) = compute_drag_velocity(&samples, now);
assert!((vx - 200.0).abs() < 1.0, "vx={vx}");
assert!(vy.abs() < 1e-3);
let empty: VecDeque<(Instant, f64, f64)> = VecDeque::new();
assert_eq!(compute_drag_velocity(&empty, now), (0.0, 0.0));
let mut old: VecDeque<(Instant, f64, f64)> = VecDeque::new();
old.push_back((now - Duration::from_millis(500), 10.0, 10.0));
assert_eq!(compute_drag_velocity(&old, now), (0.0, 0.0));
let mut vy_samples: VecDeque<(Instant, f64, f64)> = VecDeque::new();
vy_samples.push_back((now - Duration::from_millis(75), 0.0, 5.0));
vy_samples.push_back((now - Duration::from_millis(50), 0.0, 5.0));
vy_samples.push_back((now - Duration::from_millis(25), 0.0, 5.0));
let (_, vy) = compute_drag_velocity(&vy_samples, now);
assert!((vy - 200.0).abs() < 1.0, "vy={vy}");
}
#[test]
fn double_tap_ventana_y_distancia() {
let t0 = Instant::now();
let p = PhysicalPosition::new(100.0, 100.0);
assert!(!double_tap_qualifies(None, t0, p));
let near = PhysicalPosition::new(102.0, 102.0);
assert!(double_tap_qualifies(
Some((t0, p)),
t0 + Duration::from_millis(100),
near
));
let far = PhysicalPosition::new(140.0, 100.0);
assert!(!double_tap_qualifies(
Some((t0, p)),
t0 + Duration::from_millis(100),
far
));
assert!(!double_tap_qualifies(
Some((t0, p)),
t0 + Duration::from_millis(600),
near
));
}
}