#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
use accesskit_winit::Adapter;
use repose_core::a11y::ReposeActionHandler;
use repose_core::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use web_time::Instant;
#[cfg(target_os = "android")]
pub mod android;
#[cfg(target_arch = "wasm32")]
pub mod web;
#[cfg(target_arch = "wasm32")]
mod web_text_agent;
mod common;
pub mod gamepad;
pub mod render;
pub mod runner_common;
pub mod sensor;
pub mod window_v2;
use common as rc;
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
use crate::runner_common::{BackendRetry, BackendState, MAX_BACKEND_INIT_ATTEMPTS};
pub use render::{ImageHandleGuard, RenderCommand, RenderContext};
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
use winit::window::Window;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::OnceLock;
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
static APP_WINDOW: OnceLock<Arc<Window>> = OnceLock::new();
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
static WINDOW_VISIBLE: AtomicBool = AtomicBool::new(true);
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
static WINDOW_OCCLUDED: AtomicBool = AtomicBool::new(false);
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
static CLOSE_TO_TRAY: AtomicBool = AtomicBool::new(false);
#[cfg(not(target_arch = "wasm32"))]
static EVENT_LOOP_PROXY: OnceLock<winit::event_loop::EventLoopProxy<()>> = OnceLock::new();
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
static ABOUT_TO_WAIT_CALLBACK: Mutex<Option<Box<dyn Fn() + Send>>> = Mutex::new(None);
pub use repose_app::lifecycle::{
AppLifecycle, add_deeplink_listener, add_lifecycle_listener, current_lifecycle,
process_deeplinks, process_lifecycle, remove_deeplink_listener, remove_lifecycle_listener,
run_pre_redraw, set_on_deeplink, set_on_lifecycle, set_pre_redraw,
};
#[cfg(target_os = "android")]
pub(crate) fn push_lifecycle(state: AppLifecycle) {
repose_app::lifecycle::push_lifecycle(state);
#[cfg(not(target_arch = "wasm32"))]
wake_event_loop();
}
#[cfg(not(target_os = "android"))]
pub fn push_lifecycle(state: AppLifecycle) {
repose_app::lifecycle::push_lifecycle(state);
#[cfg(not(target_arch = "wasm32"))]
wake_event_loop();
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
fn push_runtime_window_lifecycle(rt: &mut repose_app::ReposeRuntime, visible: bool) {
let foreground = visible && !WINDOW_OCCLUDED.load(Ordering::Relaxed);
let state = if foreground {
AppLifecycle::Foreground
} else {
AppLifecycle::Background
};
if rt.current_lifecycle() != Some(state) {
rt.push_lifecycle(state);
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn push_window_lifecycle(visible: bool) {
let foreground = visible && !WINDOW_OCCLUDED.load(Ordering::Relaxed);
let state = if foreground {
AppLifecycle::Foreground
} else {
AppLifecycle::Background
};
if current_lifecycle() != Some(state) {
push_lifecycle(state);
}
}
pub fn push_deeplink(data: Vec<u8>) {
repose_app::lifecycle::push_deeplink(data);
#[cfg(not(target_arch = "wasm32"))]
if let Some(proxy) = EVENT_LOOP_PROXY.get() {
let _ = proxy.send_event(());
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn set_app_window(window: Arc<Window>) {
let _ = APP_WINDOW.set(window);
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_event_loop_proxy(proxy: winit::event_loop::EventLoopProxy<()>) {
let _ = EVENT_LOOP_PROXY.set(proxy);
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn set_about_to_wait_callback(cb: Box<dyn Fn() + Send>) {
*ABOUT_TO_WAIT_CALLBACK
.lock()
.unwrap_or_else(|e| e.into_inner()) = Some(cb);
}
#[cfg(not(target_arch = "wasm32"))]
pub fn wake_event_loop() {
if let Some(proxy) = EVENT_LOOP_PROXY.get() {
let _ = proxy.send_event(());
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn show_app_window() {
WINDOW_VISIBLE.store(true, Ordering::Relaxed);
if let Some(w) = APP_WINDOW.get() {
log::info!("show_app_window: calling set_visible(true)");
w.set_visible(true);
#[allow(deprecated)]
w.focus_window();
}
push_window_lifecycle(true);
repose_core::frame_clock::request_frame();
wake_event_loop();
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn hide_app_window() {
WINDOW_VISIBLE.store(false, Ordering::Relaxed);
if let Some(w) = APP_WINDOW.get() {
log::info!("hide_app_window: calling set_visible(false)");
w.set_visible(false);
}
push_window_lifecycle(false);
repose_core::frame_clock::request_frame();
wake_event_loop();
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn window_is_visible() -> bool {
WINDOW_VISIBLE.load(Ordering::Relaxed)
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn set_close_to_tray(enabled: bool) {
CLOSE_TO_TRAY.store(enabled, Ordering::Relaxed);
}
pub use repose_ui::textfield::tf_ensure_visible_in_rect;
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
use common::{cursor_is_hidden, map_cursor};
pub use repose_app::{AndroidOptions, AppConfig, ReposeOptions};
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
#[deprecated(
note = "use run_desktop_app_with_config(root, AppConfig) instead; this may be removed in a future release"
)]
pub fn run_desktop_app(
root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
) -> anyhow::Result<()> {
run_desktop_app_with_config(root, AppConfig::default())
}
pub fn install_default_logger() {
let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
.try_init();
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn run_desktop_app_with_config(
root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
config: AppConfig,
) -> anyhow::Result<()> {
install_default_logger();
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::event_loop::EventLoop;
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes};
use repose_app::ReposeRuntime;
use repose_core::a11y::A11yTree;
struct ReposeActivationHandler {
active: Arc<AtomicBool>,
needs_full: Arc<AtomicBool>,
}
impl accesskit::ActivationHandler for ReposeActivationHandler {
fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
self.active.store(true, Ordering::Release);
self.needs_full.store(true, Ordering::Release);
wake_event_loop();
Some(A11yTree::initial_tree())
}
}
struct ReposeDeactivationHandler {
active: Arc<AtomicBool>,
}
impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
fn deactivate_accessibility(&mut self) {
self.active.store(false, Ordering::Release);
}
}
#[derive(Clone, Copy, PartialEq)]
struct ImeState {
allowed: bool,
purpose: repose_core::ImePurposeHint,
auto_correct: bool,
capitalization: repose_core::KeyboardCapitalization,
cursor_area: Option<(f64, f64, f64, f64)>,
}
impl Default for ImeState {
fn default() -> Self {
Self {
allowed: false,
purpose: repose_core::ImePurposeHint::Normal,
auto_correct: true,
capitalization: repose_core::KeyboardCapitalization::Unspecified,
cursor_area: None,
}
}
}
struct App {
root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
render: RenderContext,
window: Option<Arc<Window>>,
backend: Option<repose_render_wgpu::WgpuBackend>,
backend_lifecycle: crate::runner_common::BackendLifecycle,
backend_retry_at: Option<Instant>,
rt: ReposeRuntime,
input_fit: f32,
inspector: Option<repose_devtools::Inspector>,
msaa_samples: u32,
present_mode: PresentModePref,
window_title: String,
window_size: (u32, u32),
fullscreen: bool,
pending_dropped_files: Vec<std::path::PathBuf>,
pending_drop_pos_px: Option<(f32, f32)>,
external_file_drag: bool,
hovered_files: Vec<std::path::PathBuf>,
a11y: Box<dyn A11yBridge>,
accesskit_adapter: Option<Adapter>,
a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
a11y_tree: A11yTree,
a11y_active: Arc<AtomicBool>,
a11y_needs_full: Arc<AtomicBool>,
last_window_theme: Option<bool>,
custom_cursor: Option<(u64, winit::window::CustomCursor)>,
last_cursor_key: Option<(u8, u64)>,
frame_pacer: rc::FramePacer,
redraw_deferred: bool,
occluded: bool,
os_focused: bool,
ime_output_allowed: bool,
ime_state: ImeState,
touch_gestures: rc::TouchGestureState,
gamepad: Option<Box<dyn gamepad::GamepadBackend>>,
sensors: Option<Box<dyn sensor::SensorBackend>>,
sensor_drops: Vec<String>,
}
impl App {
fn process_a11y_actions(&mut self) {
let mut actions = self.a11y_actions.lock().unwrap_or_else(|e| e.into_inner());
if actions.is_empty() {
return;
}
let pending = actions.drain(..).collect::<Vec<_>>();
drop(actions);
let Some(f) = &self.rt.frame_cache else {
return;
};
for req in pending {
let target_id = req.target_node.0;
match req.action {
accesskit::Action::Click => {
if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
&& let Some(cb) = &hit.on_click
{
cb();
self.request_redraw();
}
}
accesskit::Action::Focus => {
let _ = repose_core::request_input_mode(repose_core::InputMode::Keyboard);
self.rt.sched.focused = Some(target_id);
self.request_redraw();
}
accesskit::Action::ScrollUp
| accesskit::Action::ScrollDown
| accesskit::Action::ScrollLeft
| accesskit::Action::ScrollRight => {
let delta = match req.action {
accesskit::Action::ScrollUp => repose_core::Vec2 { x: 0.0, y: -200.0 },
accesskit::Action::ScrollDown => repose_core::Vec2 { x: 0.0, y: 200.0 },
accesskit::Action::ScrollLeft => {
repose_core::Vec2 { x: -200.0, y: 0.0 }
}
_ => repose_core::Vec2 { x: 200.0, y: 0.0 },
};
if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id) {
if let Some(scroll) = &hit.on_scroll_with_source {
scroll(delta, repose_core::input::ScrollSource::Wheel);
self.request_redraw();
} else if let Some(scroll) = &hit.on_scroll {
scroll(delta);
self.request_redraw();
}
}
}
accesskit::Action::SetValue => {
let value = match &req.data {
Some(accesskit::ActionData::NumericValue(v)) => Some(*v as f32),
Some(accesskit::ActionData::Value(v)) => v.parse::<f32>().ok(),
_ => None,
};
if let Some(value) = value
&& let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
&& let Some(cb) = &hit.on_set_progress
{
cb(value);
self.request_redraw();
}
}
accesskit::Action::Increment | accesskit::Action::Decrement => {
if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
&& let Some(cb) = &hit.on_set_progress
{
let dir = if matches!(req.action, accesskit::Action::Increment) {
1.0
} else {
-1.0
};
let value = f
.semantics_nodes
.iter()
.find(|n| n.id == target_id)
.and_then(|n| n.progress)
.map(|p| {
p.current + dir * p.step.unwrap_or((p.max - p.min) / 20.0)
});
if let Some(value) = value {
cb(value);
self.request_redraw();
}
}
}
accesskit::Action::ReplaceSelectedText => {
if let Some(accesskit::ActionData::Value(text)) = &req.data
&& let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
&& let Some(cb) = &hit.on_text_change
{
cb(text.to_string());
self.request_redraw();
}
}
accesskit::Action::Expand => {
if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
&& let Some(cb) = &hit.on_expand
{
cb();
self.request_redraw();
}
}
accesskit::Action::Collapse => {
if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
&& let Some(cb) = &hit.on_collapse
{
cb();
self.request_redraw();
}
}
_ => {}
}
}
}
fn new(
root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
config: AppConfig,
) -> Self {
Self {
root,
render: RenderContext::new(),
window: None,
backend: None,
backend_lifecycle: crate::runner_common::BackendLifecycle::new(),
backend_retry_at: None,
rt: ReposeRuntime::new(),
input_fit: 1.0,
inspector: if config.enable_inspector {
Some(repose_devtools::Inspector::new())
} else {
None
},
msaa_samples: config.common.msaa_samples,
present_mode: config.common.present_mode,
window_title: config.window_title,
window_size: config.window_size,
fullscreen: config.fullscreen,
pending_dropped_files: Vec::new(),
pending_drop_pos_px: None,
external_file_drag: false,
hovered_files: Vec::new(),
a11y: {
#[cfg(target_os = "linux")]
{
Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
}
#[cfg(not(target_os = "linux"))]
{
Box::new(NoopA11y) as Box<dyn A11yBridge>
}
},
accesskit_adapter: None,
a11y_actions: Arc::new(Mutex::new(Vec::new())),
a11y_tree: A11yTree::default(),
a11y_active: Arc::new(AtomicBool::new(false)),
a11y_needs_full: Arc::new(AtomicBool::new(false)),
frame_pacer: rc::FramePacer::new(config.common.max_fps),
redraw_deferred: false,
occluded: false,
os_focused: true,
ime_output_allowed: false,
ime_state: ImeState::default(),
last_window_theme: None,
custom_cursor: None,
last_cursor_key: None,
touch_gestures: rc::TouchGestureState::default(),
gamepad: gamepad::create_backend()
.map(|b| Box::new(b) as Box<dyn gamepad::GamepadBackend>),
sensors: sensor::create_backend()
.map(|b| Box::new(b) as Box<dyn sensor::SensorBackend>),
sensor_drops: Vec::new(),
}
}
fn request_redraw(&self) {
repose_core::request_frame();
}
fn ime_allowed_for_frame(&self, frame: &repose_core::runtime::Frame) -> bool {
WINDOW_VISIBLE.load(Ordering::Relaxed)
&& !WINDOW_OCCLUDED.load(Ordering::Relaxed)
&& self.ime_output_allowed
&& self.rt.sched.window_focused
&& self.rt.sched.focused.is_some_and(|id| {
rc::is_editable_textfield_hit(frame, id) || rc::has_ime_handler(frame, id)
})
}
fn ime_allowed_for_output(
&self,
hits: &[repose_core::HitRegion],
semantics: &[repose_core::runtime::SemNode],
output_allowed: bool,
) -> bool {
WINDOW_VISIBLE.load(Ordering::Relaxed)
&& !WINDOW_OCCLUDED.load(Ordering::Relaxed)
&& output_allowed
&& self.rt.sched.window_focused
&& self.rt.sched.focused.is_some_and(|id| {
rc::editable_textfield_hit(hits, semantics, id).is_some()
|| rc::has_ime_handler_hit(hits, id)
})
}
fn sync_ime_for_focus(&mut self) {
let hit = self.rt.frame_cache.as_ref().and_then(|frame| {
(self.ime_allowed_for_frame(frame))
.then(|| {
self.rt.sched.focused.and_then(|id| {
rc::editable_textfield_hit(
&frame.hit_regions,
&frame.semantics_nodes,
id,
)
})
})
.flatten()
});
let state = match hit {
Some(hit) => {
let scale = self.window.as_ref().map(|window| window.scale_factor());
ImeState {
allowed: true,
purpose: hit.keyboard_type.ime_purpose_hint(),
auto_correct: hit.auto_correct.unwrap_or(true),
capitalization: hit.capitalization,
cursor_area: scale.map(|scale| {
(
hit.rect.x as f64 / scale,
hit.rect.y as f64 / scale,
hit.rect.w as f64 / scale,
hit.rect.h as f64 / scale,
)
}),
}
}
None => ImeState::default(),
};
self.apply_ime_state(state);
}
fn apply_ime_state(&mut self, state: ImeState) {
if self.ime_state == state {
return;
}
let Some(window) = self.window.clone() else {
return;
};
self.ime_state = state;
if state.allowed {
rc::set_ime_for_textfield_ex(
&window,
true,
state.purpose,
state.auto_correct,
state.capitalization,
);
if let Some((x, y, w, h)) = state.cursor_area {
window.set_ime_cursor_area(LogicalPosition::new(x, y), LogicalSize::new(w, h));
}
} else {
rc::set_ime_for_textfield(&window, false);
self.rt.finish_compositions();
}
}
fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
if self.rt.dispatch_action(action) {
self.sync_ime_for_focus();
return true;
}
false
}
fn paste_from_primary(&self) -> Option<String> {
let mut opts = clipawl::ClipboardOptions::default();
opts.linux.selection = clipawl::LinuxSelection::Primary;
if let Ok(cb) = clipawl::Clipboard::new_with_options(opts) {
match pollster::block_on(cb.read()) {
Ok(t) => Some(t),
Err(e) => {
eprintln!("Primary paste error: {}", e);
None
}
}
} else {
None
}
}
fn process_render_commands(&mut self) {
let Some(backend) = self.backend.as_mut() else {
return;
};
repose_render_wgpu::apply_render_commands(backend, self.render.drain()).log_failures();
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
fn custom_cursor_key(img: &repose_core::CustomCursorImage) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for b in img.rgba.iter().copied().chain(
img.size
.iter()
.flat_map(|v| v.to_le_bytes())
.chain(img.hotspot.iter().flat_map(|v| v.to_le_bytes())),
) {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
h ^= (img.rgba.len() as u64).wrapping_mul(0x9e3779b97f4a7c15);
h
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
fn cursor_key(c: &repose_core::CursorIcon) -> (u8, u64) {
match c {
repose_core::CursorIcon::Default => (0, 0),
repose_core::CursorIcon::Pointer => (1, 0),
repose_core::CursorIcon::Text => (2, 0),
repose_core::CursorIcon::EwResize => (3, 0),
repose_core::CursorIcon::NsResize => (4, 0),
repose_core::CursorIcon::NwseResize => (5, 0),
repose_core::CursorIcon::NeswResize => (6, 0),
repose_core::CursorIcon::Grab => (7, 0),
repose_core::CursorIcon::Grabbing => (8, 0),
repose_core::CursorIcon::Hidden => (9, 0),
repose_core::CursorIcon::Custom(img) => (10, Self::custom_cursor_key(img)),
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
fn apply_cursor_static(
win: &std::sync::Arc<winit::window::Window>,
cache: &mut Option<(u64, winit::window::CustomCursor)>,
last_key: &mut Option<(u8, u64)>,
el: Option<&winit::event_loop::ActiveEventLoop>,
c: &repose_core::CursorIcon,
) {
let key = Self::cursor_key(c);
if *last_key == Some(key) {
return;
}
*last_key = Some(key);
if let repose_core::CursorIcon::Custom(img) = c {
let hash = key.1;
let hit = cache
.as_ref()
.filter(|(k, _)| *k == hash)
.map(|(_, h)| h.clone());
let custom = match hit {
Some(h) => Some(h),
None => match el {
Some(el) => {
match winit::window::CustomCursor::from_rgba(
img.rgba.to_vec(),
img.size[0],
img.size[1],
img.hotspot[0],
img.hotspot[1],
) {
Ok(source) => {
let h = el.create_custom_cursor(source);
*cache = Some((hash, h.clone()));
Some(h)
}
Err(e) => {
log::warn!("invalid custom cursor, falling back: {e:?}");
*cache = None;
None
}
}
}
None => cache.as_ref().map(|(_, h)| h.clone()),
},
};
win.set_cursor_visible(true);
match custom {
Some(h) => win.set_cursor(h),
None => win.set_cursor(map_cursor(c)),
}
return;
}
*cache = cache.take().and(None);
win.set_cursor_visible(!cursor_is_hidden(c));
win.set_cursor(map_cursor(c));
}
fn clear_input_state(&mut self) {
let active_touches = self
.touch_gestures
.active_touches()
.iter()
.map(|(id, pos)| (*id, *pos))
.collect::<Vec<_>>();
for (touch_id, pos) in active_touches {
self.touch_gestures
.touch_ended(&mut self.rt, touch_id, pos, true);
self.touch_gestures.contact_up(touch_id);
}
self.touch_gestures = rc::TouchGestureState::default();
self.rt.handle_focus_lost();
self.rt.touch_paths.clear();
self.rt.scroll_capture_id = None;
self.rt.hover_id = None;
self.rt.hover_ancestors.clear();
self.rt.last_focus = None;
self.rt.sched.pointer_pos_px = None;
self.rt.ime_preedit = false;
self.rt.modifiers = Default::default();
self.rt.sched.window_focused = false;
self.rt.sched.held_keys.clear();
self.rt.sched.mouse_primary = false;
self.rt.sched.mouse_secondary = false;
self.rt.sched.mouse_middle = false;
self.rt.pressed_ids.clear();
self.rt.key_pressed_active = None;
self.external_file_drag = false;
self.hovered_files.clear();
self.apply_ime_state(ImeState::default());
}
fn reset_pointer_state(&mut self) {
self.rt.capture_id = None;
self.rt.pressed_ids.clear();
self.rt.hover_id = None;
}
}
impl ApplicationHandler<()> for App {
fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
let _event_scope =
repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
self.occluded = false;
WINDOW_OCCLUDED.store(false, Ordering::Relaxed);
self.rt.sched.window_focused = self.os_focused;
let _ = rc::setup_clipboard();
if self.window.is_none() {
let mut attributes = WindowAttributes::default()
.with_title(self.window_title.clone())
.with_inner_size(PhysicalSize::new(self.window_size.0, self.window_size.1))
.with_visible(false);
if self.fullscreen {
attributes = attributes
.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
}
match el.create_window(attributes) {
Ok(win) => {
let w = Arc::new(win);
let activation_handler = ReposeActivationHandler {
active: self.a11y_active.clone(),
needs_full: self.a11y_needs_full.clone(),
};
let action_handler = ReposeActionHandler {
pending_actions: self.a11y_actions.clone(),
};
let deactivation_handler = ReposeDeactivationHandler {
active: self.a11y_active.clone(),
};
let adapter = Adapter::with_direct_handlers(
el,
&w,
activation_handler,
action_handler,
deactivation_handler,
);
self.accesskit_adapter = Some(adapter);
w.set_visible(true);
let size = w.inner_size();
let sf = w.scale_factor() as f32;
self.input_fit = rc::sync_viewport(&mut self.rt, &mut None, size, sf);
match repose_render_wgpu::WgpuBackend::new_with_options(
w.clone(),
Some(el.owned_display_handle()),
self.msaa_samples,
self.present_mode,
) {
Ok(mut b) => {
b.set_pixels_per_point(sf);
repose_render_wgpu::offscreen::set_shared_device(
b.device.clone(),
b.queue.clone(),
);
self.backend = Some(b);
set_app_window(w.clone());
self.window = Some(w);
self.request_redraw();
}
Err(e) => {
log::error!("Failed to create WGPU backend: {e:?}");
el.exit();
}
}
}
Err(e) => {
log::error!("Failed to create window: {e:?}");
el.exit();
}
}
}
if self.window.is_some() {
push_runtime_window_lifecycle(&mut self.rt, WINDOW_VISIBLE.load(Ordering::Relaxed));
}
}
fn window_event(
&mut self,
el: &winit::event_loop::ActiveEventLoop,
_id: winit::window::WindowId,
event: WindowEvent,
) {
let _event_scope =
repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
let _dnd_guard = self.rt.dnd_context.enter();
if let (Some(adapter), Some(window)) = (&mut self.accesskit_adapter, &self.window) {
adapter.process_event(window, &event);
}
match event {
WindowEvent::CloseRequested => {
if CLOSE_TO_TRAY.load(Ordering::Relaxed) {
self.backend = None;
if let Some(w) = &self.window {
w.set_visible(false);
}
WINDOW_VISIBLE.store(false, Ordering::Relaxed);
push_runtime_window_lifecycle(&mut self.rt, false);
} else {
el.exit();
}
}
WindowEvent::Focused(focused) => {
self.os_focused = focused;
let focused = focused && !self.occluded;
self.rt.sched.window_focused = focused;
if !focused {
self.clear_input_state();
} else {
self.sync_ime_for_focus();
}
self.request_redraw();
}
WindowEvent::Occluded(occluded) => {
self.occluded = occluded;
WINDOW_OCCLUDED.store(occluded, Ordering::Relaxed);
if occluded {
self.rt.sched.window_focused = false;
self.clear_input_state();
} else {
self.rt.sched.window_focused = self.os_focused;
if self.os_focused {
self.sync_ime_for_focus();
}
}
push_runtime_window_lifecycle(
&mut self.rt,
WINDOW_VISIBLE.load(Ordering::Relaxed),
);
self.request_redraw();
}
WindowEvent::CursorLeft { .. } => {
self.rt.pointer_inside = false;
self.rt.handle_pointer_cancel();
self.external_file_drag = false;
self.hovered_files.clear();
self.request_redraw();
}
WindowEvent::HoveredFile(path) => {
self.external_file_drag = true;
if self.hovered_files.len() < 32 {
self.hovered_files.push(path);
}
if self.pending_drop_pos_px.is_none() {
self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
}
self.request_redraw();
}
WindowEvent::HoveredFileCancelled => {
self.external_file_drag = false;
self.hovered_files.clear();
self.reset_pointer_state();
self.request_redraw();
}
WindowEvent::DroppedFile(path) => {
self.pending_dropped_files.push(path);
if self.pending_drop_pos_px.is_none() {
self.pending_drop_pos_px = Some(self.rt.mouse_pos_px);
}
self.external_file_drag = false;
self.hovered_files.clear();
self.request_redraw();
}
WindowEvent::Resized(size) => {
let sf = self
.window
.as_ref()
.map(|w| w.scale_factor() as f32)
.unwrap_or(1.0);
self.input_fit = rc::sync_viewport(&mut self.rt, &mut self.backend, size, sf);
if let Some(w) = &self.window {
let sf = w.scale_factor() as f32;
let dp_w = size.width as f32 / sf;
let dp_h = size.height as f32 / sf;
log::info!(
"Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
size.width,
size.height,
sf,
dp_w as i32,
dp_h as i32
);
}
self.request_redraw();
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
let size = self
.window
.as_ref()
.map(|w| w.inner_size())
.unwrap_or_else(|| PhysicalSize::new(0, 0));
self.input_fit = rc::sync_viewport(
&mut self.rt,
&mut self.backend,
size,
scale_factor as f32,
);
self.request_redraw();
}
WindowEvent::CursorMoved { position, .. } => {
self.rt.pointer_inside = true;
if self.external_file_drag {
let k = self.input_fit;
self.pending_drop_pos_px =
Some((position.x as f32 * k, position.y as f32 * k));
}
let pos = Vec2 {
x: position.x as f32 * self.input_fit,
y: position.y as f32 * self.input_fit,
};
let result = self.rt.handle_pointer_move(pos);
if let (Some(inspector), Some(f)) = (&mut self.inspector, &self.rt.frame_cache)
&& inspector.hud.inspector_enabled
{
let hit = f.hit_regions.iter().find(|h| h.rect.contains(pos));
let hover_rect = hit.map(|h| h.rect);
let hover_info = hit.and_then(|h| {
f.semantics_nodes.iter().find(|s| s.id == h.id).map(|s| {
repose_devtools::HoveredInfo {
id: s.id,
role: format!("{:?}", s.role),
label: s.label.clone(),
}
})
});
inspector.hud.set_hovered(hover_rect, hover_info);
}
if let Some(win) = &self.window
&& let Some(c) = result.cursor
{
Self::apply_cursor_static(
win,
&mut self.custom_cursor,
&mut self.last_cursor_key,
Some(el),
&c,
);
}
self.request_redraw();
}
WindowEvent::MouseWheel { delta, .. } => {
let scale = self.rt.scale;
let k = self.input_fit;
let delta = match delta {
winit::event::MouseScrollDelta::PixelDelta(p) => {
winit::event::MouseScrollDelta::PixelDelta(
winit::dpi::PhysicalPosition::new(p.x * k as f64, p.y * k as f64),
)
}
other => other,
};
if crate::runner_common::on_mouse_wheel(&mut self.rt, delta, scale) {
self.request_redraw();
}
}
WindowEvent::MouseInput { state, button, .. } => {
let pos = Vec2 {
x: self.rt.mouse_pos_px.0,
y: self.rt.mouse_pos_px.1,
};
let mapped = match button {
MouseButton::Left => PointerButton::Primary,
MouseButton::Right => PointerButton::Secondary,
MouseButton::Middle => PointerButton::Tertiary,
_ => return,
};
match (mapped, state) {
(PointerButton::Primary, ElementState::Pressed) => {
self.rt.sched.mouse_primary = true;
}
(PointerButton::Primary, ElementState::Released) => {
self.rt.sched.mouse_primary = false;
}
(PointerButton::Secondary, ElementState::Pressed) => {
self.rt.sched.mouse_secondary = true;
}
(PointerButton::Secondary, ElementState::Released) => {
self.rt.sched.mouse_secondary = false;
}
(PointerButton::Tertiary, ElementState::Pressed) => {
self.rt.sched.mouse_middle = true;
}
(PointerButton::Tertiary, ElementState::Released) => {
self.rt.sched.mouse_middle = false;
}
}
match state {
ElementState::Pressed => {
let result = self.rt.handle_pointer_press(pos, mapped);
self.sync_ime_for_focus();
if result.focused.is_none() {
self.rt.ime_preedit = false;
}
if result.needs_a11y_announce {
self.announce_focus_change();
}
if matches!(mapped, PointerButton::Tertiary)
&& let Some(f) = &self.rt.frame_cache
&& let Some(cid) = self.rt.capture_id
&& let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
&& self.rt.sched.window_focused
&& rc::is_editable_textfield_hit(f, hit.id)
&& let Some(txt) = self.paste_from_primary()
{
self.rt.paste_into_focused(&txt);
}
if matches!(mapped, PointerButton::Primary | PointerButton::Secondary)
&& let Some(inspector) = &mut self.inspector
&& inspector.hud.inspector_enabled
&& let Some(f) = &self.rt.frame_cache
&& let Some(hit) =
f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
{
let info =
f.semantics_nodes.iter().find(|s| s.id == hit.id).map(|s| {
repose_devtools::HoveredInfo {
id: s.id,
role: format!("{:?}", s.role),
label: s.label.clone(),
}
});
inspector
.hud
.select_widget(repose_devtools::SelectedWidget {
id: hit.id,
role: info
.as_ref()
.map(|i| i.role.clone())
.unwrap_or_default(),
label: info.as_ref().and_then(|i| i.label.clone()),
bounds: hit.rect,
});
}
self.request_redraw();
}
ElementState::Released => {
let result = self.rt.handle_pointer_release(pos, mapped);
if let Some(cid) = result.clicked_id
&& let Some(f) = &self.rt.frame_cache
&& let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid)
{
let label = node.label.as_deref().unwrap_or("");
self.a11y.announce(&format!("Activated {}", label));
}
self.request_redraw();
}
}
}
WindowEvent::Touch(mut t) => {
let scale = self.rt.scale;
let k = self.input_fit as f64;
t.location.x *= k;
t.location.y *= k;
let r = crate::runner_common::handle_touch_raw(
&mut self.rt,
&mut self.touch_gestures,
&t,
scale,
);
if matches!(
t.phase,
winit::event::TouchPhase::Started | winit::event::TouchPhase::Ended
) {
self.sync_ime_for_focus();
}
let mut dirty = r.dirty;
if let Some((delta_scale, center)) = r.pinch
&& self.dispatch_action(repose_core::shortcuts::Action::Gesture(
repose_core::shortcuts::Gesture::PinchWithCenter {
delta_scale,
center,
},
))
{
dirty = true;
}
if let Some((delta, center)) = r.pan
&& self.dispatch_action(repose_core::shortcuts::Action::Gesture(
repose_core::shortcuts::Gesture::Pan { delta, center },
))
{
dirty = true;
}
if let Some((delta_rotation, center)) = r.rotation
&& self.dispatch_action(repose_core::shortcuts::Action::Gesture(
repose_core::shortcuts::Gesture::Rotate {
delta_rotation,
center,
},
))
{
dirty = true;
}
if let Some(right) = r.swipe_right {
let g = if right {
repose_core::shortcuts::Gesture::SwipeRight
} else {
repose_core::shortcuts::Gesture::SwipeLeft
};
if self.dispatch_action(repose_core::shortcuts::Action::Gesture(g)) {
dirty = true;
}
}
if dirty {
self.request_redraw();
}
}
WindowEvent::ModifiersChanged(new_mods) => {
crate::runner_common::on_modifiers_changed(&mut self.rt, &new_mods.state());
}
WindowEvent::KeyboardInput {
event: key_event, ..
} => {
if crate::runner_common::on_keyboard_input(
&mut self.rt,
&key_event,
&mut self.inspector,
) {
self.request_redraw();
return;
}
if key_event.state == ElementState::Pressed
&& !key_event.repeat
&& (rc::is_back_key(&key_event) || rc::is_escape_key(&key_event))
{
if self.rt.overlay.handle_back() {
self.request_redraw();
return;
}
use repose_navigation::back;
if back::handle() {
self.request_redraw();
return;
}
if rc::is_back_key(&key_event) {
el.exit();
return;
}
}
if key_event.state == ElementState::Released
&& let Some(active_id) = self.rt.key_pressed_active
{
match key_event.physical_key {
PhysicalKey::Code(KeyCode::Space)
| PhysicalKey::Code(KeyCode::Enter) => {
if let Some(f) = &self.rt.frame_cache
&& let Some(node) =
f.semantics_nodes.iter().find(|n| n.id == active_id)
{
let label = node.label.as_deref().unwrap_or("");
self.a11y.announce(&format!("Activated {}", label));
}
}
_ => {}
}
}
}
WindowEvent::Ime(ime) => {
crate::runner_common::on_ime(&mut self.rt, &ime);
self.request_redraw();
}
WindowEvent::RedrawRequested => {
crate::run_pre_redraw(&self.render);
let now = Instant::now();
let has_frame = self.rt.frame_cache.is_some();
if (has_frame || repose_core::frame_clock::peek_frame_request())
&& !self.frame_pacer.due(now)
{
self.redraw_deferred = true;
if let Some(deadline) = self.frame_pacer.deadline(now) {
el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
deadline,
));
}
return;
}
self.redraw_deferred = false;
let compose_requested = take_frame_request();
self.process_a11y_actions();
self.process_render_commands();
let Some(win) = self.window.clone() else {
return;
};
if !compose_requested {
if let (Some(backend), Some(frame)) =
(self.backend.as_mut(), self.rt.frame_cache.as_ref())
{
let scale = win.scale_factor() as f32;
let inspector_active = self
.inspector
.as_ref()
.is_some_and(|inspector| inspector.hud.inspector_enabled);
let _dnd_guard = self.rt.dnd_context.enter();
let drag_active =
repose_core::dnd::is_dragging() || self.external_file_drag;
let mut overlay =
(inspector_active || drag_active).then(|| frame.scene.clone());
if let Some(scene) = &mut overlay {
if let Some(inspector) = &mut self.inspector {
inspector.frame(scene);
}
repose_core::dnd::overlay_drag_indicator(
scene,
self.rt.mouse_pos_px,
self.external_file_drag,
);
}
let _ = take_present_request();
let presented = backend.frame(
overlay.as_ref().unwrap_or(&frame.scene),
GlyphRasterConfig {
px: Px(18.0 * scale),
},
);
if presented {
self.frame_pacer.rendered(Instant::now());
}
}
return;
}
log::trace!("RedrawRequested: frame request pending, composing");
if self.backend.is_none() {
return;
}
repose_core::animation_driver::tick();
let t0 = Instant::now();
let scale = self.rt.scale;
let output = self.rt.frame(&mut self.root, &self.render);
if let Some(cursor) = &output.platform.cursor {
let win = win.clone();
Self::apply_cursor_static(
&win,
&mut self.custom_cursor,
&mut self.last_cursor_key,
Some(el),
cursor,
);
}
if let Some(fullscreen) = self.rt.take_fullscreen_request()
&& fullscreen != self.fullscreen
{
self.fullscreen = fullscreen;
let _ = win.set_fullscreen(if fullscreen {
Some(winit::window::Fullscreen::Borderless(None))
} else {
None
});
}
if let Some(dark) = output.platform.window_theme_dark
&& self.last_window_theme != Some(dark)
{
win.set_theme(Some(if dark {
winit::window::Theme::Dark
} else {
winit::window::Theme::Light
}));
self.last_window_theme = Some(dark);
}
self.ime_output_allowed = output.platform.ime_allowed;
let ime_allowed = self.ime_allowed_for_output(
&output.hit_regions,
&output.semantics_nodes,
output.platform.ime_allowed,
);
self.apply_ime_state(if ime_allowed {
ImeState {
allowed: true,
purpose: output.platform.ime_purpose,
auto_correct: output.platform.ime_auto_correct,
capitalization: output.platform.ime_capitalization,
cursor_area: output.platform.ime_cursor_area,
}
} else {
ImeState::default()
});
let frame = output.into_shared_frame();
let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
if self.a11y_active.load(Ordering::Acquire) {
if self.a11y_needs_full.swap(false, Ordering::AcqRel) {
self.a11y_tree = A11yTree::default();
}
if let Some(adapter) = &mut self.accesskit_adapter {
let scale = win.scale_factor();
if let Some(update) = self.a11y_tree.update(
&frame.semantics_nodes,
scale,
self.rt.sched.focused,
) {
adapter.update_if_active(|| update);
}
}
}
self.rt.after_compose_shared(frame.clone(), scale);
let inspector_active = self
.inspector
.as_ref()
.is_some_and(|inspector| inspector.hud.inspector_enabled);
let _dnd_guard = self.rt.dnd_context.enter();
let drag_active = repose_core::dnd::is_dragging() || self.external_file_drag;
let mut overlay =
(inspector_active || drag_active).then(|| frame.scene.clone());
let scene = overlay.as_ref().unwrap_or(&frame.scene);
if let Some(inspector) = &mut self.inspector {
let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
let signal_count = self.rt.sched.id_count() as usize;
let ls = repose_ui::last_layout_stats();
inspector.hud.metrics = Some(repose_devtools::Metrics {
build_ms: build_layout_ms,
layout_ms: ls.layout_time_ms,
paint_ms: ls.paint_time_ms,
scene_nodes: scene.nodes.len(),
widget_count,
signal_count,
taffy_created: ls.taffy_created,
taffy_reused: ls.taffy_reused,
layout_hits: ls.layout_hits,
layout_misses: ls.layout_misses,
paint_cache_hits: ls.paint_cache_hits,
paint_cache_misses: ls.paint_cache_misses,
paint_culled: ls.paint_culled,
});
if let Some(scene) = &mut overlay {
inspector.frame(scene);
}
}
if let Some(scene) = &mut overlay {
repose_core::dnd::overlay_drag_indicator(
scene,
self.rt.mouse_pos_px,
self.external_file_drag,
);
}
self.process_render_commands();
let _ = take_present_request();
let presented = if let Some(backend) = self.backend.as_mut() {
backend.frame(
overlay.as_ref().unwrap_or(&frame.scene),
GlyphRasterConfig {
px: Px(18.0 * scale),
},
)
} else {
false
};
self.rt.cache_frame(frame);
self.dispatch_file_drop_now();
self.rt.tick_overlays();
if presented {
self.frame_pacer.rendered(Instant::now());
}
}
_ => {}
}
}
fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
let _event_scope =
repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
if let Some(cb) = ABOUT_TO_WAIT_CALLBACK
.lock()
.unwrap_or_else(|e| e.into_inner())
.as_ref()
{
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(cb));
if let Err(e) = res {
log::error!(
"ABOUT_TO_WAIT_CALLBACK panicked: {}",
e.downcast_ref::<String>()
.map(|s| s.as_str())
.or_else(|| e.downcast_ref::<&str>().copied())
.unwrap_or("unknown")
);
}
}
self.rt.process_deeplinks();
self.rt.process_lifecycle();
if let Some(backend) = &mut self.gamepad {
for ev in backend.poll() {
self.rt.handle_gamepad(&ev);
}
for (id, low, high, duration_ms) in self.rt.take_rumble_requests() {
use repose_core::input::GamepadId;
let gid = GamepadId(id);
if duration_ms == 0 {
backend.stop_rumble(gid);
} else if !backend.set_rumble(gid, low, high, duration_ms) {
log::warn!("gamepad: rumble not supported for pad {id}");
}
}
} else {
self.rt.take_rumble_requests();
}
if let Some(backend) = &mut self.sensors {
for reading in backend.poll_sensors() {
match self.rt.gamepad_id_by_sensor_device(&reading.device) {
Some(id) => self.rt.handle_sensor_sample(id, reading.sample),
None if !self.sensor_drops.contains(&reading.device) => {
self.sensor_drops.push(reading.device.clone());
log::warn!(
"sensor: no connected pad named {:?}, dropping samples",
reading.device
);
}
None => {}
}
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
if let Some(reason) = self.backend.as_ref().and_then(|b| b.take_device_lost()) {
log::error!("GPU device lost, rebuilding backend: {reason}");
self.backend = None;
self.backend_retry_at = None;
self.backend_lifecycle.begin();
self.rt.frame_cache = None;
self.request_redraw();
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
if WINDOW_VISIBLE.load(Ordering::Relaxed)
&& !WINDOW_OCCLUDED.load(Ordering::Relaxed)
&& self.backend.is_none()
&& self.backend_lifecycle.state() != BackendState::Unavailable
&& let Some(w) = &self.window
{
let now = Instant::now();
if let Some(retry_at) = self.backend_retry_at
&& now < retry_at
{
el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(retry_at));
return;
}
self.backend_retry_at = None;
log::info!("about_to_wait: recreating GPU backend");
match repose_render_wgpu::WgpuBackend::new_with_options(
w.clone(),
Some(el.owned_display_handle()),
self.msaa_samples,
self.present_mode,
) {
Ok(b) => {
let size = w.inner_size();
let scale = w.scale_factor() as f32;
let mut backend = Some(b);
self.input_fit = rc::sync_viewport(&mut self.rt, &mut backend, size, scale);
let Some(b) = backend.take() else {
unreachable!()
};
repose_render_wgpu::offscreen::set_shared_device(
b.device.clone(),
b.queue.clone(),
);
self.backend = Some(b);
self.backend_lifecycle.ready();
self.request_redraw();
}
Err(e) => match self.backend_lifecycle.failed() {
BackendRetry::RetryAfter(delay) => {
self.backend_retry_at = Some(now + delay);
log::error!(
"about_to_wait: failed to recreate backend, retrying in \
{delay:?}: {e:?}"
);
}
BackendRetry::Unavailable => log::error!(
"about_to_wait: GPU backend permanently unavailable after {} \
attempts, giving up: {e:?}",
MAX_BACKEND_INIT_ATTEMPTS
),
BackendRetry::Ready => unreachable!(),
},
}
}
if self.redraw_deferred {
let now = Instant::now();
if self.frame_pacer.due(now) {
self.redraw_deferred = false;
rc::request_redraw(&self.window);
} else if let Some(deadline) = self.frame_pacer.deadline(now) {
el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
} else {
self.redraw_deferred = false;
rc::request_redraw(&self.window);
}
return;
}
let now = Instant::now();
let idle_cap = web_time::Duration::from_millis(1000);
let idle_deadline = now + idle_cap;
let wakeup_due = self.rt.is_wakeup_due(now);
if wakeup_due
|| self.frame_pacer.idle_due(now, idle_cap)
|| repose_core::animation_driver::is_active()
|| (self.a11y_active.load(Ordering::Acquire)
&& self.a11y_needs_full.load(Ordering::Acquire))
{
request_frame();
}
let wakeup_deadline = self
.rt
.next_wakeup_deadline()
.filter(|deadline| *deadline > now);
if wakeup_due {
rc::request_redraw(&self.window);
return;
}
let compose_requested = repose_core::frame_clock::peek_frame_request();
let present_requested = repose_core::frame_clock::peek_present_request();
if compose_requested {
if self.frame_pacer.due(now) {
rc::request_redraw(&self.window);
} else if let Some(deadline) = self
.frame_pacer
.deadline(now)
.into_iter()
.chain(wakeup_deadline)
.chain(Some(idle_deadline))
.min()
{
el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
}
return;
}
if present_requested && self.rt.frame_cache.is_some() {
if self.frame_pacer.due(now) {
rc::request_redraw(&self.window);
} else if let Some(deadline) = self
.frame_pacer
.deadline(now)
.into_iter()
.chain(wakeup_deadline)
.chain(Some(idle_deadline))
.min()
{
el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
}
return;
}
let deadline = wakeup_deadline.unwrap_or(idle_deadline);
el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(deadline));
}
fn new_events(
&mut self,
_: &winit::event_loop::ActiveEventLoop,
_: winit::event::StartCause,
) {
}
fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
request_frame();
}
fn device_event(
&mut self,
_: &winit::event_loop::ActiveEventLoop,
_: winit::event::DeviceId,
_: winit::event::DeviceEvent,
) {
}
fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {
let _event_scope =
repose_app::lifecycle::enter_dispatchers(self.rt.event_dispatchers());
push_runtime_window_lifecycle(&mut self.rt, false);
}
fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {
#[cfg(not(target_arch = "wasm32"))]
rc::flush_clipboard_writes();
repose_core::shutdown_composition();
}
fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {
repose_core::runtime::COMPOSER.with(|c| {
let mut c = c.borrow_mut();
let n = c.cursor;
if c.slots.len() > n {
c.slots.truncate(n);
}
if c.slot_callers.len() > n {
c.slot_callers.truncate(n);
}
});
repose_core::scope_cache::gc_dead_scopes();
}
}
impl App {
fn announce_focus_change(&mut self) {
if let Some(f) = &self.rt.frame_cache {
let focused_node = self
.rt
.sched
.focused
.and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
self.a11y.focus_changed(focused_node);
}
}
fn dispatch_file_drop_now(&mut self) {
let Some(f) = &self.rt.frame_cache else {
self.pending_dropped_files.clear();
self.pending_drop_pos_px = None;
return;
};
if self.pending_dropped_files.is_empty() {
return;
}
let pos_px = self.pending_drop_pos_px.unwrap_or(self.rt.mouse_pos_px);
let pos = Vec2 {
x: pos_px.0,
y: pos_px.1,
};
let mut files = Vec::new();
for p in self.pending_dropped_files.drain(..) {
let name = p
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("file")
.to_string();
files.push(repose_core::dnd::DroppedFile {
name,
path: Some(p),
});
}
let payload: repose_core::dnd::DragPayload =
std::rc::Rc::new(repose_core::dnd::DroppedFiles { files });
let Some(target_id) = repose_core::dnd::dnd_target_id_at(f, pos) else {
self.pending_drop_pos_px = None;
return;
};
if let Some(hit) = f.hit_regions.iter().find(|h| h.id == target_id)
&& let Some(cb) = &hit.on_drop
{
let accepted = cb(repose_core::dnd::DropEvent {
source_id: 0, target_id,
position: pos,
modifiers: self.rt.modifiers,
payload: payload.clone(),
});
if accepted && let Some(node) = f.semantics_nodes.iter().find(|n| n.id == target_id)
{
let label = node.label.as_deref().unwrap_or("");
self.a11y.announce(&format!("Dropped files on {}", label));
}
}
self.pending_drop_pos_px = None;
self.request_redraw();
}
}
let event_loop = EventLoop::new()?;
set_event_loop_proxy(event_loop.create_proxy());
let mut app = App::new(Box::new(root), config);
repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
event_loop.run_app(&mut app)?;
Ok(())
}
pub trait A11yBridge: Send {
fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
fn announce(&mut self, msg: &str);
}
#[cfg_attr(target_os = "linux", allow(dead_code))]
struct NoopA11y;
impl A11yBridge for NoopA11y {
fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
}
fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
if let Some(n) = node {
log::info!("A11y focus: {:?} {:?}", n.role, n.label);
} else {
log::info!("A11y focus: None");
}
}
fn announce(&mut self, msg: &str) {
log::info!("A11y announce: {msg}");
}
}
#[cfg(target_os = "linux")]
struct LinuxAtspiStub;
#[cfg(target_os = "linux")]
impl A11yBridge for LinuxAtspiStub {
fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
}
fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
if let Some(n) = node {
log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
} else {
log::info!("AT-SPI stub focus: None");
}
}
fn announce(&mut self, msg: &str) {
log::info!("AT-SPI stub announce: {msg}");
}
}