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, 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 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,
}
}
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,
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>,
}
#[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,
}
}
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 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 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,
}