use crate::tree::{El, Rect};
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct UiTarget {
pub key: String,
pub node_id: String,
pub rect: Rect,
pub tooltip: Option<String>,
pub scroll_offset_y: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointerButton {
Primary,
Secondary,
Middle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum PointerKind {
#[default]
Mouse,
Touch,
Pen,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct PointerId(pub u32);
impl PointerId {
pub const PRIMARY: PointerId = PointerId(0);
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Pointer {
pub x: f32,
pub y: f32,
pub button: PointerButton,
pub kind: PointerKind,
pub id: PointerId,
pub pressure: Option<f32>,
}
impl Pointer {
pub fn mouse(x: f32, y: f32, button: PointerButton) -> Self {
Self {
x,
y,
button,
kind: PointerKind::Mouse,
id: PointerId::PRIMARY,
pressure: None,
}
}
pub fn moving(x: f32, y: f32) -> Self {
Self::mouse(x, y, PointerButton::Primary)
}
pub fn touch(x: f32, y: f32, button: PointerButton, id: PointerId) -> Self {
Self {
x,
y,
button,
kind: PointerKind::Touch,
id,
pressure: None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum UiKey {
Enter,
Escape,
Tab,
Space,
ArrowUp,
ArrowDown,
ArrowLeft,
ArrowRight,
Backspace,
Delete,
Home,
End,
PageUp,
PageDown,
Character(String),
Other(String),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct KeyModifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub logo: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct KeyPress {
pub key: UiKey,
pub modifiers: KeyModifiers,
pub repeat: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct KeyChord {
pub key: UiKey,
pub modifiers: KeyModifiers,
}
impl KeyChord {
pub fn vim(c: char) -> Self {
Self {
key: UiKey::Character(c.to_string()),
modifiers: KeyModifiers::default(),
}
}
pub fn ctrl(c: char) -> Self {
Self {
key: UiKey::Character(c.to_string()),
modifiers: KeyModifiers {
ctrl: true,
..Default::default()
},
}
}
pub fn ctrl_shift(c: char) -> Self {
Self {
key: UiKey::Character(c.to_string()),
modifiers: KeyModifiers {
ctrl: true,
shift: true,
..Default::default()
},
}
}
pub fn named(key: UiKey) -> Self {
Self {
key,
modifiers: KeyModifiers::default(),
}
}
pub fn with_modifiers(mut self, modifiers: KeyModifiers) -> Self {
self.modifiers = modifiers;
self
}
pub fn matches(&self, key: &UiKey, modifiers: KeyModifiers) -> bool {
key_eq(&self.key, key) && self.modifiers == modifiers
}
}
fn key_eq(a: &UiKey, b: &UiKey) -> bool {
match (a, b) {
(UiKey::Character(x), UiKey::Character(y)) => x.eq_ignore_ascii_case(y),
_ => a == b,
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct UiEvent {
pub key: Option<String>,
pub target: Option<UiTarget>,
pub pointer: Option<(f32, f32)>,
pub key_press: Option<KeyPress>,
pub text: Option<String>,
pub selection: Option<crate::selection::Selection>,
pub modifiers: KeyModifiers,
pub click_count: u8,
pub path: Option<std::path::PathBuf>,
pub pointer_kind: Option<PointerKind>,
pub kind: UiEventKind,
}
impl UiEvent {
pub fn synthetic_click(key: impl Into<String>) -> Self {
Self {
kind: UiEventKind::Click,
key: Some(key.into()),
target: None,
pointer: None,
key_press: None,
text: None,
selection: None,
modifiers: KeyModifiers::default(),
click_count: 1,
path: None,
pointer_kind: None,
}
}
pub fn route(&self) -> Option<&str> {
self.key.as_deref()
}
pub fn target_key(&self) -> Option<&str> {
self.target.as_ref().map(|t| t.key.as_str())
}
pub fn is_route(&self, key: &str) -> bool {
self.route() == Some(key)
}
pub fn is_click_or_activate(&self, key: &str) -> bool {
matches!(self.kind, UiEventKind::Click | UiEventKind::Activate) && self.is_route(key)
}
pub fn is_hotkey(&self, action: &str) -> bool {
self.kind == UiEventKind::Hotkey && self.is_route(action)
}
pub fn pointer_pos(&self) -> Option<(f32, f32)> {
self.pointer
}
pub fn pointer_x(&self) -> Option<f32> {
self.pointer.map(|(x, _)| x)
}
pub fn pointer_y(&self) -> Option<f32> {
self.pointer.map(|(_, y)| y)
}
pub fn target_rect(&self) -> Option<Rect> {
self.target.as_ref().map(|t| t.rect)
}
pub fn text(&self) -> Option<&str> {
self.text.as_deref()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum UiEventKind {
Click,
LinkActivated,
SecondaryClick,
MiddleClick,
Activate,
Escape,
Hotkey,
KeyDown,
TextInput,
Drag,
PointerUp,
PointerDown,
SelectionChanged,
PointerEnter,
PointerLeave,
PointerCancel,
LongPress,
FileHovered,
FileHoverCancelled,
FileDropped,
}
#[derive(Copy, Clone, Debug)]
pub struct BuildCx<'a> {
theme: &'a crate::Theme,
ui_state: Option<&'a crate::state::UiState>,
diagnostics: Option<&'a HostDiagnostics>,
viewport: Option<(f32, f32)>,
safe_area: Option<crate::tree::Sides>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum FrameTrigger {
#[default]
Other,
Initial,
Resize,
Pointer,
Keyboard,
Animation,
ShaderPaint,
Periodic,
}
impl FrameTrigger {
pub fn label(self) -> &'static str {
match self {
FrameTrigger::Other => "other",
FrameTrigger::Initial => "initial",
FrameTrigger::Resize => "resize",
FrameTrigger::Pointer => "pointer",
FrameTrigger::Keyboard => "keyboard",
FrameTrigger::Animation => "animation",
FrameTrigger::ShaderPaint => "shader-paint",
FrameTrigger::Periodic => "periodic",
}
}
}
#[derive(Clone, Debug)]
pub struct HostDiagnostics {
pub backend: &'static str,
pub surface_size: (u32, u32),
pub scale_factor: f32,
pub msaa_samples: u32,
pub frame_index: u64,
pub last_frame_dt: std::time::Duration,
pub last_build: std::time::Duration,
pub last_prepare: std::time::Duration,
pub last_layout: std::time::Duration,
pub last_layout_intrinsic_cache_hits: u64,
pub last_layout_intrinsic_cache_misses: u64,
pub last_layout_pruned_subtrees: u64,
pub last_layout_pruned_nodes: u64,
pub last_draw_ops: std::time::Duration,
pub last_draw_ops_culled_text_ops: u64,
pub last_paint: std::time::Duration,
pub last_paint_culled_ops: u64,
pub last_gpu_upload: std::time::Duration,
pub last_snapshot: std::time::Duration,
pub last_submit: std::time::Duration,
pub last_text_layout_cache_hits: u64,
pub last_text_layout_cache_misses: u64,
pub last_text_layout_cache_evictions: u64,
pub last_text_layout_shaped_bytes: u64,
pub trigger: FrameTrigger,
}
impl Default for HostDiagnostics {
fn default() -> Self {
Self {
backend: "?",
surface_size: (0, 0),
scale_factor: 1.0,
msaa_samples: 1,
frame_index: 0,
last_frame_dt: std::time::Duration::ZERO,
last_build: std::time::Duration::ZERO,
last_prepare: std::time::Duration::ZERO,
last_layout: std::time::Duration::ZERO,
last_layout_intrinsic_cache_hits: 0,
last_layout_intrinsic_cache_misses: 0,
last_layout_pruned_subtrees: 0,
last_layout_pruned_nodes: 0,
last_draw_ops: std::time::Duration::ZERO,
last_draw_ops_culled_text_ops: 0,
last_paint: std::time::Duration::ZERO,
last_paint_culled_ops: 0,
last_gpu_upload: std::time::Duration::ZERO,
last_snapshot: std::time::Duration::ZERO,
last_submit: std::time::Duration::ZERO,
last_text_layout_cache_hits: 0,
last_text_layout_cache_misses: 0,
last_text_layout_cache_evictions: 0,
last_text_layout_shaped_bytes: 0,
trigger: FrameTrigger::default(),
}
}
}
impl<'a> BuildCx<'a> {
pub fn new(theme: &'a crate::Theme) -> Self {
Self {
theme,
ui_state: None,
diagnostics: None,
viewport: None,
safe_area: None,
}
}
pub fn with_ui_state(mut self, ui_state: &'a crate::state::UiState) -> Self {
self.ui_state = Some(ui_state);
self
}
pub fn with_diagnostics(mut self, diagnostics: &'a HostDiagnostics) -> Self {
self.diagnostics = Some(diagnostics);
self
}
pub fn with_viewport(mut self, width: f32, height: f32) -> Self {
self.viewport = Some((width, height));
self
}
pub fn with_safe_area(mut self, sides: crate::tree::Sides) -> Self {
self.safe_area = Some(sides);
self
}
pub fn diagnostics(&self) -> Option<&HostDiagnostics> {
self.diagnostics
}
pub fn theme(&self) -> &crate::Theme {
self.theme
}
pub fn palette(&self) -> &crate::Palette {
self.theme.palette()
}
pub fn viewport(&self) -> Option<(f32, f32)> {
self.viewport
}
pub fn viewport_width(&self) -> Option<f32> {
self.viewport.map(|(w, _)| w)
}
pub fn viewport_height(&self) -> Option<f32> {
self.viewport.map(|(_, h)| h)
}
pub fn viewport_below(&self, threshold: f32) -> bool {
self.viewport_width().is_some_and(|w| w < threshold)
}
pub fn safe_area(&self) -> crate::tree::Sides {
self.safe_area.unwrap_or_default()
}
pub fn safe_area_bottom(&self) -> f32 {
self.safe_area().bottom
}
pub fn hovered_key(&self) -> Option<&str> {
self.ui_state?.hovered_key()
}
pub fn is_hovering_within(&self, key: &str) -> bool {
self.ui_state
.is_some_and(|state| state.is_hovering_within(key))
}
}
pub trait App {
fn before_build(&mut self) {}
fn build(&self, cx: &BuildCx) -> El;
fn on_event(&mut self, _event: UiEvent) {}
fn selection(&self) -> crate::selection::Selection {
crate::selection::Selection::default()
}
fn hotkeys(&self) -> Vec<(KeyChord, String)> {
Vec::new()
}
fn drain_toasts(&mut self) -> Vec<crate::toast::ToastSpec> {
Vec::new()
}
fn drain_focus_requests(&mut self) -> Vec<String> {
Vec::new()
}
fn drain_scroll_requests(&mut self) -> Vec<crate::scroll::ScrollRequest> {
Vec::new()
}
fn drain_link_opens(&mut self) -> Vec<String> {
Vec::new()
}
fn shaders(&self) -> Vec<AppShader> {
Vec::new()
}
fn theme(&self) -> crate::Theme {
crate::Theme::default()
}
}
#[derive(Clone, Copy, Debug)]
pub struct AppShader {
pub name: &'static str,
pub wgsl: &'static str,
pub samples_backdrop: bool,
pub samples_time: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Theme;
#[test]
fn viewport_unset_returns_none_and_breakpoint_returns_false() {
let theme = Theme::default();
let cx = BuildCx::new(&theme);
assert!(cx.viewport().is_none());
assert!(cx.viewport_width().is_none());
assert!(!cx.viewport_below(600.0));
}
#[test]
fn viewport_set_exposes_width_and_height() {
let theme = Theme::default();
let cx = BuildCx::new(&theme).with_viewport(420.0, 800.0);
assert_eq!(cx.viewport(), Some((420.0, 800.0)));
assert_eq!(cx.viewport_width(), Some(420.0));
assert_eq!(cx.viewport_height(), Some(800.0));
}
#[test]
fn viewport_below_uses_strict_less_than() {
let theme = Theme::default();
let cx = BuildCx::new(&theme).with_viewport(600.0, 800.0);
assert!(!cx.viewport_below(600.0), "boundary is exclusive");
assert!(cx.viewport_below(601.0));
assert!(!cx.viewport_below(599.0));
}
}