1use repose_core::locals::dp_to_px;
3use repose_core::*;
4use repose_ui::textfield::{TF_FONT_DP, TF_PADDING_X_DP, index_for_x_bytes, measure_text};
5use std::cell::RefCell;
6use std::rc::Rc;
7use web_time::Instant;
8
9#[cfg(all(feature = "android", target_os = "android"))]
10pub mod android;
11
12#[cfg(all(target_arch = "wasm32"))]
13pub mod web;
14
15mod common;
16
17pub fn compose_frame<F>(
19 sched: &mut Scheduler,
20 root_fn: &mut F,
21 scale: f32,
22 size_px_u32: (u32, u32),
23 hover_id: Option<u64>,
24 pressed_ids: &std::collections::HashSet<u64>,
25 tf_states: &std::collections::HashMap<u64, Rc<RefCell<repose_ui::TextFieldState>>>,
26 focused: Option<u64>,
27) -> Frame
28where
29 F: FnMut(&mut Scheduler) -> View,
30{
31 set_density_default(Density { scale });
32
33 sched.repose(
34 {
35 let scale = scale;
36 move |s: &mut Scheduler| with_density(Density { scale }, || (root_fn)(s))
37 },
38 {
39 let hover_id = hover_id;
40 let pressed_ids = pressed_ids.clone();
41 move |view, _size| {
42 let interactions = repose_ui::Interactions {
43 hover: hover_id,
44 pressed: pressed_ids.clone(),
45 };
46 with_density(Density { scale }, || {
47 repose_ui::layout_and_paint(
48 view,
49 size_px_u32,
50 tf_states,
51 &interactions,
52 focused,
53 )
54 })
55 }
56 },
57 )
58}
59
60pub fn tf_ensure_visible_in_rect(state: &mut repose_ui::TextFieldState, inner_rect: Rect) {
62 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
63 let m = measure_text(&state.text, font_px);
64 let caret_x_px = m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
65 state.ensure_caret_visible(
66 caret_x_px,
67 inner_rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
68 dp_to_px(2.0),
69 );
70}
71
72#[cfg(feature = "desktop")]
73pub fn run_desktop_app(root: impl FnMut(&mut Scheduler) -> View + 'static) -> anyhow::Result<()> {
74 use std::cell::RefCell;
75 use std::collections::{HashMap, HashSet};
76 use std::rc::Rc;
77 use std::sync::Arc;
78
79 use repose_ui::TextFieldState;
80 use winit::application::ApplicationHandler;
81 use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
82 use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
83 use winit::event_loop::EventLoop;
84 use winit::keyboard::{KeyCode, PhysicalKey};
85 use winit::window::{ImePurpose, Window, WindowAttributes};
86
87 struct App {
88 root: Box<dyn FnMut(&mut Scheduler) -> View>,
90 window: Option<Arc<Window>>,
91 backend: Option<repose_render_wgpu::WgpuBackend>,
92 sched: Scheduler,
93 inspector: repose_devtools::Inspector,
94 frame_cache: Option<Frame>,
95 mouse_pos_px: (f32, f32),
96 modifiers: Modifiers,
97 textfield_states: HashMap<u64, Rc<RefCell<TextFieldState>>>,
98 ime_preedit: bool,
99 hover_id: Option<u64>,
100 capture_id: Option<u64>,
101 pressed_ids: HashSet<u64>,
102 key_pressed_active: Option<u64>, clipboard: Option<clipawl::Clipboard>,
104 a11y: Box<dyn A11yBridge>,
105 last_focus: Option<u64>,
106 }
107
108 impl App {
109 fn new(root: Box<dyn FnMut(&mut Scheduler) -> View>) -> Self {
110 Self {
111 root,
112 window: None,
113 backend: None,
114 sched: Scheduler::new(),
115 inspector: repose_devtools::Inspector::new(),
116 frame_cache: None,
117 mouse_pos_px: (0.0, 0.0),
118 modifiers: Modifiers::default(),
119 textfield_states: HashMap::new(),
120 ime_preedit: false,
121 hover_id: None,
122 capture_id: None,
123 pressed_ids: HashSet::new(),
124 key_pressed_active: None,
125 clipboard: None,
126 a11y: {
127 #[cfg(target_os = "linux")]
128 {
129 Box::new(LinuxAtspiStub) as Box<dyn A11yBridge>
130 }
131 #[cfg(not(target_os = "linux"))]
132 {
133 Box::new(NoopA11y) as Box<dyn A11yBridge>
134 }
135 },
136 last_focus: None,
137 }
138 }
139
140 fn request_redraw(&self) {
141 if let Some(w) = &self.window {
142 w.request_redraw();
143 }
144 }
145
146 fn tf_ensure_caret_visible(st: &mut TextFieldState) {
148 let font_px = dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
149 let m = measure_text(&st.text, font_px);
150 let caret_x_px = m.positions.get(st.caret_index()).copied().unwrap_or(0.0);
151 st.ensure_caret_visible(caret_x_px, st.inner_width, dp_to_px(2.0));
152 }
153
154 fn copy_to_clipboard(&mut self, text: String) {
155 if let Some(cb) = &mut self.clipboard {
156 let _ = pollster::block_on(cb.set_text(&text));
158 }
159 }
160
161 fn paste_from_clipboard(&mut self) -> Option<String> {
162 if let Some(cb) = &mut self.clipboard {
163 match pollster::block_on(cb.get_text()) {
164 Ok(t) => Some(t),
165 Err(e) => {
166 eprintln!("Paste error: {}", e);
167 None
168 }
169 }
170 } else {
171 None
172 }
173 }
174 }
175
176 impl ApplicationHandler<()> for App {
177 fn resumed(&mut self, el: &winit::event_loop::ActiveEventLoop) {
178 self.clipboard = clipawl::Clipboard::new().ok();
179 if self.window.is_none() {
181 match el.create_window(
182 WindowAttributes::default()
183 .with_title("Repose")
184 .with_inner_size(PhysicalSize::new(1280, 800)),
185 ) {
186 Ok(win) => {
187 let w = Arc::new(win);
188 let size = w.inner_size();
189 self.sched.size = (size.width, size.height);
190 match repose_render_wgpu::WgpuBackend::new(w.clone()) {
192 Ok(b) => {
193 self.backend = Some(b);
194 self.window = Some(w);
195 self.request_redraw();
196 }
197 Err(e) => {
198 log::error!("Failed to create WGPU backend: {e:?}");
199 el.exit();
200 }
201 }
202 }
203 Err(e) => {
204 log::error!("Failed to create window: {e:?}");
205 el.exit();
206 }
207 }
208 }
209 }
210
211 fn window_event(
212 &mut self,
213 el: &winit::event_loop::ActiveEventLoop,
214 _id: winit::window::WindowId,
215 event: WindowEvent,
216 ) {
217 match event {
218 WindowEvent::CloseRequested => {
219 el.exit();
220 }
221 WindowEvent::Resized(size) => {
222 self.sched.size = (size.width, size.height);
223 if let Some(b) = &mut self.backend {
224 b.configure_surface(size.width, size.height);
225 }
226 if let Some(w) = &self.window {
227 let sf = w.scale_factor() as f32;
228 let dp_w = size.width as f32 / sf;
229 let dp_h = size.height as f32 / sf;
230 log::info!(
231 "Resized: fb={}x{} px, scale_factor={}, ~{}x{} dp",
232 size.width,
233 size.height,
234 sf,
235 dp_w as i32,
236 dp_h as i32
237 );
238 }
239 self.request_redraw();
240 }
241 WindowEvent::CursorMoved { position, .. } => {
242 self.mouse_pos_px = (position.x as f32, position.y as f32);
243
244 if self.inspector.hud.inspector_enabled
246 && let Some(f) = &self.frame_cache
247 {
248 let hover_rect = f
249 .hit_regions
250 .iter()
251 .find(|h| {
252 h.rect.contains(Vec2 {
253 x: self.mouse_pos_px.0,
254 y: self.mouse_pos_px.1,
255 })
256 })
257 .map(|h| h.rect);
258 self.inspector.hud.set_hovered(hover_rect);
259 self.request_redraw();
260 }
261
262 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
263 && let Some(_sem) = f
264 .semantics_nodes
265 .iter()
266 .find(|n| n.id == cid && n.role == Role::TextField)
267 {
268 let key = self.tf_key_of(cid);
269 if let Some(state_rc) = self.textfield_states.get(&key) {
270 let mut state = state_rc.borrow_mut();
271 let inner_x_px = f
273 .hit_regions
274 .iter()
275 .find(|h| h.id == cid)
276 .map(|h| h.rect.x + dp_to_px(TF_PADDING_X_DP))
277 .unwrap_or(0.0);
278 let content_x_px =
279 self.mouse_pos_px.0 - inner_x_px + state.scroll_offset;
280 let font_px =
281 dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
282 let idx =
283 index_for_x_bytes(&state.text, font_px, content_x_px.max(0.0));
284 state.drag_to(idx);
285
286 let m = measure_text(&state.text, font_px);
288 let caret_x_px =
289 m.positions.get(state.caret_index()).copied().unwrap_or(0.0);
290 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid) {
291 state.ensure_caret_visible(
292 caret_x_px,
293 hit.rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
294 dp_to_px(2.0),
295 );
296 }
297 self.request_redraw();
298 }
299 }
300
301 if let Some(f) = &self.frame_cache {
303 let pos = Vec2 {
305 x: self.mouse_pos_px.0,
306 y: self.mouse_pos_px.1,
307 };
308 let top = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos));
309 let new_hover = top.map(|h| h.id);
310
311 if new_hover != self.hover_id {
313 if let Some(prev_id) = self.hover_id
314 && let Some(prev) = f.hit_regions.iter().find(|h| h.id == prev_id)
315 && let Some(cb) = &prev.on_pointer_leave
316 {
317 let pe = repose_core::input::PointerEvent {
318 id: repose_core::input::PointerId(0),
319 kind: repose_core::input::PointerKind::Mouse,
320 event: repose_core::input::PointerEventKind::Leave,
321 position: pos,
322 pressure: 1.0,
323 modifiers: self.modifiers,
324 };
325 cb(pe);
326 }
327 if let Some(h) = top
328 && let Some(cb) = &h.on_pointer_enter
329 {
330 let pe = repose_core::input::PointerEvent {
331 id: repose_core::input::PointerId(0),
332 kind: repose_core::input::PointerKind::Mouse,
333 event: repose_core::input::PointerEventKind::Enter,
334 position: pos,
335 pressure: 1.0,
336 modifiers: self.modifiers,
337 };
338 cb(pe);
339 }
340 self.hover_id = new_hover;
341 }
342
343 let pe = repose_core::input::PointerEvent {
345 id: repose_core::input::PointerId(0),
346 kind: repose_core::input::PointerKind::Mouse,
347 event: repose_core::input::PointerEventKind::Move,
348 position: pos,
349 pressure: 1.0,
350 modifiers: self.modifiers,
351 };
352
353 if let Some(cid) = self.capture_id {
355 if let Some(h) = f.hit_regions.iter().find(|h| h.id == cid)
356 && let Some(cb) = &h.on_pointer_move
357 {
358 cb(pe.clone());
359 }
360 } else if let Some(h) = &top
361 && let Some(cb) = &h.on_pointer_move
362 {
363 cb(pe);
364 }
365 }
366 }
367 WindowEvent::MouseWheel { delta, .. } => {
368 let (dx_px, dy_px) = match delta {
370 MouseScrollDelta::LineDelta(x, y) => {
371 let unit_px = dp_to_px(60.0);
372 (-(x * unit_px), -(y * unit_px))
373 }
374 MouseScrollDelta::PixelDelta(lp) => (-(lp.x as f32), -(lp.y as f32)),
375 };
376 log::debug!("MouseWheel: dx={}, dy={}", dx_px, dy_px);
377
378 if let Some(f) = &self.frame_cache {
379 let pos = Vec2 {
380 x: self.mouse_pos_px.0,
381 y: self.mouse_pos_px.1,
382 };
383
384 for hit in f.hit_regions.iter().rev().filter(|h| h.rect.contains(pos)) {
385 if let Some(cb) = &hit.on_scroll {
386 log::debug!("Calling on_scroll for hit region id={}", hit.id);
387 let before = Vec2 { x: dx_px, y: dy_px };
388 let leftover = cb(before);
389 let consumed_x = (before.x - leftover.x).abs() > 0.001;
390 let consumed_y = (before.y - leftover.y).abs() > 0.001;
391 if consumed_x || consumed_y {
392 self.request_redraw();
393 break; }
395 }
396 }
397 }
398 }
399 WindowEvent::MouseInput {
400 state: ElementState::Pressed,
401 button: MouseButton::Left,
402 ..
403 } => {
404 let mut need_announce = false;
405 if let Some(f) = &self.frame_cache {
406 let pos = Vec2 {
407 x: self.mouse_pos_px.0,
408 y: self.mouse_pos_px.1,
409 };
410 if let Some(hit) = f.hit_regions.iter().rev().find(|h| h.rect.contains(pos))
411 {
412 self.capture_id = Some(hit.id);
414 self.pressed_ids.insert(hit.id);
416 self.request_redraw();
418
419 if hit.focusable {
421 self.sched.focused = Some(hit.id);
422 need_announce = true;
423 let key = self.tf_key_of(hit.id);
424 self.textfield_states.entry(key).or_insert_with(|| {
425 Rc::new(RefCell::new(
426 repose_ui::textfield::TextFieldState::new(),
427 ))
428 });
429 if let Some(win) = &self.window {
430 let sf = win.scale_factor();
431 win.set_ime_allowed(true);
432 win.set_ime_purpose(ImePurpose::Normal);
433 win.set_ime_cursor_area(
434 LogicalPosition::new(
435 hit.rect.x as f64 / sf,
436 hit.rect.y as f64 / sf,
437 ),
438 LogicalSize::new(
439 hit.rect.w as f64 / sf,
440 hit.rect.h as f64 / sf,
441 ),
442 );
443 }
444 }
445
446 if let Some(cb) = &hit.on_pointer_down {
448 let pe = repose_core::input::PointerEvent {
449 id: repose_core::input::PointerId(0),
450 kind: repose_core::input::PointerKind::Mouse,
451 event: repose_core::input::PointerEventKind::Down(
452 repose_core::input::PointerButton::Primary,
453 ),
454 position: pos,
455 pressure: 1.0,
456 modifiers: self.modifiers,
457 };
458 cb(pe);
459 }
460
461 if let Some(_sem) = f
463 .semantics_nodes
464 .iter()
465 .find(|n| n.id == hit.id && n.role == Role::TextField)
466 {
467 let key = self.tf_key_of(hit.id);
468 if let Some(state_rc) = self.textfield_states.get(&key) {
469 let mut state = state_rc.borrow_mut();
470 let inner_x_px = hit.rect.x + dp_to_px(TF_PADDING_X_DP);
471 let content_x_px =
472 self.mouse_pos_px.0 - inner_x_px + state.scroll_offset;
473 let font_px =
474 dp_to_px(TF_FONT_DP) * repose_core::locals::text_scale().0;
475 let idx = index_for_x_bytes(
476 &state.text,
477 font_px,
478 content_x_px.max(0.0),
479 );
480 state.begin_drag(idx, self.modifiers.shift);
481 let m = measure_text(&state.text, font_px);
482 let caret_x_px = m
483 .positions
484 .get(state.caret_index())
485 .copied()
486 .unwrap_or(0.0);
487 state.ensure_caret_visible(
488 caret_x_px,
489 hit.rect.w - 2.0 * dp_to_px(TF_PADDING_X_DP),
490 dp_to_px(2.0),
491 );
492 }
493 }
494 if need_announce {
495 self.announce_focus_change();
496 }
497
498 self.request_redraw();
499 } else {
500 if self.ime_preedit {
502 if let Some(win) = &self.window {
503 win.set_ime_allowed(false);
504 }
505 self.ime_preedit = false;
506 }
507 self.sched.focused = None;
508 self.request_redraw();
509 }
510 }
511 }
512 WindowEvent::MouseInput {
513 state: ElementState::Released,
514 button: MouseButton::Left,
515 ..
516 } => {
517 if let Some(cid) = self.capture_id {
518 self.pressed_ids.remove(&cid);
519 self.request_redraw();
520 }
521
522 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id) {
524 let pos = Vec2 {
525 x: self.mouse_pos_px.0,
526 y: self.mouse_pos_px.1,
527 };
528 if let Some(hit) = f.hit_regions.iter().find(|h| h.id == cid)
529 && hit.rect.contains(pos)
530 && let Some(cb) = &hit.on_click
531 {
532 cb();
533 if let Some(node) = f.semantics_nodes.iter().find(|n| n.id == cid) {
535 let label = node.label.as_deref().unwrap_or("");
536 self.a11y.announce(&format!("Activated {}", label));
537 }
538 }
539 }
540 if let (Some(f), Some(cid)) = (&self.frame_cache, self.capture_id)
542 && let Some(_sem) = f
543 .semantics_nodes
544 .iter()
545 .find(|n| n.id == cid && n.role == Role::TextField)
546 {
547 let key = self.tf_key_of(cid);
548 if let Some(state_rc) = self.textfield_states.get(&key) {
549 state_rc.borrow_mut().end_drag();
550 }
551 }
552 self.capture_id = None;
553 }
554 WindowEvent::ModifiersChanged(new_mods) => {
555 self.modifiers.shift = new_mods.state().shift_key();
556 self.modifiers.ctrl = new_mods.state().control_key();
557 self.modifiers.alt = new_mods.state().alt_key();
558 self.modifiers.meta = new_mods.state().super_key();
559 }
560 WindowEvent::KeyboardInput {
561 event: key_event, ..
562 } => {
563 if key_event.state == ElementState::Pressed && !key_event.repeat {
564 match key_event.physical_key {
565 PhysicalKey::Code(KeyCode::BrowserBack)
566 | PhysicalKey::Code(KeyCode::Escape) => {
567 use repose_navigation::back;
568
569 if !back::handle() {
570 }
572 return;
573 }
574 _ => {}
575 }
576 }
577 if matches!(key_event.physical_key, PhysicalKey::Code(KeyCode::Tab)) {
579 if key_event.state == ElementState::Pressed
581 && !key_event.repeat
582 && let Some(f) = &self.frame_cache
583 {
584 let chain = &f.focus_chain;
585 if !chain.is_empty() {
586 if let Some(active) = self.key_pressed_active.take() {
588 self.pressed_ids.remove(&active);
589 }
590
591 let shift = self.modifiers.shift;
592 let current = self.sched.focused;
593 let next = if let Some(cur) = current {
594 if let Some(idx) = chain.iter().position(|&id| id == cur) {
595 if shift {
596 if idx == 0 {
597 chain[chain.len() - 1]
598 } else {
599 chain[idx - 1]
600 }
601 } else {
602 chain[(idx + 1) % chain.len()]
603 }
604 } else {
605 chain[0]
606 }
607 } else {
608 chain[0]
609 };
610 self.sched.focused = Some(next);
611
612 if let Some(win) = &self.window {
614 if f.semantics_nodes
615 .iter()
616 .any(|n| n.id == next && n.role == Role::TextField)
617 {
618 win.set_ime_allowed(true);
619 win.set_ime_purpose(ImePurpose::Normal);
620 } else {
621 win.set_ime_allowed(false);
622 }
623 }
624 self.announce_focus_change();
625 self.request_redraw();
626 }
627 }
628 return; }
630
631 if let Some(fid) = self.sched.focused {
632 let is_textfield = if let Some(f) = &self.frame_cache {
634 f.semantics_nodes
635 .iter()
636 .any(|n| n.id == fid && n.role == Role::TextField)
637 } else {
638 false
639 };
640
641 if !is_textfield {
642 match key_event.physical_key {
643 PhysicalKey::Code(KeyCode::Space)
644 | PhysicalKey::Code(KeyCode::Enter) => {
645 if key_event.state == ElementState::Pressed && !key_event.repeat
646 {
647 self.pressed_ids.insert(fid);
648 self.key_pressed_active = Some(fid);
649 self.request_redraw();
650 return;
651 }
652 }
653 _ => {}
654 }
655 }
656 }
657
658 if key_event.state == ElementState::Pressed
660 && !key_event.repeat
661 && let PhysicalKey::Code(KeyCode::Enter) = key_event.physical_key
662 && let Some(focused_id) = self.sched.focused
663 && let Some(f) = &self.frame_cache
664 && let Some(hit) = f.hit_regions.iter().find(|h| h.id == focused_id)
665 && let Some(on_submit) = &hit.on_text_submit
666 {
667 let key = self.tf_key_of(focused_id);
668
669 if let Some(state) = self.textfield_states.get(&key) {
670 let text = state.borrow().text.clone();
671 on_submit(text);
672 self.request_redraw();
673 return; }
675 }
676
677 if key_event.state == ElementState::Pressed {
678 if self.modifiers.ctrl
680 && self.modifiers.shift
681 && let PhysicalKey::Code(KeyCode::KeyI) = key_event.physical_key
682 {
683 self.inspector.hud.toggle_inspector();
684 self.request_redraw();
685 return;
686 }
687
688 if let Some(focused_id) = self.sched.focused {
690 let key = self.tf_key_of(focused_id);
691 if let Some(state_rc) = self.textfield_states.get(&key) {
692 let mut state = state_rc.borrow_mut();
693 match key_event.physical_key {
694 PhysicalKey::Code(KeyCode::Backspace) => {
695 state.delete_backward();
696 let new_text = state.text.clone();
697 self.notify_text_change(focused_id, new_text);
698 App::tf_ensure_caret_visible(&mut state);
699 self.request_redraw();
700 }
701 PhysicalKey::Code(KeyCode::Delete) => {
702 state.delete_forward();
703 let new_text = state.text.clone();
704 self.notify_text_change(focused_id, new_text);
705 App::tf_ensure_caret_visible(&mut state);
706 self.request_redraw();
707 }
708 PhysicalKey::Code(KeyCode::ArrowLeft) => {
709 state.move_cursor(-1, self.modifiers.shift);
710 App::tf_ensure_caret_visible(&mut state);
711 self.request_redraw();
712 }
713 PhysicalKey::Code(KeyCode::ArrowRight) => {
714 state.move_cursor(1, self.modifiers.shift);
715 App::tf_ensure_caret_visible(&mut state);
716 self.request_redraw();
717 }
718 PhysicalKey::Code(KeyCode::Home) => {
719 state.selection = 0..0;
720 App::tf_ensure_caret_visible(&mut state);
721 self.request_redraw();
722 }
723 PhysicalKey::Code(KeyCode::End) => {
724 {
725 let end = state.text.len();
726 state.selection = end..end;
727 }
728 App::tf_ensure_caret_visible(&mut state);
729 self.request_redraw();
730 }
731 PhysicalKey::Code(KeyCode::KeyA) if self.modifiers.ctrl => {
732 state.selection = 0..state.text.len();
733 App::tf_ensure_caret_visible(&mut state);
734 self.request_redraw();
735 }
736 _ => {}
737 }
738 }
739 if self.modifiers.ctrl {
740 match key_event.physical_key {
741 PhysicalKey::Code(KeyCode::KeyC) => {
742 if let Some(fid) = self.sched.focused {
743 let key = self.tf_key_of(fid);
744 if let Some(state) = self.textfield_states.get(&key) {
745 let txt = state.borrow().selected_text();
746 if !txt.is_empty() {
747 let _ = self.copy_to_clipboard(txt);
748 }
749 }
750 }
751 return;
752 }
753 PhysicalKey::Code(KeyCode::KeyX) => {
754 if let Some(fid) = self.sched.focused {
755 let key = self.tf_key_of(fid);
756 if let Some(state_rc) =
757 self.textfield_states.get(&key).cloned()
758 {
759 let txt = state_rc.borrow().selected_text();
761 if !txt.is_empty() {
762 {
763 let _ = self.copy_to_clipboard(txt.clone());
764 }
765 {
767 let mut st = state_rc.borrow_mut();
768 st.insert_text(""); let new_text = st.text.clone();
770 self.notify_text_change(
771 focused_id, new_text,
772 );
773 App::tf_ensure_caret_visible(&mut st);
774 }
775 self.request_redraw();
776 }
777 }
778 }
779 return;
780 }
781 PhysicalKey::Code(KeyCode::KeyV) => {
782 if let Some(fid) = self.sched.focused {
783 let key = self.tf_key_of(fid);
784 if let Some(state_rc) =
785 self.textfield_states.get(&key).cloned()
786 && let Some(mut txt) = self.paste_from_clipboard()
787 {
788 txt.retain(|c| {
790 !c.is_control() && c != '\n' && c != '\r'
791 });
792 if !txt.is_empty() {
793 let mut st = state_rc.borrow_mut();
794 st.insert_text(&txt);
795 let new_text = st.text.clone();
796 self.notify_text_change(focused_id, new_text);
797 App::tf_ensure_caret_visible(&mut st);
798 self.request_redraw();
799 }
800 }
801 }
802 return;
803 }
804 _ => {}
805 }
806 }
807 }
808
809 if !self.ime_preedit
811 && !self.modifiers.ctrl
812 && !self.modifiers.alt
813 && !self.modifiers.meta
814 && let Some(raw) = key_event.text.as_deref()
815 {
816 let text: String = raw
817 .chars()
818 .filter(|c| !c.is_control() && *c != '\n' && *c != '\r')
819 .collect();
820 if !text.is_empty()
821 && let Some(fid) = self.sched.focused
822 {
823 let key = self.tf_key_of(fid);
824 if let Some(state_rc) = self.textfield_states.get(&key) {
825 let mut st = state_rc.borrow_mut();
826 st.insert_text(&text);
827 self.notify_text_change(fid, st.text.clone());
828 App::tf_ensure_caret_visible(&mut st);
829 self.request_redraw();
830 }
831 }
832 }
833 } else if key_event.state == ElementState::Released {
834 if let Some(active_id) = self.key_pressed_active {
836 match key_event.physical_key {
837 PhysicalKey::Code(KeyCode::Space)
838 | PhysicalKey::Code(KeyCode::Enter) => {
839 self.pressed_ids.remove(&active_id);
840 self.key_pressed_active = None;
841
842 if let Some(f) = &self.frame_cache
843 && let Some(hit) =
844 f.hit_regions.iter().find(|h| h.id == active_id)
845 && let Some(cb) = &hit.on_click
846 {
847 cb();
848 if let Some(node) =
849 f.semantics_nodes.iter().find(|n| n.id == active_id)
850 {
851 let label = node.label.as_deref().unwrap_or("");
852 self.a11y.announce(&format!("Activated {}", label));
853 }
854 }
855 self.request_redraw();
856 }
857 _ => {}
858 }
859 }
860 }
861 }
862
863 WindowEvent::Ime(ime) => {
864 use winit::event::Ime;
865 if let Some(focused_id) = self.sched.focused {
866 let key = self.tf_key_of(focused_id);
867 if let Some(state_rc) = self.textfield_states.get(&key) {
868 let mut state = state_rc.borrow_mut();
869 match ime {
870 Ime::Enabled => {
871 self.ime_preedit = false;
873 }
874 Ime::Preedit(text, cursor) => {
875 let cursor_usize = cursor.map(|(a, b)| (a, b));
876 state.set_composition(text.clone(), cursor_usize);
877 self.ime_preedit = !text.is_empty();
878 if let Some(f) = &self.frame_cache
879 && let Some(hit) =
880 f.hit_regions.iter().find(|h| h.id == focused_id)
881 {
882 let inner = Rect {
883 x: hit.rect.x + dp_to_px(TF_PADDING_X_DP),
884 y: hit.rect.y,
885 w: hit.rect.w,
886 h: hit.rect.h,
887 };
888 tf_ensure_visible_in_rect(&mut state, inner);
889 }
890 self.notify_text_change(focused_id, state.text.clone());
892 self.request_redraw();
893 }
894 Ime::Commit(text) => {
895 state.commit_composition(text);
896 self.ime_preedit = false;
897 if let Some(f) = &self.frame_cache
898 && let Some(hit) =
899 f.hit_regions.iter().find(|h| h.id == focused_id)
900 {
901 let inner = Rect {
902 x: hit.rect.x + dp_to_px(TF_PADDING_X_DP),
903 y: hit.rect.y,
904 w: hit.rect.w,
905 h: hit.rect.h,
906 };
907 tf_ensure_visible_in_rect(&mut state, inner);
908 }
909 self.notify_text_change(focused_id, state.text.clone());
910 self.request_redraw();
911 }
912 Ime::Disabled => {
913 self.ime_preedit = false;
914 if state.composition.is_some() {
915 state.cancel_composition();
916 if let Some(f) = &self.frame_cache
917 && let Some(hit) =
918 f.hit_regions.iter().find(|h| h.id == focused_id)
919 {
920 let inner = Rect {
921 x: hit.rect.x + dp_to_px(TF_PADDING_X_DP),
922 y: hit.rect.y,
923 w: hit.rect.w,
924 h: hit.rect.h,
925 };
926 tf_ensure_visible_in_rect(&mut state, inner);
927 }
928 self.notify_text_change(focused_id, state.text.clone());
929 }
930 self.request_redraw();
931 }
932 }
933 }
934 }
935 }
936 WindowEvent::RedrawRequested => {
937 if let (Some(backend), Some(win)) =
938 (self.backend.as_mut(), self.window.as_ref())
939 {
940 let t0 = Instant::now();
941 let scale = win.scale_factor() as f32;
942 let size_px_u32 = self.sched.size;
943 let focused = self.sched.focused;
944
945 let frame = compose_frame(
946 &mut self.sched,
947 &mut self.root,
948 scale,
949 size_px_u32,
950 self.hover_id,
951 &self.pressed_ids,
952 &self.textfield_states,
953 focused,
954 );
955
956 let build_layout_ms = (Instant::now() - t0).as_secs_f32() * 1000.0;
957
958 self.a11y.publish_tree(&frame.semantics_nodes);
960 if self.last_focus != self.sched.focused {
962 let focused_node = self
963 .sched
964 .focused
965 .and_then(|id| frame.semantics_nodes.iter().find(|n| n.id == id));
966 self.a11y.focus_changed(focused_node);
967 self.last_focus = self.sched.focused;
968 }
969
970 let mut scene = frame.scene.clone();
972 self.inspector.hud.metrics = Some(repose_devtools::Metrics {
974 build_layout_ms,
975 scene_nodes: scene.nodes.len(),
976 });
977 self.inspector.frame(&mut scene);
978 backend
979 .frame(&scene, GlyphRasterConfig { px: 18.0 * scale });
981 self.frame_cache = Some(frame);
982 }
983 }
984 _ => {}
985 }
986 }
987
988 fn about_to_wait(&mut self, _el: &winit::event_loop::ActiveEventLoop) {
989 self.request_redraw();
990 }
991
992 fn new_events(
993 &mut self,
994 _: &winit::event_loop::ActiveEventLoop,
995 _: winit::event::StartCause,
996 ) {
997 }
998 fn user_event(&mut self, _: &winit::event_loop::ActiveEventLoop, _: ()) {}
999 fn device_event(
1000 &mut self,
1001 _: &winit::event_loop::ActiveEventLoop,
1002 _: winit::event::DeviceId,
1003 _: winit::event::DeviceEvent,
1004 ) {
1005 }
1006 fn suspended(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1007 fn exiting(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1008 fn memory_warning(&mut self, _: &winit::event_loop::ActiveEventLoop) {}
1009 }
1010
1011 impl App {
1012 fn announce_focus_change(&mut self) {
1013 if let Some(f) = &self.frame_cache {
1014 let focused_node = self
1015 .sched
1016 .focused
1017 .and_then(|id| f.semantics_nodes.iter().find(|n| n.id == id));
1018 self.a11y.focus_changed(focused_node);
1019 }
1020 }
1021 fn notify_text_change(&self, id: u64, text: String) {
1022 if let Some(f) = &self.frame_cache
1023 && let Some(h) = f.hit_regions.iter().find(|h| h.id == id)
1024 && let Some(cb) = &h.on_text_change
1025 {
1026 cb(text);
1027 }
1028 }
1029 fn tf_key_of(&self, visual_id: u64) -> u64 {
1030 if let Some(f) = &self.frame_cache
1031 && let Some(hr) = f.hit_regions.iter().find(|h| h.id == visual_id)
1032 {
1033 return hr.tf_state_key.unwrap_or(hr.id);
1034 }
1035 visual_id
1036 }
1037 }
1038
1039 let event_loop = EventLoop::new()?;
1040 let mut app = App::new(Box::new(root));
1041 repose_core::animation::set_clock(Box::new(repose_core::animation::SystemClock));
1043 event_loop.run_app(&mut app)?;
1044 Ok(())
1045}
1046
1047pub trait A11yBridge: Send {
1055 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]);
1057
1058 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>);
1060
1061 fn announce(&mut self, msg: &str);
1063}
1064
1065struct NoopA11y;
1066impl A11yBridge for NoopA11y {
1067 fn publish_tree(&mut self, _nodes: &[repose_core::runtime::SemNode]) {
1068 }
1070 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1071 if let Some(n) = node {
1072 log::info!("A11y focus: {:?} {:?}", n.role, n.label);
1073 } else {
1074 log::info!("A11y focus: None");
1075 }
1076 }
1077 fn announce(&mut self, msg: &str) {
1078 log::info!("A11y announce: {msg}");
1079 }
1080}
1081
1082#[cfg(target_os = "linux")]
1083struct LinuxAtspiStub;
1084#[cfg(target_os = "linux")]
1085impl A11yBridge for LinuxAtspiStub {
1086 fn publish_tree(&mut self, nodes: &[repose_core::runtime::SemNode]) {
1087 log::debug!("AT-SPI stub: publish {} nodes", nodes.len());
1088 }
1089 fn focus_changed(&mut self, node: Option<&repose_core::runtime::SemNode>) {
1090 if let Some(n) = node {
1091 log::info!("AT-SPI stub focus: {:?} {:?}", n.role, n.label);
1092 } else {
1093 log::info!("AT-SPI stub focus: None");
1094 }
1095 }
1096 fn announce(&mut self, msg: &str) {
1097 log::info!("AT-SPI stub announce: {msg}");
1098 }
1099}