use crate::a11y::ReposeActionHandler;
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
use accesskit_winit::Adapter;
use repose_core::locals::dp_to_px;
use repose_core::*;
use repose_ui::textfield::{
self, TF_FONT_DP, TF_PADDING_X_DP, TextFieldState, TextMeasureConfig, caret_xy_for_byte, measure_text,
};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
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;
pub mod a11y;
mod common;
#[cfg(not(target_arch = "wasm32"))]
mod common_android;
mod common_web;
pub mod render;
use common as rc;
#[cfg(not(target_arch = "wasm32"))]
use common_android as rc_android;
use common_web as rc_web;
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 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);
static DEEPLINK_CB: Mutex<Option<Box<dyn Fn(Vec<u8>) + Send>>> = Mutex::new(None);
static PENDING_DEEPLINKS: Mutex<Vec<Vec<u8>>> = Mutex::new(Vec::new());
pub fn set_on_deeplink(callback: Box<dyn Fn(Vec<u8>) + Send>) {
*DEEPLINK_CB.lock().unwrap() = Some(callback);
}
pub fn push_deeplink(data: Vec<u8>) {
PENDING_DEEPLINKS.lock().unwrap().push(data);
#[cfg(not(target_arch = "wasm32"))]
if let Some(proxy) = EVENT_LOOP_PROXY.get() {
let _ = proxy.send_event(());
}
}
pub(crate) fn process_deeplinks() {
let mut queue = PENDING_DEEPLINKS.lock().unwrap();
if queue.is_empty() {
return;
}
let batch = std::mem::take(&mut *queue);
drop(queue);
if let Some(cb) = DEEPLINK_CB.lock().unwrap().as_ref() {
for data in batch {
cb(data);
}
}
}
#[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() = 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();
}
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);
}
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 fn compose_frame<F>(
sched: &mut Scheduler,
root_fn: &mut F,
scale: f32,
size_px_u32: (u32, u32),
hover_id: Option<u64>,
pressed_ids: &std::collections::HashSet<u64>,
tf_states: &std::collections::HashMap<u64, Rc<RefCell<repose_ui::TextFieldState>>>,
_focused: Option<u64>,
) -> Frame
where
F: FnMut(&mut Scheduler) -> View,
{
if let Some(requested_id) = repose_core::take_focus_request() {
if requested_id == repose_core::runtime::CLEAR_FOCUS_MARKER {
sched.focused = None;
} else {
sched.focused = Some(requested_id);
}
}
set_density_default(Density { scale });
let current_focused = sched.focused;
let frame = sched.repose(
{
let scale = scale;
move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
},
{
let hover_id = hover_id;
let pressed_ids = pressed_ids.clone();
move |view, _size| {
let interactions = repose_ui::Interactions {
hover: hover_id,
pressed: pressed_ids.clone(),
};
with_density(Density { scale }, || {
repose_ui::layout_and_paint(
view,
size_px_u32,
tf_states,
&interactions,
current_focused,
)
})
}
},
);
if let Some(fid) = sched.focused {
if !frame.focus_chain.contains(&fid) {
sched.focused = None;
}
}
frame
}
pub(crate) fn next_caret_blink_deadline(
sched: &Scheduler,
frame_cache: &Option<Frame>,
textfield_states: &std::collections::HashMap<u64, Rc<RefCell<TextFieldState>>>,
) -> Option<Instant> {
let fid = sched.focused?;
let frame = frame_cache.as_ref()?;
let hit = frame.hit_regions.iter().find(|h| h.id == fid)?;
let key = hit.tf_state_key?;
textfield_states.get(&key)?.borrow().next_blink_deadline()
}
pub fn tf_ensure_visible_in_rect(state: &mut repose_ui::TextFieldState, inner_rect: Rect) {
let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
let m = measure_text(&state.text, font_px, TextMeasureConfig::default());
let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
state.ensure_caret_visible(
caret_x_px,
inner_rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
dp_to_px(2.0),
);
}
fn winit_key_to_repose(
ev: &winit::event::KeyEvent,
mapped_key: &repose_core::input::Key,
mods: &repose_core::input::Modifiers,
) -> repose_core::input::KeyEvent {
let utf16 = match mapped_key {
repose_core::input::Key::Character(c) => *c as u16,
_ => 0,
};
repose_core::input::KeyEvent {
key: mapped_key.clone(),
modifiers: *mods,
is_repeat: ev.repeat,
event_type: if ev.state == winit::event::ElementState::Pressed {
repose_core::input::KeyEventType::Down
} else {
repose_core::input::KeyEventType::Up
},
utf16_code_point: utf16,
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
fn map_cursor(c: repose_core::CursorIcon) -> winit::window::CursorIcon {
use winit::window::CursorIcon as W;
match c {
repose_core::CursorIcon::Default => W::Default,
repose_core::CursorIcon::Pointer => W::Pointer,
repose_core::CursorIcon::Text => W::Text,
repose_core::CursorIcon::EwResize => W::EwResize,
repose_core::CursorIcon::NsResize => W::NsResize,
repose_core::CursorIcon::Grab => W::Grab,
repose_core::CursorIcon::Grabbing => W::Grabbing,
}
}
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
pub fn run_desktop_app(
root: impl FnMut(&mut Scheduler, &RenderContext) -> View + 'static,
) -> anyhow::Result<()> {
use std::collections::{HashMap, HashSet};
use winit::application::ApplicationHandler;
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use winit::event_loop::EventLoop;
use winit::keyboard::{KeyCode, PhysicalKey};
use winit::window::{Window, WindowAttributes};
use crate::a11y::A11yTree;
use repose_app::ReposeRuntime;
struct ReposeActivationHandler {
initial_tree: Option<accesskit::TreeUpdate>,
}
impl accesskit::ActivationHandler for ReposeActivationHandler {
fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
self.initial_tree.take()
}
}
struct ReposeDeactivationHandler;
impl accesskit::DeactivationHandler for ReposeDeactivationHandler {
fn deactivate_accessibility(&mut self) {
}
}
struct App {
root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>,
render: RenderContext,
window: Option<Arc<Window>>,
backend: Option<repose_render_wgpu::WgpuBackend>,
rt: ReposeRuntime,
inspector: repose_devtools::Inspector,
pending_dropped_files: Vec<std::path::PathBuf>,
pending_drop_pos_px: Option<(f32, f32)>,
external_file_drag: bool,
hovered_files: Vec<std::path::PathBuf>,
clipboard: Option<clipawl::Clipboard>,
a11y: Box<dyn A11yBridge>,
accesskit_adapter: Option<Adapter>,
a11y_actions: Arc<Mutex<Vec<accesskit::ActionRequest>>>,
a11y_tree: A11yTree,
last_redraw: Instant,
pending_redraw: bool,
redraw_requested: Cell<bool>,
}
impl App {
fn process_a11y_actions(&mut self) {
let mut actions = self.a11y_actions.lock().unwrap();
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 => {
self.rt.sched.focused = Some(target_id);
self.request_redraw();
}
_ => {}
}
}
}
fn new(root: Box<dyn FnMut(&mut Scheduler, &RenderContext) -> View>) -> Self {
Self {
root,
render: RenderContext::new(),
window: None,
backend: None,
rt: ReposeRuntime::new(),
inspector: repose_devtools::Inspector::new(),
pending_dropped_files: Vec::new(),
pending_drop_pos_px: None,
external_file_drag: false,
hovered_files: Vec::new(),
clipboard: None,
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(),
last_redraw: Instant::now(),
pending_redraw: false,
redraw_requested: Cell::new(false),
}
}
fn request_redraw(&self) {
self.redraw_requested.set(true);
repose_core::request_frame();
rc::request_redraw(&self.window);
}
fn tf_ensure_caret_visible(st: &mut TextFieldState, is_multiline: bool) {
rc::tf_ensure_caret_visible(st, is_multiline);
}
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;
};
rc::process_render_commands(backend, self.render.drain());
}
fn reset_pointer_state(&mut self) {
self.rt.capture_id = None;
self.rt.pressed_ids.clear();
self.rt.hover_id = None;
}
fn is_textfield(&self, id: u64) -> bool {
rc::is_textfield_in_frame(&self.rt.frame_cache, id)
}
fn is_multiline_id(&self, id: u64) -> bool {
if let Some(f) = &self.rt.frame_cache {
f.hit_regions
.iter()
.find(|h| h.id == id)
.map(|h| h.tf_multiline)
.unwrap_or(false)
} else {
false
}
}
fn hit_by_id(f: &Frame, id: u64) -> Option<&HitRegion> {
f.hit_regions.iter().find(|h| h.id == id)
}
fn dp_px(&self, dp: f32) -> f32 {
dp_to_px(dp)
}
}
impl ApplicationHandler<()> for App {
fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
self.clipboard = clipawl::Clipboard::new()
.map_err(|e| {
eprintln!("clipawl clipboard init failed: {e}");
e
})
.ok();
repose_core::clipboard::set_clipboard_read_fn(Box::new(|| {
clipawl::blocking::read().ok()
}));
repose_core::clipboard::set_clipboard_fn(Box::new(move |text| {
if let Err(e) = clipawl::blocking::write(text) {
eprintln!("clipboard write error: {e}");
}
}));
repose_core::clipboard::set_primary_fn(Box::new(|text| {
let mut opts = clipawl::ClipboardOptions::default();
opts.linux.selection = clipawl::LinuxSelection::Primary;
match clipawl::Clipboard::new_with_options(opts) {
Ok(cb) => {
if let Err(e) = pollster::block_on(cb.write(text)) {
eprintln!("primary selection write error: {e}");
}
}
Err(e) => eprintln!("primary clipboard init error: {e}"),
}
}));
if self.window.is_none() {
match el.create_window(
WindowAttributes::default()
.with_title("Repose")
.with_inner_size(PhysicalSize::new(1280, 800))
.with_visible(false),
) {
Ok(win) => {
let w = Arc::new(win);
let activation_handler = ReposeActivationHandler {
initial_tree: Some(A11yTree::initial_tree()),
};
let action_handler = ReposeActionHandler {
pending_actions: self.a11y_actions.clone(),
};
let deactivation_handler = ReposeDeactivationHandler;
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.rt.set_viewport_and_scale(size.width, size.height, sf);
match repose_render_wgpu::WgpuBackend::new(w.clone()) {
Ok(b) => {
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();
}
}
}
}
fn window_event(
&mut self,
el: &winit::event_loop::ActiveEventLoop,
_id: winit::window::WindowId,
event: WindowEvent,
) {
if let Some(adapter) = &mut self.accesskit_adapter {
adapter.process_event(self.window.as_ref().unwrap(), &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);
} else {
el.exit();
}
}
WindowEvent::Focused(false) => {
self.rt.handle_focus_lost();
self.external_file_drag = false;
self.hovered_files.clear();
if let Some(w) = &self.window {
rc_web::set_ime_for_textfield(w, false);
}
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.rt.set_viewport_and_scale(size.width, size.height, sf);
if let Some(b) = self.backend.as_mut() {
b.configure_surface(size.width, size.height);
}
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::CursorMoved { position, .. } => {
if self.external_file_drag {
self.pending_drop_pos_px = Some((position.x as f32, position.y as f32));
}
let pos = Vec2 {
x: position.x as f32,
y: position.y as f32,
};
let result = self.rt.handle_pointer_move(pos);
if self.inspector.hud.inspector_enabled
&& let Some(f) = &self.rt.frame_cache
{
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(),
}
})
});
self.inspector.hud.set_hovered(hover_rect, hover_info);
}
if let Some(win) = &self.window {
if let Some(c) = result.cursor {
win.set_cursor(winit::window::Cursor::Icon(map_cursor(c)));
}
}
self.request_redraw();
}
WindowEvent::MouseWheel { delta, .. } => {
let (dx_px, dy_px) = match delta {
MouseScrollDelta::LineDelta(x, y) => {
let unit_px = dp_to_px(60.0);
(-(x * unit_px), -(y * unit_px))
}
MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
};
log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
if self.rt.handle_scroll(Vec2 { x: dx_px, y: dy_px }) {
self.request_redraw();
}
}
WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Left,
..
} => {
let pos = Vec2 {
x: self.rt.mouse_pos_px.0,
y: self.rt.mouse_pos_px.1,
};
let result = self.rt.handle_pointer_press(pos, PointerButton::Primary);
if let Some(fid) = result.focused {
if let Some(win) = &self.window
&& let Some(f) = &self.rt.frame_cache
&& let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
{
let sf = win.scale_factor();
rc_web::set_ime_for_textfield(win, true);
win.set_ime_cursor_area(
LogicalPosition::new(
hit.rect.x as f64 / sf,
hit.rect.y as f64 / sf,
),
LogicalSize::new(
hit.rect.w as f64 / sf,
hit.rect.h as f64 / sf,
),
);
}
}
if result.focused.is_none() && self.rt.ime_preedit {
if let Some(win) = &self.window {
rc_web::set_ime_for_textfield(win, false);
}
self.rt.ime_preedit = false;
}
if result.needs_a11y_announce {
self.announce_focus_change();
}
self.request_redraw();
}
WindowEvent::MouseInput {
state: ElementState::Pressed,
button: MouseButton::Middle,
..
} => {
let Some(f) = &self.rt.frame_cache else {
return;
};
let pos = Vec2 {
x: self.rt.mouse_pos_px.0,
y: self.rt.mouse_pos_px.1,
};
if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos)) {
if let Some(cb) = &hit.on_pointer_down {
cb(PointerEvent::new(
PointerId(0),
PointerKind::Mouse,
PointerEventKind::Down(PointerButton::Tertiary),
pos,
1.0,
self.rt.modifiers,
));
}
if self.is_textfield(hit.id) {
let key = self.tf_key_of(hit.id);
if let Some(state_rc) = self.rt.textfield_states.get(&key) {
if let Some(txt) = self.paste_from_primary() {
let mut st = state_rc.borrow_mut();
st.insert_text_atomic(&txt);
self.notify_text_change(hit.id, st.text.clone());
if let Some(f) = &self.rt.frame_cache
&& let Some(h) =
f.hit_regions.iter().find(|h| h.id == hit.id)
{
App::tf_ensure_caret_visible(&mut st, h.tf_multiline);
}
}
}
}
}
self.request_redraw();
}
WindowEvent::MouseInput {
state: ElementState::Released,
button: MouseButton::Left,
..
} => {
let pos = Vec2 {
x: self.rt.mouse_pos_px.0,
y: self.rt.mouse_pos_px.1,
};
self.rt.handle_pointer_release(pos, PointerButton::Primary);
if let (Some(f), Some(cid)) = (&self.rt.frame_cache, self.rt.capture_id) {
if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
&& hit.rect.contains(pos)
&& hit.on_click.is_some()
&& 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));
}
}
repose_core::request_frame();
}
WindowEvent::MouseInput {
state: ElementState::Released,
button: MouseButton::Middle,
..
} => {
if let Some(f) = &self.rt.frame_cache {
let pos = Vec2 {
x: self.rt.mouse_pos_px.0,
y: self.rt.mouse_pos_px.1,
};
if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
{
if let Some(cb) = &hit.on_pointer_up {
cb(PointerEvent::new(
PointerId(0),
PointerKind::Mouse,
PointerEventKind::Up(PointerButton::Tertiary),
pos,
1.0,
self.rt.modifiers,
));
}
}
}
}
WindowEvent::ModifiersChanged(new_mods) => {
let state = new_mods.state();
self.rt.modifiers.shift = state.shift_key();
self.rt.modifiers.ctrl = state.control_key();
self.rt.modifiers.alt = state.alt_key();
self.rt.modifiers.meta = state.super_key();
self.rt.modifiers.command = if cfg!(target_os = "macos") {
self.rt.modifiers.meta
} else {
self.rt.modifiers.ctrl
};
}
WindowEvent::KeyboardInput {
event: key_event, ..
} => {
if key_event.state == ElementState::Pressed && !key_event.repeat {
match key_event.physical_key {
PhysicalKey::Code(KeyCode::BrowserBack)
| PhysicalKey::Code(KeyCode::Escape) => {
use repose_navigation::back;
if repose_core::dnd::handle_drag_action(
&repose_core::shortcuts::DragAction::Cancel,
) {
return;
}
let mapped = rc::map_key(key_event.physical_key);
if self.dispatch_focus_key_event(&key_event, &mapped) {
self.request_redraw();
return;
}
if !back::handle() {
}
return;
}
_ => {}
}
}
if key_event.state == ElementState::Pressed
&& self.rt.modifiers.ctrl
&& self.rt.modifiers.shift
&& key_event.physical_key == PhysicalKey::Code(KeyCode::KeyI)
{
self.inspector.hud.toggle_inspector();
self.request_redraw();
return;
}
if key_event.state == ElementState::Pressed
&& !key_event.repeat
&& self.rt.modifiers.command
{
match key_event.physical_key {
PhysicalKey::Code(KeyCode::KeyZ) if self.rt.modifiers.shift => {
if let Some(fid) = self.rt.sched.focused {
let key = self.tf_key_of(fid);
if let Some(state_rc) = self.rt.textfield_states.get(&key) {
let mut st = state_rc.borrow_mut();
if st.can_redo() {
st.redo();
self.notify_text_change(fid, st.text.clone());
self.request_redraw();
return;
}
}
}
}
PhysicalKey::Code(KeyCode::KeyZ) => {
if let Some(fid) = self.rt.sched.focused {
let key = self.tf_key_of(fid);
if let Some(state_rc) = self.rt.textfield_states.get(&key) {
let mut st = state_rc.borrow_mut();
if st.can_undo() {
st.undo();
self.notify_text_change(fid, st.text.clone());
self.request_redraw();
return;
}
}
}
}
_ => {}
}
}
let mapped = rc::map_key(key_event.physical_key);
let ke = winit_key_to_repose(&key_event, &mapped, &self.rt.modifiers);
let consumed = self.rt.handle_key(&ke);
if consumed {
self.request_redraw();
return;
}
if key_event.state == ElementState::Pressed
&& !key_event.repeat
&& !self.rt.ime_preedit
&& !self.rt.modifiers.ctrl
&& !self.rt.modifiers.alt
&& !self.rt.modifiers.meta
&& let Some(raw) = key_event.text.as_deref()
{
let text: String = raw
.chars()
.filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
.collect();
if !text.is_empty()
&& let Some(fid) = self.rt.sched.focused
{
let key = self.tf_key_of(fid);
if let Some(state_rc) = self.rt.textfield_states.get(&key) {
let mut st = state_rc.borrow_mut();
st.insert_text(&text);
self.notify_text_change(fid, st.text.clone());
if let Some(f) = &self.rt.frame_cache
&& let Some(hit) =
f.hit_regions.iter().find(|h| h.id == fid)
{
App::tf_ensure_caret_visible(&mut st, hit.tf_multiline);
}
self.request_redraw();
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) => {
if let Some(focused_id) = self.rt.sched.focused {
let key = self.tf_key_of(focused_id);
if let Some(state_rc) = self.rt.textfield_states.get(&key) {
let mut state = state_rc.borrow_mut();
let on_text_change = self.rt
.frame_cache
.as_ref()
.and_then(|f| f.hit_regions.iter().find(|h| h.id == focused_id))
.and_then(|h| h.on_text_change.clone());
let mut notify = |text: String| {
if let Some(cb) = &on_text_change {
cb(text);
}
};
rc_android::handle_ime_event(
ime,
&mut state,
&mut notify,
&mut self.rt.ime_preedit,
);
self.request_redraw();
}
}
}
WindowEvent::RedrawRequested => {
if !self.redraw_requested.replace(false) {
self.process_a11y_actions();
self.process_render_commands();
log::trace!("RedrawRequested: no frame request, skipping compose");
return;
}
log::trace!("RedrawRequested: frame request pending, composing");
self.process_a11y_actions();
self.process_render_commands();
let Some(win) = self.window.as_ref() else {
return;
};
if self.backend.is_none() {
return;
}
repose_core::animation_driver::tick();
let t0 = Instant::now();
let scale = win.scale_factor() as f32;
self.rt.scale = scale;
let focused = self.rt.sched.focused;
let output = self.rt.frame(&mut self.root, &self.render);
if let Some(cursor) = &output.platform.cursor {
win.set_cursor(winit::window::Cursor::Icon(map_cursor(*cursor)));
}
if !output.wants_keyboard && focused.is_some() && self.rt.sched.focused.is_none() && self.rt.ime_preedit {
rc_web::set_ime_for_textfield(win, false);
self.rt.ime_preedit = false;
}
let frame = Frame {
scene: output.scene,
hit_regions: output.hit_regions,
semantics_nodes: output.semantics_nodes,
focus_chain: output.focus_chain,
};
let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
if let Some(adapter) = &mut self.accesskit_adapter {
let win = self.window.as_ref().unwrap();
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);
}
}
let mut scene = frame.scene.clone();
let widget_count = frame.semantics_nodes.len() + frame.hit_regions.len();
let signal_count = self.rt.sched.id_count() as usize;
self.inspector.hud.metrics = Some(repose_devtools::Metrics {
build_ms: build_layout_ms,
layout_ms: build_layout_ms * 0.5,
scene_nodes: scene.nodes.len(),
widget_count,
signal_count,
});
self.inspector.frame(&mut scene);
repose_core::dnd::overlay_drag_indicator(
&mut scene,
self.rt.mouse_pos_px,
self.external_file_drag,
);
let win = self.window.as_ref().unwrap();
let scale = win.scale_factor() as f32;
if let Some(backend) = self.backend.as_mut() {
backend.frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
}
if let Some(fid) = self.rt.sched.focused {
if let Some(hit) = frame.hit_regions.iter().find(|h| h.id == fid)
&& let Some(key) = hit.tf_state_key
&& !self.rt.textfield_states.contains_key(&key)
{
self.rt.textfield_states
.entry(key)
.or_insert_with(|| {
Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
})
.borrow_mut()
.reset_caret_blink();
}
}
repose_core::dnd::set_dnd_frame(Some(frame.clone()));
self.rt.frame_cache = Some(frame);
repose_core::dnd::set_dnd_scale(scale);
self.dispatch_file_drop_now();
rc::tick_snackbar(self.last_redraw);
self.last_redraw = Instant::now();
}
_ => {}
}
}
fn about_to_wait(&mut self, el: &winit::event_loop::ActiveEventLoop) {
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
if let Some(cb) = ABOUT_TO_WAIT_CALLBACK.lock().unwrap().as_ref() {
cb();
}
process_deeplinks();
#[cfg(all(not(target_os = "android"), not(target_arch = "wasm32")))]
if WINDOW_VISIBLE.load(Ordering::Relaxed) && self.backend.is_none() {
if let Some(w) = &self.window {
log::info!("about_to_wait: recreating GPU backend");
match repose_render_wgpu::WgpuBackend::new(w.clone()) {
Ok(b) => self.backend = Some(b),
Err(e) => log::error!("about_to_wait: failed to recreate backend: {e:?}"),
}
}
}
if take_frame_request() {
self.pending_redraw = true;
}
if !self.pending_redraw {
let now = Instant::now();
let idle_cap = web_time::Duration::from_millis(1000);
let deadline = self
.next_caret_blink_deadline()
.unwrap_or(now + idle_cap);
if now.saturating_duration_since(self.last_redraw) >= idle_cap || now >= deadline {
self.redraw_requested.set(true);
request_frame();
rc::request_redraw(&self.window);
self.last_redraw = now;
}
el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
Ord::min(deadline, now + idle_cap),
));
return;
}
let now = Instant::now();
let interval = web_time::Duration::from_millis(16);
if now.saturating_duration_since(self.last_redraw) >= interval {
self.pending_redraw = false;
self.redraw_requested.set(true);
rc::request_redraw(&self.window);
self.last_redraw = now;
} else {
el.set_control_flow(winit::event_loop::ControlFlow::WaitUntil(
self.last_redraw + interval,
));
}
}
fn new_events(
&mut self,
_: &winit::event_loop::ActiveEventLoop,
_: winit::event::StartCause,
) {
}
fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {
self.pending_redraw = true;
}
fn device_event(
&mut self,
_: &winit::event_loop::ActiveEventLoop,
_: winit::event::DeviceId,
_: winit::event::DeviceEvent,
) {
}
fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
}
impl App {
fn dispatch_focus_key_event(
&self,
key_event: &winit::event::KeyEvent,
mapped_key: &repose_core::input::Key,
) -> bool {
let Some(f) = &self.rt.frame_cache else {
return false;
};
let Some(focused) = self.rt.sched.focused else {
return false;
};
let utf16 = match mapped_key {
repose_core::input::Key::Character(c) => *c as u16,
_ => 0,
};
let mods = self.rt.modifiers;
let repeat = key_event.repeat;
let ev_type = if key_event.state == ElementState::Pressed {
repose_core::input::KeyEventType::Down
} else {
repose_core::input::KeyEventType::Up
};
let hit_by_id: std::collections::HashMap<u64, &HitRegion> =
f.hit_regions.iter().map(|h| (h.id, h)).collect();
let sem_parent_of: std::collections::HashMap<u64, u64> = f
.semantics_nodes
.iter()
.filter_map(|n| n.parent.map(|p| (n.id, p)))
.collect();
let mut ancestors = Vec::new();
let mut cur = focused;
loop {
ancestors.push(cur);
if let Some(&p) = sem_parent_of.get(&cur) {
cur = p;
} else {
break;
}
}
let make_ke = || repose_core::input::KeyEvent {
key: mapped_key.clone(),
modifiers: mods,
is_repeat: repeat,
event_type: ev_type,
utf16_code_point: utf16,
};
for &id in ancestors.iter().rev() {
if let Some(hit) = hit_by_id.get(&id) {
if let Some(cb) = &hit.on_preview_key_event {
if cb(make_ke()) {
return true;
}
}
}
}
for &id in ancestors.iter() {
if let Some(hit) = hit_by_id.get(&id) {
if let Some(cb) = &hit.on_key_event {
if cb(make_ke()) {
return true;
}
}
}
}
false
}
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 notify_text_change(&self, id: u64, text: String) {
if let Some(f) = &self.rt.frame_cache
&& let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
&& let Some(cb) = &h.on_text_change
{
cb(text);
}
}
fn tf_key_of(&self, visual_id: u64) -> u64 {
rc::tf_key_of_in_frame(&self.rt.frame_cache, visual_id)
}
fn next_caret_blink_deadline(&self) -> Option<Instant> {
next_caret_blink_deadline(&self.rt.sched, &self.rt.frame_cache, &self.rt.textfield_states)
}
fn dispatch_action(&mut self, action: repose_core::shortcuts::Action) -> bool {
use repose_core::shortcuts;
if let (Some(f), Some(fid)) = (&self.rt.frame_cache, self.rt.sched.focused)
&& let Some(hit) = f.hit_regions.iter().find(|h| h.id == fid)
&& let Some(cb) = &hit.on_action
&& cb(action.clone())
{
return true;
}
if shortcuts::handle(action.clone()) {
return true;
}
if let Some(f) = &self.rt.frame_cache {
if let Some(new_id) = repose_core::focus::handle_action(&action, &mut self.rt.sched, f)
{
if let Some(active) = self.rt.key_pressed_active.take() {
self.rt.pressed_ids.remove(&active);
}
let tf_state_key = f
.hit_regions
.iter()
.find(|h| h.id == new_id)
.and_then(|h| h.tf_state_key);
if let Some(key) = tf_state_key {
self.rt.textfield_states.entry(key).or_insert_with(|| {
Rc::new(RefCell::new(repose_ui::TextFieldState::new()))
});
if let Some(state_rc) = self.rt.textfield_states.get(&key) {
state_rc.borrow_mut().reset_caret_blink();
}
}
if let Some(win) = &self.window {
let is_textfield = f.semantics_nodes.iter().any(|n| {
n.id == new_id && n.role == repose_core::semantics::Role::TextField
});
rc_web::set_ime_for_textfield(win, is_textfield);
}
self.announce_focus_change();
return true;
}
}
false
}
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));
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);
}
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}");
}
}