1use core_glyph::{
2 clear_redraw, needs_redraw, tick_tweens, FlatView, FlatViewKind, Signal, Theme, View, ViewTree,
3};
4use std::path::PathBuf;
5use render_glyph::{GpuContext, Renderer};
6use crate::menu::{install_menu_macos, poll_menu_events, MenuBar};
7#[cfg(target_os = "windows")]
8use crate::menu::install_menu_windows;
9use std::collections::HashMap;
10use std::sync::{Arc, Mutex};
11use std::time::Instant;
12use winit::{
13 application::ApplicationHandler,
14 event::{ElementState, KeyEvent, MouseButton, MouseScrollDelta, WindowEvent},
15 event_loop::{ActiveEventLoop, EventLoop},
16 keyboard::{Key, ModifiersState, NamedKey},
17 window::{Cursor, CursorIcon, Window, WindowId},
18};
19
20
21
22type BuildViewFn = Box<dyn Fn(&WindowOpener, &WindowCloser) -> (Theme, View) + Send>;
23
24pub struct WindowRequest {
25 pub build_view: BuildViewFn,
26 pub title: String,
27 pub width: f64,
28 pub height: f64,
29 pub theme: Theme,
30}
31
32#[derive(Clone)]
34pub struct WindowOpener(Arc<Mutex<Vec<WindowRequest>>>);
35
36impl WindowOpener {
37 fn new() -> (Self, Arc<Mutex<Vec<WindowRequest>>>) {
38 let q = Arc::new(Mutex::new(Vec::new()));
39 (Self(Arc::clone(&q)), q)
40 }
41
42 pub fn open(
43 &self,
44 build_view: impl Fn(&WindowOpener, &WindowCloser) -> (Theme, View) + Send + 'static,
45 title: impl Into<String>,
46 width: f64,
47 height: f64,
48 theme: Theme,
49 ) {
50 self.0.lock().unwrap().push(WindowRequest {
51 build_view: Box::new(build_view),
52 title: title.into(),
53 width,
54 height,
55 theme,
56 });
57 }
58}
59
60#[derive(Clone)]
65pub struct WindowCloser {
66 id: Arc<Mutex<Option<WindowId>>>,
69 queue: Arc<Mutex<Vec<WindowId>>>,
71}
72
73impl WindowCloser {
74 fn new(queue: Arc<Mutex<Vec<WindowId>>>) -> Self {
75 Self {
76 id: Arc::new(Mutex::new(None)),
77 queue,
78 }
79 }
80
81 fn set_id(&self, id: WindowId) {
82 *self.id.lock().unwrap() = Some(id);
83 }
84
85 pub fn close(&self) {
87 if let Some(id) = *self.id.lock().unwrap() {
88 self.queue.lock().unwrap().push(id);
89 }
90 }
91}
92
93struct HitItem {
97 x: f32,
98 y: f32,
99 w: f32,
100 h: f32,
101 kind: HitKind,
102}
103
104enum HitKind {
105 Button(bool),
106 Text,
107 Slider,
108}
109
110#[derive(Default)]
111struct TextEditState {
112 focused_flat_index: Option<usize>,
113 selection_anchor: Option<usize>,
114 selection: Option<(usize, usize)>,
115 composing: Option<(usize, String)>,
116}
117
118struct ScrollItem {
120 cx: f32,
122 cy: f32,
123 w: f32,
124 h: f32,
125 offset_x: Signal<f32>,
126 offset_y: Signal<f32>,
127 max_x: f32,
128 max_y: f32,
129 enclosing: Vec<(Signal<f32>, Signal<f32>)>,
132}
133
134struct WindowState {
135 window: Arc<Window>,
136 renderer: Renderer,
137 build_view: BuildViewFn,
138 closer: WindowCloser,
139 theme: Theme,
140 cursor_pos: (f32, f32),
141 frame: u32,
142 hit_items: Vec<HitItem>,
143 scroll_items: Vec<ScrollItem>,
144 scroll_vx: f32,
145 scroll_vy: f32,
146 last_scroll: Option<Instant>,
147 flat_cache: Vec<FlatView>,
148 scaled_cache: Vec<FlatView>,
149 modifiers: ModifiersState,
150 text_edit: TextEditState,
151 scroll_dirty: bool,
155 vlist_ranges: Vec<f32>,
157 dragging_slider: Option<usize>,
159}
160
161impl WindowState {
162 fn build(&mut self, opener: &WindowOpener) -> View {
164 let (theme, view) = (self.build_view)(opener, &self.closer);
165 self.theme = theme;
166 view
167 }
168
169 fn scale(&self) -> f32 {
170 self.window.scale_factor() as f32
171 }
172
173 fn lw(&self) -> f32 {
175 self.renderer.surface_cfg.width as f32 / self.scale()
176 }
177
178 fn lh(&self) -> f32 {
180 self.renderer.surface_cfg.height as f32 / self.scale()
181 }
182}
183
184pub struct App {
187 ctx: Option<Arc<GpuContext>>,
188 windows: HashMap<WindowId, WindowState>,
189 opener: WindowOpener,
190 queue: Arc<Mutex<Vec<WindowRequest>>>,
191 pending_close: Arc<Mutex<Vec<WindowId>>>,
192 initial: Option<WindowRequest>,
193 last_tick: Option<Instant>,
194 pending_fonts: Vec<Vec<u8>>,
195 pending_font_files: Vec<PathBuf>,
196 on_quit: Option<Box<dyn Fn() + Send>>,
197 on_open_file: Option<Box<dyn Fn(PathBuf) + Send>>,
198 on_focus: Option<Box<dyn Fn(bool) + Send>>,
199 menu: Option<MenuBar>,
200}
201
202pub struct AppBuilder {
204 build_view: BuildViewFn,
205 theme: Theme,
206 title: String,
207 width: f64,
208 height: f64,
209 fonts: Vec<Vec<u8>>,
210 font_files: Vec<PathBuf>,
211 on_quit: Option<Box<dyn Fn() + Send>>,
212 on_open_file: Option<Box<dyn Fn(PathBuf) + Send>>,
213 on_focus: Option<Box<dyn Fn(bool) + Send>>,
214 menu: Option<MenuBar>,
215}
216
217impl AppBuilder {
218 pub fn font(mut self, data: Vec<u8>) -> Self {
220 self.fonts.push(data);
221 self
222 }
223
224 pub fn font_file(mut self, path: impl Into<PathBuf>) -> Self {
226 self.font_files.push(path.into());
227 self
228 }
229
230 pub fn on_quit(mut self, f: impl Fn() + Send + 'static) -> Self {
232 self.on_quit = Some(Box::new(f));
233 self
234 }
235
236 pub fn on_open_file(mut self, f: impl Fn(PathBuf) + Send + 'static) -> Self {
238 self.on_open_file = Some(Box::new(f));
239 self
240 }
241
242 pub fn on_focus(mut self, f: impl Fn(bool) + Send + 'static) -> Self {
244 self.on_focus = Some(Box::new(f));
245 self
246 }
247
248 pub fn menu(mut self, menu: MenuBar) -> Self {
250 self.menu = Some(menu);
251 self
252 }
253
254 pub fn run(self) {
255 let (opener, queue) = WindowOpener::new();
256 let pending_close = Arc::new(Mutex::new(Vec::<WindowId>::new()));
257 let initial = WindowRequest {
258 build_view: self.build_view,
259 title: self.title,
260 width: self.width,
261 height: self.height,
262 theme: self.theme,
263 };
264 let event_loop = EventLoop::new().expect("event loop");
265 let mut app = App {
266 ctx: None,
267 windows: HashMap::new(),
268 opener,
269 queue,
270 pending_close,
271 initial: Some(initial),
272 last_tick: None,
273 pending_fonts: self.fonts,
274 pending_font_files: self.font_files,
275 on_quit: self.on_quit,
276 on_open_file: self.on_open_file,
277 on_focus: self.on_focus,
278 menu: self.menu,
279 };
280 event_loop.run_app(&mut app).expect("event loop run");
281 }
282}
283
284impl App {
285 pub fn run(
287 build_view: impl Fn(&WindowOpener, &WindowCloser) -> (Theme, View) + Send + 'static,
288 theme: Theme,
289 title: impl Into<String>,
290 width: f64,
291 height: f64,
292 ) {
293 App::builder(build_view, theme, title, width, height).run();
294 }
295
296 pub fn builder(
298 build_view: impl Fn(&WindowOpener, &WindowCloser) -> (Theme, View) + Send + 'static,
299 theme: Theme,
300 title: impl Into<String>,
301 width: f64,
302 height: f64,
303 ) -> AppBuilder {
304 AppBuilder {
305 build_view: Box::new(build_view),
306 theme,
307 title: title.into(),
308 width,
309 height,
310 fonts: Vec::new(),
311 font_files: Vec::new(),
312 on_quit: None,
313 on_open_file: None,
314 on_focus: None,
315 menu: None,
316 }
317 }
318
319 fn apply_pending_fonts(&self, renderer: &mut Renderer) {
320 for data in &self.pending_fonts {
321 renderer.load_font(data.clone());
322 }
323 for path in &self.pending_font_files {
324 if let Err(e) = renderer.load_font_file(path) {
325 eprintln!("glyph: failed to load font {:?}: {}", path, e);
326 }
327 }
328 }
329
330 fn open_window(&mut self, req: WindowRequest, event_loop: &ActiveEventLoop) {
331 let ctx = self
332 .ctx
333 .as_ref()
334 .expect("GpuContext not initialised")
335 .clone();
336 let window = Arc::new(
337 event_loop
338 .create_window(
339 Window::default_attributes()
340 .with_title(&req.title)
341 .with_inner_size(winit::dpi::LogicalSize::new(req.width, req.height)),
342 )
343 .expect("window"),
344 );
345 let size = window.inner_size();
346 let (surface, surface_cfg) =
347 ctx.create_surface(Arc::clone(&window), size.width.max(1), size.height.max(1));
348 let mut renderer = Renderer::new(Arc::clone(&ctx), surface, surface_cfg);
349 self.apply_pending_fonts(&mut renderer);
350 let id = window.id();
351 let closer = WindowCloser::new(Arc::clone(&self.pending_close));
352 closer.set_id(id);
353 self.windows.insert(
354 id,
355 WindowState {
356 window,
357 renderer,
358 build_view: req.build_view,
359 closer,
360 theme: req.theme,
361 cursor_pos: (0.0, 0.0),
362 frame: 0,
363 hit_items: Vec::new(),
364 scroll_items: Vec::new(),
365 scroll_vx: 0.0,
366 scroll_vy: 0.0,
367 last_scroll: None,
368 flat_cache: Vec::new(),
369 scaled_cache: Vec::new(),
370 modifiers: ModifiersState::empty(),
371 text_edit: TextEditState::default(),
372 scroll_dirty: false,
373 vlist_ranges: Vec::new(),
374 dragging_slider: None,
375 },
376 );
377 }
378}
379
380impl ApplicationHandler for App {
381 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
382 if self.ctx.is_some() {
383 return;
384 }
385 if let Some(req) = self.initial.take() {
388 let window = Arc::new(
389 event_loop
390 .create_window(
391 Window::default_attributes()
392 .with_title(&req.title)
393 .with_inner_size(winit::dpi::LogicalSize::new(req.width, req.height)),
394 )
395 .expect("window"),
396 );
397 let ctx = pollster::block_on(GpuContext::new_with_window(Arc::clone(&window)));
398 self.ctx = Some(Arc::clone(&ctx));
399 if let Some(ref mb) = self.menu {
401 install_menu_macos(mb);
402 #[cfg(target_os = "windows")]
403 {
404 use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
405 if let Ok(handle) = window.window_handle() {
406 if let RawWindowHandle::Win32(h) = handle.as_raw() {
407 install_menu_windows(mb, h.hwnd.get());
408 }
409 }
410 }
411 }
412 let size = window.inner_size();
413 let (surface, surface_cfg) =
414 ctx.create_surface(Arc::clone(&window), size.width.max(1), size.height.max(1));
415 let mut renderer = Renderer::new(ctx, surface, surface_cfg);
416 self.apply_pending_fonts(&mut renderer);
417 let id = window.id();
418 let closer = WindowCloser::new(Arc::clone(&self.pending_close));
419 closer.set_id(id);
420 self.windows.insert(
421 id,
422 WindowState {
423 window,
424 renderer,
425 build_view: req.build_view,
426 closer,
427 theme: req.theme,
428 cursor_pos: (0.0, 0.0),
429 frame: 0,
430 hit_items: Vec::new(),
431 scroll_items: Vec::new(),
432 scroll_vx: 0.0,
433 scroll_vy: 0.0,
434 last_scroll: None,
435 flat_cache: Vec::new(),
436 scaled_cache: Vec::new(),
437 modifiers: ModifiersState::empty(),
438 text_edit: TextEditState::default(),
439 scroll_dirty: false,
440 vlist_ranges: Vec::new(),
441 dragging_slider: None,
442 },
443 );
444 }
445 }
446
447 fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
448 let now = Instant::now();
449 let dt = self
450 .last_tick
451 .map_or(0.0, |t| now.duration_since(t).as_secs_f32())
452 .min(0.05); self.last_tick = Some(now);
454
455 if tick_tweens(dt) {
456 for ws in self.windows.values() {
457 ws.window.request_redraw();
458 }
459 }
460
461 let mut any_scrolling = false;
464 for ws in self.windows.values_mut() {
465 let speed = ws.scroll_vx.abs().max(ws.scroll_vy.abs());
466 if speed > 1.0 {
467 any_scrolling = true;
468 let (cur_x, cur_y) = ws.cursor_pos;
469 let only_scroll = !needs_redraw();
470 let dx = ws.scroll_vx * dt;
471 let dy = ws.scroll_vy * dt;
472 apply_scroll(&ws.scroll_items, cur_x, cur_y, dx, dy);
473 let decay = (-dt / 0.35).exp();
474 ws.scroll_vx *= decay;
475 ws.scroll_vy *= decay;
476 clear_redraw();
477 if only_scroll {
478 ws.scroll_dirty = true;
479 }
480 ws.window.request_redraw();
481 } else {
482 ws.scroll_vx = 0.0;
483 ws.scroll_vy = 0.0;
484 }
485 }
486
487 let recently_scrolled = self.windows.values().any(|ws| {
491 ws.last_scroll
492 .is_some_and(|t| now.duration_since(t).as_millis() < 100)
493 });
494 if any_scrolling || recently_scrolled {
495 for ws in self.windows.values() {
496 ws.window.request_redraw();
497 }
498 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
499 } else {
500 event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
501 }
502
503 if let Some(ref mb) = self.menu {
505 poll_menu_events(&mb.handlers);
506 }
507
508 let pending: Vec<WindowRequest> = self.queue.lock().unwrap().drain(..).collect();
509 for req in pending {
510 self.open_window(req, event_loop);
511 }
512
513 let to_close: Vec<WindowId> = self.pending_close.lock().unwrap().drain(..).collect();
515 for id in to_close {
516 self.windows.remove(&id);
517 }
518
519 if self.windows.is_empty() && self.ctx.is_some() {
520 if let Some(ref f) = self.on_quit { f(); }
521 event_loop.exit();
522 }
523 }
524
525 fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
526 let opener = self.opener.clone();
527
528 match event {
529 WindowEvent::CloseRequested => {
530 self.windows.remove(&id);
531 if self.windows.is_empty() {
532 if let Some(ref f) = self.on_quit { f(); }
533 event_loop.exit();
534 }
535 }
536
537 WindowEvent::Focused(gained) => {
538 if let Some(ref f) = self.on_focus { f(gained); }
539 }
540
541 WindowEvent::DroppedFile(path) => {
542 if let Some(ref f) = self.on_open_file { f(path); }
543 }
544
545 WindowEvent::Resized(size) => {
546 if let Some(ws) = self.windows.get_mut(&id) {
547 ws.renderer.resize(size.width, size.height);
548 ws.window.request_redraw();
549 }
550 }
551
552 WindowEvent::CursorMoved { position, .. } => {
553 let Some(ws) = self.windows.get_mut(&id) else {
554 return;
555 };
556 let scale = ws.scale();
557 let (px, py) = (position.x as f32 / scale, position.y as f32 / scale);
558 ws.cursor_pos = (px, py);
559
560 let mut icon = CursorIcon::Default;
563 let mut needs_rebuild = false;
564 for item in &ws.hit_items {
565 let hit = px >= item.x
566 && px <= item.x + item.w
567 && py >= item.y
568 && py <= item.y + item.h;
569 match &item.kind {
570 HitKind::Button(has_hover) => {
571 if hit {
572 icon = CursorIcon::Pointer;
573 }
574 if *has_hover {
575 needs_rebuild = true;
576 }
577 }
578 HitKind::Text => {
579 if hit {
580 icon = CursorIcon::Text;
581 }
582 }
583 HitKind::Slider => {
584 if hit { icon = CursorIcon::EwResize; }
585 }
586 }
587 }
588 if let Some(drag_idx) = ws.dragging_slider {
590 if let Some(fv) = ws.scaled_cache.get(drag_idx) {
591 if let FlatViewKind::Slider { on_drag, .. } = &fv.kind {
592 let l = fv.layout.location.x;
593 let w = fv.layout.size.width;
594 let norm = ((px - l) / w).clamp(0.0, 1.0);
595 on_drag(norm);
596 ws.window.request_redraw();
597 }
598 }
599 }
600 ws.window.set_cursor(Cursor::Icon(icon));
601
602 if needs_rebuild {
603 let mut changed = false;
606 let mut scroll_stack: Vec<(f32, f32)> = Vec::new();
607 let mut pending_scroll: Option<(f32, f32)> = None;
608 for fv in &ws.flat_cache {
609 match &fv.kind {
610 FlatViewKind::ScrollRegion { offset_x, offset_y, .. } => {
611 pending_scroll = Some((offset_x.get(), offset_y.get()));
612 continue;
613 }
614 FlatViewKind::ClipStart { .. } => {
615 scroll_stack.push(pending_scroll.take().unwrap_or((0.0, 0.0)));
616 continue;
617 }
618 FlatViewKind::ClipEnd => { scroll_stack.pop(); continue; }
619 FlatViewKind::OpacityStart { .. } | FlatViewKind::OpacityEnd => continue,
620 _ => {}
621 }
622 let sox: f32 = scroll_stack.iter().map(|(ox, _)| ox).sum();
623 let soy: f32 = scroll_stack.iter().map(|(_, oy)| oy).sum();
624 let l = fv.layout.location.x - sox;
625 let t = fv.layout.location.y - soy;
626 let hit = px >= l
627 && px <= l + fv.layout.size.width
628 && py >= t
629 && py <= t + fv.layout.size.height;
630 if let FlatViewKind::Button {
631 on_hover: Some(on_hover),
632 ..
633 } = &fv.kind
634 {
635 on_hover(hit);
636 changed = true;
637 }
638 }
639 clear_redraw();
642 if changed {
643 ws.window.request_redraw();
644 }
645 }
646 }
647
648 WindowEvent::CursorLeft { .. } => {
649 let Some(ws) = self.windows.get_mut(&id) else {
650 return;
651 };
652 ws.window.set_cursor(Cursor::Icon(CursorIcon::Default));
653 let has_hover_btns = ws
654 .hit_items
655 .iter()
656 .any(|i| matches!(&i.kind, HitKind::Button(true)));
657 if has_hover_btns {
658 let mut changed = false;
659 for fv in &ws.flat_cache {
660 if let FlatViewKind::Button {
661 on_hover: Some(on_hover),
662 ..
663 } = &fv.kind
664 {
665 on_hover(false);
666 changed = true;
667 }
668 }
669 clear_redraw();
670 if changed {
671 ws.window.request_redraw();
672 }
673 }
674 }
675
676 WindowEvent::ModifiersChanged(modifiers) => {
677 let Some(ws) = self.windows.get_mut(&id) else {
678 return;
679 };
680 ws.modifiers = modifiers.state();
681 }
682
683 WindowEvent::MouseWheel { delta, .. } => {
684 let Some(ws) = self.windows.get_mut(&id) else {
685 return;
686 };
687 let (cur_x, cur_y) = ws.cursor_pos;
688 let only_scroll = !needs_redraw();
691 match delta {
692 MouseScrollDelta::PixelDelta(pos) => {
695 let dx = pos.x as f32;
696 let dy = pos.y as f32;
697 apply_scroll(&ws.scroll_items, cur_x, cur_y, dx, dy);
698 ws.scroll_vx = 0.0;
699 ws.scroll_vy = 0.0;
700 }
701 MouseScrollDelta::LineDelta(x, y) => {
703 let dx = x * 40.0;
704 let dy = y * 40.0;
705 apply_scroll(&ws.scroll_items, cur_x, cur_y, dx, dy);
706 ws.scroll_vx = ws.scroll_vx * 0.8 + dx * 6.0;
707 ws.scroll_vy = ws.scroll_vy * 0.8 + dy * 6.0;
708 }
709 }
710 clear_redraw();
713 ws.last_scroll = Some(Instant::now());
714 if only_scroll {
715 ws.scroll_dirty = true;
716 }
717 ws.window.request_redraw();
718 }
719
720 WindowEvent::MouseInput {
721 state,
722 button: MouseButton::Left,
723 ..
724 } => {
725 let Some(ws) = self.windows.get_mut(&id) else {
726 return;
727 };
728 let (cx, cy) = ws.cursor_pos;
729 let (w, h) = (ws.lw(), ws.lh());
730 let view = ws.build(&opener);
731 let flat = ViewTree::build(view, &ws.theme, w, h, &mut ws.renderer.measurer());
734 let pressed = state == ElementState::Pressed;
735 let mut clicked = false;
736 let mut hit_text_input = false;
737 let mut scroll_stack: Vec<(f32, f32)> = Vec::new();
738 let mut pending_scroll: Option<(f32, f32)> = None;
739 for (idx, fv) in flat.iter().enumerate() {
740 match &fv.kind {
742 FlatViewKind::ScrollRegion { offset_x, offset_y, .. } => {
743 pending_scroll = Some((offset_x.get(), offset_y.get()));
744 continue;
745 }
746 FlatViewKind::ClipStart { .. } => {
747 scroll_stack.push(pending_scroll.take().unwrap_or((0.0, 0.0)));
748 continue;
749 }
750 FlatViewKind::ClipEnd => {
751 scroll_stack.pop();
752 continue;
753 }
754 FlatViewKind::OpacityStart { .. } | FlatViewKind::OpacityEnd => continue,
755 _ => {}
756 }
757 let sox: f32 = scroll_stack.iter().map(|(ox, _)| ox).sum();
758 let soy: f32 = scroll_stack.iter().map(|(_, oy)| oy).sum();
759 let l = fv.layout.location.x - sox;
760 let t = fv.layout.location.y - soy;
761 let hit = cx >= l
762 && cx <= l + fv.layout.size.width
763 && cy >= t
764 && cy <= t + fv.layout.size.height;
765 match &fv.kind {
766 FlatViewKind::Button {
767 on_click, on_press, disabled, ..
768 } => {
769 if *disabled { continue; }
770 if hit {
771 if let Some(op) = on_press {
772 op(pressed);
773 }
774 if pressed && !clicked {
775 on_click();
776 clicked = true;
777 }
778 } else if !pressed {
779 if let Some(op) = on_press {
780 op(false);
781 }
782 }
783 }
784 FlatViewKind::TextInput {
785 focused,
786 cursor,
787 value,
788 font_size,
789 disabled,
790 ..
791 } if pressed => {
792 if *disabled { continue; }
793 focused.set(hit);
794 ws.window.set_ime_allowed(hit);
795 if hit {
796 hit_text_input = true;
797 ws.frame = 0;
798 let val = value.get();
799 let pad = 8.0;
800 let click_offset = (cx - l - pad).max(0.0);
801 let byte_idx = ws.renderer.cursor_for_x(
802 &val,
803 *font_size,
804 click_offset,
805 );
806 cursor.set(byte_idx);
807 ws.text_edit.focused_flat_index = Some(idx);
808 ws.text_edit.composing = None;
809 if ws.modifiers.shift_key() {
810 let anchor =
811 ws.text_edit.selection_anchor.unwrap_or(cursor.get());
812 ws.text_edit.selection_anchor = Some(anchor);
813 ws.text_edit.selection =
814 normalized_selection(anchor, byte_idx);
815 } else {
816 ws.text_edit.selection_anchor = None;
817 ws.text_edit.selection = None;
818 }
819 }
820 }
821 FlatViewKind::TextArea {
822 focused,
823 cursor,
824 value,
825 font_size,
826 scroll_y,
827 ..
828 } if pressed => {
829 focused.set(hit);
830 if hit {
831 ws.frame = 0;
832 let val = value.get();
833 let line_height = font_size * 1.4;
834 let pad = 8.0;
835 let rel_y = cy - t - pad + scroll_y.get();
836 let line_idx = (rel_y / line_height).floor().max(0.0) as usize;
837 let lines: Vec<&str> = val.split('\n').collect();
838 let line_idx = line_idx.min(lines.len().saturating_sub(1));
839 let mut byte_offset: usize =
840 lines[..line_idx].iter().map(|l| l.len() + 1).sum();
841 let rel_x = (cx - l - pad).max(0.0);
842 let line_cursor = ws.renderer.cursor_for_x(
843 lines[line_idx],
844 *font_size,
845 rel_x,
846 );
847 byte_offset += line_cursor;
848 cursor.set(byte_offset.min(val.len()));
849 }
850 }
851 FlatViewKind::Slider { on_drag, .. } if pressed && hit => {
852 let norm = ((cx - l) / fv.layout.size.width).clamp(0.0, 1.0);
854 on_drag(norm);
855 ws.dragging_slider = Some(idx);
856 }
857 FlatViewKind::Slider { .. } if !pressed => {
858 ws.dragging_slider = None;
859 }
860 _ => {}
861 }
862 }
863 if pressed && !hit_text_input {
864 ws.text_edit = TextEditState::default();
865 ws.window.set_ime_allowed(false);
866 }
867 if needs_redraw() {
868 clear_redraw();
869 }
870 ws.window.request_redraw();
871 }
872
873 WindowEvent::Ime(ime) => {
874 let Some(ws) = self.windows.get_mut(&id) else {
875 return;
876 };
877 let (w, h) = (ws.lw(), ws.lh());
878 let view = ws.build(&opener);
879 let flat = ViewTree::build(view, &ws.theme, w, h, &mut ws.renderer.measurer());
880 let Some(idx) = ws.text_edit.focused_flat_index else {
881 return;
882 };
883 let Some(fv) = flat.get(idx) else {
884 return;
885 };
886 if let FlatViewKind::TextInput {
887 value,
888 focused,
889 cursor,
890 on_change,
891 ..
892 } = &fv.kind
893 {
894 if !focused.get() {
895 return;
896 }
897 match ime {
898 winit::event::Ime::Preedit(text, _) => {
899 let mut s = value.get();
900 let mut cur = cursor.get().min(s.len());
901 if !text.is_empty() && ws.text_edit.composing.is_none()
902 && delete_selection(&mut s, &mut cur, ws.text_edit.selection) {
903 value.set(s);
904 cursor.set(cur);
905 ws.text_edit.selection = None;
906 ws.text_edit.selection_anchor = None;
907 }
908 ws.text_edit.composing = if text.is_empty() {
909 None
910 } else {
911 Some((cur, text))
912 };
913 ws.window.request_redraw();
914 }
915 winit::event::Ime::Commit(text) => {
916 let mut s = value.get();
917 let mut cur = cursor.get().min(s.len());
918 delete_selection(&mut s, &mut cur, ws.text_edit.selection);
919 s.insert_str(cur, &text);
920 cur += text.len();
921 value.set(s.clone());
922 cursor.set(cur);
923 ws.text_edit.selection = None;
924 ws.text_edit.selection_anchor = None;
925 ws.text_edit.composing = None;
926 if let Some(f) = on_change {
927 f(s);
928 }
929 if needs_redraw() {
930 clear_redraw();
931 }
932 ws.window.request_redraw();
933 }
934 winit::event::Ime::Enabled => {}
935 winit::event::Ime::Disabled => {
936 ws.text_edit.composing = None;
937 ws.window.request_redraw();
938 }
939 }
940 }
941 }
942
943 WindowEvent::KeyboardInput {
944 event:
945 KeyEvent {
946 logical_key,
947 state: ElementState::Pressed,
948 ..
949 },
950 ..
951 } => {
952 let Some(ws) = self.windows.get_mut(&id) else {
953 return;
954 };
955 let (w, h) = (ws.lw(), ws.lh());
956 let view = ws.build(&opener);
957 let flat = ViewTree::build(view, &ws.theme, w, h, &mut ws.renderer.measurer());
958
959 let input_indices: Vec<usize> = flat
961 .iter()
962 .enumerate()
963 .filter(|(_, fv)| {
964 matches!(
965 &fv.kind,
966 FlatViewKind::TextInput { .. } | FlatViewKind::TextArea { .. }
967 )
968 })
969 .map(|(i, _)| i)
970 .collect();
971
972 let mut handled = false;
974 for (pos, &idx) in input_indices.iter().enumerate() {
975 match &flat[idx].kind {
976 FlatViewKind::TextInput {
977 value,
978 focused,
979 cursor,
980 on_change,
981 on_submit,
982 disabled,
983 ..
984 } => {
985 if !focused.get() || *disabled {
986 continue;
987 }
988 ws.text_edit.focused_flat_index = Some(idx);
989 let mut s = value.get();
990 let mut cur = cursor.get().min(s.len());
991 let mut changed = false;
992 let command = ws.modifiers.super_key() || ws.modifiers.control_key();
993 match &logical_key {
994 Key::Character(ch)
995 if command && ch.as_str().eq_ignore_ascii_case("a") =>
996 {
997 ws.text_edit.selection_anchor = Some(0);
998 ws.text_edit.selection = normalized_selection(0, s.len());
999 cursor.set(s.len());
1000 }
1001 Key::Named(NamedKey::Backspace) => {
1002 if delete_selection(&mut s, &mut cur, ws.text_edit.selection) {
1003 value.set(s.clone());
1004 cursor.set(cur);
1005 ws.text_edit.selection = None;
1006 ws.text_edit.selection_anchor = None;
1007 changed = true;
1008 } else if cur > 0 {
1009 let prev = s[..cur]
1010 .char_indices()
1011 .next_back()
1012 .map(|(i, _)| i)
1013 .unwrap_or(0);
1014 s.remove(prev);
1015 cur = prev;
1016 value.set(s.clone());
1017 cursor.set(cur);
1018 changed = true;
1019 }
1020 }
1021 Key::Named(NamedKey::Delete) => {
1022 if delete_selection(&mut s, &mut cur, ws.text_edit.selection) {
1023 value.set(s.clone());
1024 cursor.set(cur);
1025 ws.text_edit.selection = None;
1026 ws.text_edit.selection_anchor = None;
1027 changed = true;
1028 } else if cur < s.len() {
1029 s.remove(cur);
1030 value.set(s.clone());
1031 changed = true;
1032 }
1033 }
1034 Key::Named(NamedKey::ArrowLeft) if cur > 0 => {
1035 let prev = s[..cur]
1036 .char_indices()
1037 .next_back()
1038 .map(|(i, _)| i)
1039 .unwrap_or(0);
1040 cursor.set(prev);
1041 update_selection_for_move(
1042 &mut ws.text_edit,
1043 cur,
1044 prev,
1045 ws.modifiers.shift_key(),
1046 );
1047 }
1048 Key::Named(NamedKey::ArrowRight) if cur < s.len() => {
1049 let next = s[cur..]
1050 .char_indices()
1051 .nth(1)
1052 .map(|(i, _)| cur + i)
1053 .unwrap_or(s.len());
1054 cursor.set(next);
1055 update_selection_for_move(
1056 &mut ws.text_edit,
1057 cur,
1058 next,
1059 ws.modifiers.shift_key(),
1060 );
1061 }
1062 Key::Named(NamedKey::Space) => {
1063 delete_selection(&mut s, &mut cur, ws.text_edit.selection);
1064 s.insert(cur, ' ');
1065 cur += 1;
1066 value.set(s.clone());
1067 cursor.set(cur);
1068 ws.text_edit.selection = None;
1069 ws.text_edit.selection_anchor = None;
1070 changed = true;
1071 }
1072 Key::Named(NamedKey::Home) => {
1073 cursor.set(0);
1074 update_selection_for_move(
1075 &mut ws.text_edit,
1076 cur,
1077 0,
1078 ws.modifiers.shift_key(),
1079 );
1080 }
1081 Key::Named(NamedKey::End) => {
1082 cursor.set(s.len());
1083 update_selection_for_move(
1084 &mut ws.text_edit,
1085 cur,
1086 s.len(),
1087 ws.modifiers.shift_key(),
1088 );
1089 }
1090 Key::Named(NamedKey::Escape) => {
1091 focused.set(false);
1092 ws.text_edit = TextEditState::default();
1093 ws.window.set_ime_allowed(false);
1094 }
1095 Key::Named(NamedKey::Tab) => {
1096 focused.set(false);
1097 let next_idx = input_indices[(pos + 1) % input_indices.len()];
1098 set_focused_at(&flat, next_idx, true);
1099 ws.window.set_ime_allowed(true);
1100 ws.text_edit = TextEditState {
1101 focused_flat_index: Some(next_idx),
1102 ..TextEditState::default()
1103 };
1104 }
1105 Key::Named(NamedKey::Enter) => {
1106 if let Some(f) = on_submit {
1107 f(value.get());
1108 }
1109 focused.set(false);
1110 ws.text_edit = TextEditState::default();
1111 ws.window.set_ime_allowed(false);
1112 }
1113 Key::Character(ch) if !command => {
1114 delete_selection(&mut s, &mut cur, ws.text_edit.selection);
1115 s.insert_str(cur, ch.as_str());
1116 cur += ch.len();
1117 value.set(s.clone());
1118 cursor.set(cur);
1119 ws.text_edit.selection = None;
1120 ws.text_edit.selection_anchor = None;
1121 ws.text_edit.composing = None;
1122 changed = true;
1123 }
1124 _ => {}
1125 }
1126 if changed {
1127 if let Some(f) = on_change {
1128 f(s);
1129 }
1130 }
1131 handled = true;
1132 break;
1133 }
1134 FlatViewKind::TextArea {
1135 value,
1136 focused,
1137 cursor,
1138 scroll_y,
1139 on_change,
1140 font_size,
1141 ..
1142 } => {
1143 if !focused.get() {
1144 continue;
1145 }
1146 let mut s = value.get();
1147 let mut cur = cursor.get().min(s.len());
1148 let mut changed = false;
1149 match &logical_key {
1150 Key::Named(NamedKey::Backspace) if cur > 0 => {
1151 let prev = s[..cur]
1152 .char_indices()
1153 .next_back()
1154 .map(|(i, _)| i)
1155 .unwrap_or(0);
1156 s.remove(prev);
1157 cur = prev;
1158 value.set(s.clone());
1159 cursor.set(cur);
1160 changed = true;
1161 }
1162 Key::Named(NamedKey::Delete) if cur < s.len() => {
1163 s.remove(cur);
1164 value.set(s.clone());
1165 changed = true;
1166 }
1167 Key::Named(NamedKey::ArrowLeft) if cur > 0 => {
1168 let prev = s[..cur]
1169 .char_indices()
1170 .next_back()
1171 .map(|(i, _)| i)
1172 .unwrap_or(0);
1173 cursor.set(prev);
1174 }
1175 Key::Named(NamedKey::ArrowRight) if cur < s.len() => {
1176 let next = s[cur..]
1177 .char_indices()
1178 .nth(1)
1179 .map(|(i, _)| cur + i)
1180 .unwrap_or(s.len());
1181 cursor.set(next);
1182 }
1183 Key::Named(NamedKey::ArrowUp) => {
1184 let line_height = font_size * 1.4;
1185 let (line_idx, col_off) = byte_to_line_col(&s, cur);
1186 if line_idx > 0 {
1187 cur = line_col_to_byte(&s, line_idx - 1, col_off);
1188 cursor.set(cur);
1189 let new_oy = (scroll_y.get() - line_height).max(0.0);
1190 scroll_y.set(new_oy);
1191 }
1192 }
1193 Key::Named(NamedKey::ArrowDown) => {
1194 let line_height = font_size * 1.4;
1195 let lines: Vec<&str> = s.split('\n').collect();
1196 let (line_idx, col_off) = byte_to_line_col(&s, cur);
1197 if line_idx + 1 < lines.len() {
1198 cur = line_col_to_byte(&s, line_idx + 1, col_off);
1199 cursor.set(cur);
1200 scroll_y.set(scroll_y.get() + line_height);
1201 }
1202 }
1203 Key::Named(NamedKey::Space) => {
1204 s.insert(cur, ' ');
1205 cur += 1;
1206 value.set(s.clone());
1207 cursor.set(cur);
1208 changed = true;
1209 }
1210 Key::Named(NamedKey::Home) => {
1211 cursor.set(0);
1212 }
1213 Key::Named(NamedKey::End) => {
1214 cursor.set(s.len());
1215 }
1216 Key::Named(NamedKey::Escape) => {
1217 focused.set(false);
1218 }
1219 Key::Named(NamedKey::Tab) => {
1220 focused.set(false);
1221 let next_idx = input_indices[(pos + 1) % input_indices.len()];
1222 set_focused_at(&flat, next_idx, true);
1223 }
1224 Key::Named(NamedKey::Enter) => {
1225 s.insert(cur, '\n');
1226 cur += 1;
1227 value.set(s.clone());
1228 cursor.set(cur);
1229 changed = true;
1230 }
1231 Key::Character(ch) => {
1232 s.insert_str(cur, ch.as_str());
1233 cur += ch.len();
1234 value.set(s.clone());
1235 cursor.set(cur);
1236 changed = true;
1237 }
1238 _ => {}
1239 }
1240 if changed {
1241 if let Some(f) = on_change {
1242 f(s);
1243 }
1244 }
1245 handled = true;
1246 break;
1247 }
1248 _ => {}
1249 }
1250 }
1251 let _ = handled;
1252 if needs_redraw() {
1253 clear_redraw();
1254 }
1255 ws.window.request_redraw();
1256 }
1257
1258 WindowEvent::RedrawRequested => {
1259 let Some(ws) = self.windows.get_mut(&id) else {
1260 return;
1261 };
1262 ws.frame = ws.frame.wrapping_add(1);
1263 let cursor_visible = (ws.frame / 30) % 2 == 0;
1264 let scale = ws.scale();
1265
1266 let skip_rebuild = ws.scroll_dirty && !ws.scaled_cache.is_empty() && {
1273 let new_ranges = vlist_ranges_from_flat(&ws.flat_cache);
1274 new_ranges == ws.vlist_ranges
1275 };
1276 ws.scroll_dirty = false;
1277
1278 if skip_rebuild {
1279 ws.renderer.render(&ws.scaled_cache, cursor_visible, ws.theme.background, scale);
1280 return;
1281 }
1282
1283 let (w, h) = (ws.lw(), ws.lh());
1284 let view = ws.build(&opener);
1285 let mut flat = ViewTree::build(view, &ws.theme, w, h, &mut ws.renderer.measurer());
1286 decorate_text_input_state(&mut flat, &ws.text_edit);
1287 let any_focused = flat.iter().any(|fv| {
1288 matches!(&fv.kind, FlatViewKind::TextInput { focused, .. } if focused.get())
1289 || matches!(&fv.kind, FlatViewKind::TextArea { focused, .. } if focused.get())
1290 });
1291 if any_focused {
1292 ws.window.request_redraw();
1293 }
1294 {
1295 let mut scroll_stack: Vec<(f32, f32)> = Vec::new();
1296 let mut pending: Option<(f32, f32)> = None;
1297 let mut hit_items: Vec<HitItem> = Vec::new();
1298 for fv in &flat {
1299 match &fv.kind {
1300 FlatViewKind::ScrollRegion {
1301 offset_x, offset_y, ..
1302 } => {
1303 pending = Some((offset_x.get(), offset_y.get()));
1304 }
1305 FlatViewKind::ClipStart { .. } => {
1306 scroll_stack.push(pending.take().unwrap_or((0.0, 0.0)));
1307 }
1308 FlatViewKind::ClipEnd => {
1309 scroll_stack.pop();
1310 }
1311 _ => {
1312 let sox: f32 = scroll_stack.iter().map(|(ox, _)| ox).sum();
1313 let soy: f32 = scroll_stack.iter().map(|(_, oy)| oy).sum();
1314 let x = fv.layout.location.x - sox;
1315 let y = fv.layout.location.y - soy;
1316 let w = fv.layout.size.width;
1317 let h = fv.layout.size.height;
1318 let item = match &fv.kind {
1319 FlatViewKind::Button { on_hover, .. } => Some(HitItem {
1320 x,
1321 y,
1322 w,
1323 h,
1324 kind: HitKind::Button(on_hover.is_some()),
1325 }),
1326 FlatViewKind::TextInput { .. }
1327 | FlatViewKind::TextArea { .. } => Some(HitItem {
1328 x,
1329 y,
1330 w,
1331 h,
1332 kind: HitKind::Text,
1333 }),
1334 FlatViewKind::Slider { .. } => Some(HitItem {
1335 x,
1336 y,
1337 w,
1338 h,
1339 kind: HitKind::Slider,
1340 }),
1341 _ => None,
1342 };
1343 if let Some(item) = item {
1344 hit_items.push(item);
1345 }
1346 }
1347 }
1348 }
1349 ws.hit_items = hit_items;
1350 }
1351 ws.scroll_items = {
1354 let mut items = Vec::new();
1355 let mut enclosing_stack: Vec<(Signal<f32>, Signal<f32>)> = Vec::new();
1357 #[allow(clippy::type_complexity)]
1358 let mut pending: Option<(Signal<f32>, Signal<f32>, f32, f32, core_glyph::TaffyLayout)> = None;
1359 for fv in &flat {
1360 match &fv.kind {
1361 FlatViewKind::ScrollRegion { offset_x, offset_y, max_x, max_y, .. } => {
1362 pending = Some((offset_x.clone(), offset_y.clone(), *max_x, *max_y, fv.layout));
1363 }
1364 FlatViewKind::ClipStart { .. } => {
1365 if let Some((offset_x, offset_y, max_x, max_y, l)) = pending.take() {
1366 items.push(ScrollItem {
1367 cx: l.location.x,
1368 cy: l.location.y,
1369 w: l.size.width,
1370 h: l.size.height,
1371 enclosing: enclosing_stack.clone(),
1372 offset_x: offset_x.clone(),
1373 offset_y: offset_y.clone(),
1374 max_x,
1375 max_y,
1376 });
1377 enclosing_stack.push((offset_x, offset_y));
1378 } else {
1379 enclosing_stack.push((Signal::new(0.0), Signal::new(0.0)));
1381 }
1382 }
1383 FlatViewKind::ClipEnd => { enclosing_stack.pop(); }
1384 _ => {}
1385 }
1386 }
1387 items
1388 };
1389 ws.vlist_ranges = vlist_ranges_from_flat(&flat);
1390 ws.flat_cache = flat.clone();
1391 ws.scaled_cache = scale_flat(flat, scale);
1392 clear_redraw();
1393 ws.renderer
1394 .render(&ws.scaled_cache, cursor_visible, ws.theme.background, scale);
1395 }
1396
1397 _ => {}
1398 }
1399 }
1400}
1401
1402fn byte_to_line_col(s: &str, byte: usize) -> (usize, usize) {
1405 let before = &s[..byte.min(s.len())];
1406 let line_idx = before.chars().filter(|&c| c == '\n').count();
1407 let col = before.rfind('\n').map(|p| byte - p - 1).unwrap_or(byte);
1408 (line_idx, col)
1409}
1410
1411fn line_col_to_byte(s: &str, target_line: usize, col: usize) -> usize {
1412 let mut offset = 0;
1413 for (li, line) in s.split('\n').enumerate() {
1414 if li == target_line {
1415 return offset + col.min(line.len());
1416 }
1417 offset += line.len() + 1;
1418 }
1419 s.len()
1420}
1421
1422fn set_focused_at(flat: &[FlatView], idx: usize, focused: bool) {
1423 match &flat[idx].kind {
1424 FlatViewKind::TextInput { focused: f, .. } => f.set(focused),
1425 FlatViewKind::TextArea { focused: f, .. } => f.set(focused),
1426 _ => {}
1427 }
1428}
1429
1430fn normalized_selection(anchor: usize, cursor: usize) -> Option<(usize, usize)> {
1431 if anchor == cursor {
1432 None
1433 } else {
1434 Some((anchor.min(cursor), anchor.max(cursor)))
1435 }
1436}
1437
1438fn update_selection_for_move(
1439 edit: &mut TextEditState,
1440 old_cursor: usize,
1441 new_cursor: usize,
1442 extend: bool,
1443) {
1444 if extend {
1445 let anchor = edit.selection_anchor.unwrap_or(old_cursor);
1446 edit.selection_anchor = Some(anchor);
1447 edit.selection = normalized_selection(anchor, new_cursor);
1448 } else {
1449 edit.selection_anchor = None;
1450 edit.selection = None;
1451 }
1452 edit.composing = None;
1453}
1454
1455fn delete_selection(s: &mut String, cursor: &mut usize, selection: Option<(usize, usize)>) -> bool {
1456 let Some((start, end)) = selection else {
1457 return false;
1458 };
1459 let start = start.min(s.len());
1460 let end = end.min(s.len());
1461 if start >= end || !s.is_char_boundary(start) || !s.is_char_boundary(end) {
1462 return false;
1463 }
1464 s.replace_range(start..end, "");
1465 *cursor = start;
1466 true
1467}
1468
1469fn decorate_text_input_state(flat: &mut [FlatView], edit: &TextEditState) {
1470 let Some(idx) = edit.focused_flat_index else {
1471 return;
1472 };
1473 let Some(fv) = flat.get_mut(idx) else {
1474 return;
1475 };
1476 if let FlatViewKind::TextInput {
1477 selection,
1478 composing,
1479 ..
1480 } = &mut fv.kind
1481 {
1482 *selection = edit.selection;
1483 *composing = edit.composing.clone();
1484 }
1485}
1486
1487fn scale_flat(flat: Vec<FlatView>, scale: f32) -> Vec<FlatView> {
1491 if (scale - 1.0).abs() < f32::EPSILON {
1492 return flat;
1493 }
1494 flat.into_iter()
1495 .map(|fv| {
1496 let l = &fv.layout;
1497 let mut layout = *l;
1498 layout.location.x *= scale;
1499 layout.location.y *= scale;
1500 layout.size.width *= scale;
1501 layout.size.height *= scale;
1502 let kind = match fv.kind {
1503 FlatViewKind::Text {
1504 content,
1505 font_size,
1506 color,
1507 weight,
1508 align,
1509 wrap,
1510 family,
1511 } => FlatViewKind::Text {
1512 content,
1513 font_size: font_size * scale,
1514 color,
1515 weight,
1516 align,
1517 wrap,
1518 family,
1519 },
1520 FlatViewKind::Button {
1521 label,
1522 on_click,
1523 on_hover,
1524 on_press,
1525 bg_color,
1526 hover_bg_color,
1527 press_bg_color,
1528 text_color,
1529 corner_radius,
1530 font_size,
1531 wrap,
1532 family,
1533 disabled,
1534 } => FlatViewKind::Button {
1535 label,
1536 on_click,
1537 on_hover,
1538 on_press,
1539 bg_color,
1540 hover_bg_color,
1541 press_bg_color,
1542 text_color,
1543 corner_radius: corner_radius * scale,
1544 font_size: font_size * scale,
1545 wrap,
1546 family,
1547 disabled,
1548 },
1549 FlatViewKind::TextInput {
1550 value,
1551 focused,
1552 cursor,
1553 scroll_x,
1554 placeholder,
1555 font_size,
1556 bg_color,
1557 text_color,
1558 border_color,
1559 corner_radius,
1560 on_change,
1561 on_submit,
1562 selection,
1563 composing,
1564 disabled,
1565 } => FlatViewKind::TextInput {
1566 value,
1567 focused,
1568 cursor,
1569 scroll_x,
1570 placeholder,
1571 font_size: font_size * scale,
1572 bg_color,
1573 text_color,
1574 border_color,
1575 corner_radius: corner_radius * scale,
1576 on_change,
1577 on_submit,
1578 selection,
1579 composing,
1580 disabled,
1581 },
1582 FlatViewKind::ContainerRect {
1583 bg_color,
1584 border_color,
1585 border_width,
1586 corner_radius,
1587 shadow,
1588 } => {
1589 let shadow = shadow.map(|s| core_glyph::Shadow {
1590 offset_x: s.offset_x * scale,
1591 offset_y: s.offset_y * scale,
1592 blur: s.blur * scale,
1593 color: s.color,
1594 });
1595 FlatViewKind::ContainerRect {
1596 bg_color,
1597 border_color,
1598 border_width: border_width * scale,
1599 corner_radius: corner_radius * scale,
1600 shadow,
1601 }
1602 }
1603 FlatViewKind::ClipStart {
1604 x,
1605 y,
1606 width,
1607 height,
1608 is_virtual_list,
1609 } => FlatViewKind::ClipStart {
1610 x: x * scale,
1611 y: y * scale,
1612 width: width * scale,
1613 height: height * scale,
1614 is_virtual_list,
1615 },
1616 FlatViewKind::Image {
1617 path,
1618 corner_radius,
1619 tint,
1620 } => FlatViewKind::Image {
1621 path,
1622 corner_radius: corner_radius * scale,
1623 tint,
1624 },
1625 FlatViewKind::Slider { value, on_drag } => FlatViewKind::Slider { value, on_drag },
1626 FlatViewKind::TextArea {
1627 value,
1628 focused,
1629 cursor,
1630 scroll_y,
1631 placeholder,
1632 font_size,
1633 bg_color,
1634 text_color,
1635 border_color,
1636 corner_radius,
1637 on_change,
1638 } => FlatViewKind::TextArea {
1639 value,
1640 focused,
1641 cursor,
1642 scroll_y,
1643 placeholder,
1644 font_size: font_size * scale,
1645 bg_color,
1646 text_color,
1647 border_color,
1648 corner_radius: corner_radius * scale,
1649 on_change,
1650 },
1651 FlatViewKind::Rect { color, corner_radius } => FlatViewKind::Rect {
1652 color,
1653 corner_radius: corner_radius * scale,
1654 },
1655 FlatViewKind::ScrollRegion {
1657 offset_x,
1658 offset_y,
1659 max_x,
1660 max_y,
1661 is_virtual_list,
1662 } => FlatViewKind::ScrollRegion {
1663 offset_x,
1664 offset_y,
1665 max_x,
1666 max_y,
1667 is_virtual_list,
1668 },
1669 other => other,
1670 };
1671 FlatView { kind, layout }
1672 })
1673 .collect()
1674}
1675
1676fn vlist_ranges_from_flat(flat: &[FlatView]) -> Vec<f32> {
1684 flat.iter()
1685 .filter_map(|fv| {
1686 if let FlatViewKind::ScrollRegion { offset_y, is_virtual_list: true, .. } = &fv.kind {
1687 Some(offset_y.get())
1688 } else {
1689 None
1690 }
1691 })
1692 .collect()
1693}
1694
1695fn apply_scroll(items: &[ScrollItem], cx: f32, cy: f32, dx: f32, dy: f32) {
1698 for item in items.iter().rev() {
1702 let sox: f32 = item.enclosing.iter().map(|(ox, _)| ox.get()).sum();
1704 let soy: f32 = item.enclosing.iter().map(|(_, oy)| oy.get()).sum();
1705 let sx = item.cx - sox;
1706 let sy = item.cy - soy;
1707 if cx >= sx && cx <= sx + item.w && cy >= sy && cy <= sy + item.h {
1708 let cur_x = item.offset_x.get();
1709 let cur_y = item.offset_y.get();
1710 let nx = (cur_x - dx).clamp(0.0, item.max_x);
1711 let ny = (cur_y - dy).clamp(0.0, item.max_y);
1712 item.offset_x.set(nx);
1713 item.offset_y.set(ny);
1714 return;
1715 }
1716 }
1717}
1718
1719#[cfg(feature = "hot-reload")]
1722fn dispatch_scroll(
1723 view: &View,
1724 theme: &Theme,
1725 flat: &[FlatView],
1726 cx: f32,
1727 cy: f32,
1728 dx: f32,
1729 dy: f32,
1730 flat_idx: &mut usize,
1731) {
1732 match view {
1733 View::Scroll {
1734 child,
1735 offset_x,
1736 offset_y,
1737 ..
1738 } => {
1739 *flat_idx += 1; if let Some(fv) = flat.get(*flat_idx) {
1741 if let FlatViewKind::ClipStart {
1742 x,
1743 y,
1744 width,
1745 height,
1746 ..
1747 } = &fv.kind
1748 {
1749 if cx >= *x && cx <= x + width && cy >= *y && cy <= y + height {
1750 offset_x.set((offset_x.get() - dx).max(0.0));
1751 offset_y.set((offset_y.get() - dy).max(0.0));
1752 }
1753 }
1754 }
1755 *flat_idx += 1;
1756 dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1757 *flat_idx += 1;
1758 }
1759 View::Column {
1760 children,
1761 bg_color,
1762 border_color,
1763 shadow,
1764 clip,
1765 ..
1766 }
1767 | View::Row {
1768 children,
1769 bg_color,
1770 border_color,
1771 shadow,
1772 clip,
1773 ..
1774 } => {
1775 if bg_color.is_some() || border_color.is_some() || shadow.is_some() {
1776 *flat_idx += 1;
1777 }
1778 if *clip {
1779 *flat_idx += 1;
1780 }
1781 for child in children {
1782 dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1783 }
1784 if *clip {
1785 *flat_idx += 1;
1786 }
1787 }
1788 View::ZStack { children, .. } => {
1789 for child in children {
1790 dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1791 }
1792 }
1793 View::Component(c) => {
1794 let rendered = c.render(theme);
1795 dispatch_scroll(&rendered, theme, flat, cx, cy, dx, dy, flat_idx);
1796 }
1797 View::Button { .. }
1798 | View::Rect { .. }
1799 | View::Text { .. }
1800 | View::TextInput { .. }
1801 | View::Image { .. }
1802 | View::TextArea { .. } => {
1803 *flat_idx += 1;
1804 }
1805 View::VirtualList {
1806 item_count,
1807 row_height,
1808 offset_y,
1809 viewport_height,
1810 ..
1811 } => {
1812 *flat_idx += 1; if let Some(fv) = flat.get(*flat_idx) {
1814 if let FlatViewKind::ClipStart {
1815 x,
1816 y,
1817 width,
1818 height,
1819 ..
1820 } = &fv.kind
1821 {
1822 if cx >= *x && cx <= x + width && cy >= *y && cy <= y + height {
1823 let max_scroll =
1824 ((*item_count as f32) * row_height - viewport_height).max(0.0);
1825 offset_y.set((offset_y.get() - dy).clamp(0.0, max_scroll));
1826 }
1827 }
1828 }
1829 *flat_idx += 1; let mut depth = 1usize;
1834 while *flat_idx < flat.len() && depth > 0 {
1835 match &flat[*flat_idx].kind {
1836 FlatViewKind::ClipStart { .. } => {
1837 depth += 1;
1838 *flat_idx += 1;
1839 }
1840 FlatViewKind::ClipEnd => {
1841 depth -= 1;
1842 *flat_idx += 1;
1843 }
1844 _ => {
1845 *flat_idx += 1;
1846 }
1847 }
1848 }
1849 }
1850 View::Flexible { child, .. } => {
1851 dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1852 }
1853 View::Opacity { child, .. } => {
1854 *flat_idx += 1; dispatch_scroll(child, theme, flat, cx, cy, dx, dy, flat_idx);
1856 *flat_idx += 1; }
1858 View::Spacer => {}
1859 }
1860}
1861
1862#[cfg(test)]
1863mod tests {
1864 use super::*;
1865
1866 #[test]
1867 fn delete_selection_removes_valid_byte_range_and_moves_cursor() {
1868 let mut value = "hello".to_string();
1869 let mut cursor = 4;
1870
1871 assert!(delete_selection(&mut value, &mut cursor, Some((1, 4))));
1872 assert_eq!(value, "ho");
1873 assert_eq!(cursor, 1);
1874 }
1875
1876 #[test]
1877 fn delete_selection_rejects_non_char_boundaries() {
1878 let mut value = "éx".to_string();
1879 let mut cursor = value.len();
1880
1881 assert!(!delete_selection(&mut value, &mut cursor, Some((1, 2))));
1882 assert_eq!(value, "éx");
1883 assert_eq!(cursor, 3);
1884 }
1885
1886 #[test]
1887 fn delete_selection_noop_when_none() {
1888 let mut value = "hello".to_string();
1889 let mut cursor = 3;
1890 assert!(!delete_selection(&mut value, &mut cursor, None));
1891 assert_eq!(value, "hello");
1892 assert_eq!(cursor, 3);
1893 }
1894
1895 #[test]
1898 fn normalized_selection_returns_none_when_equal() {
1899 assert_eq!(normalized_selection(3, 3), None);
1900 }
1901
1902 #[test]
1903 fn normalized_selection_orders_low_high() {
1904 assert_eq!(normalized_selection(5, 2), Some((2, 5)));
1905 assert_eq!(normalized_selection(2, 5), Some((2, 5)));
1906 }
1907
1908 #[test]
1911 fn update_selection_extend_creates_selection() {
1912 let mut edit = TextEditState::default();
1913 update_selection_for_move(&mut edit, 0, 3, true);
1914 assert_eq!(edit.selection_anchor, Some(0));
1915 assert_eq!(edit.selection, Some((0, 3)));
1916 }
1917
1918 #[test]
1919 fn update_selection_no_extend_clears_selection() {
1920 let mut edit = TextEditState {
1921 selection: Some((1, 4)),
1922 selection_anchor: Some(1),
1923 ..Default::default()
1924 };
1925 update_selection_for_move(&mut edit, 4, 5, false);
1926 assert_eq!(edit.selection, None);
1927 assert_eq!(edit.selection_anchor, None);
1928 }
1929
1930 #[test]
1931 fn update_selection_extend_preserves_anchor() {
1932 let mut edit = TextEditState {
1933 selection_anchor: Some(2),
1934 ..Default::default()
1935 };
1936 update_selection_for_move(&mut edit, 4, 6, true);
1937 assert_eq!(edit.selection_anchor, Some(2));
1938 assert_eq!(edit.selection, Some((2, 6)));
1939 }
1940
1941 fn rect_flat(x: f32, y: f32, w: f32, h: f32, corner_radius: f32) -> FlatView {
1944 use core_glyph::FlatView;
1945 let mut layout = core_glyph::TaffyLayout::default();
1946 layout.location.x = x;
1947 layout.location.y = y;
1948 layout.size.width = w;
1949 layout.size.height = h;
1950 FlatView {
1951 kind: FlatViewKind::Rect { color: core_glyph::Color::WHITE, corner_radius },
1952 layout,
1953 }
1954 }
1955
1956 #[test]
1957 fn scale_flat_scales_position_and_size() {
1958 let flat = vec![rect_flat(10.0, 20.0, 100.0, 50.0, 4.0)];
1959 let scaled = scale_flat(flat, 2.0);
1960 assert_eq!(scaled[0].layout.location.x, 20.0);
1961 assert_eq!(scaled[0].layout.location.y, 40.0);
1962 assert_eq!(scaled[0].layout.size.width, 200.0);
1963 assert_eq!(scaled[0].layout.size.height, 100.0);
1964 }
1965
1966 #[test]
1967 fn scale_flat_scales_corner_radius() {
1968 let flat = vec![rect_flat(0.0, 0.0, 50.0, 50.0, 8.0)];
1969 let scaled = scale_flat(flat, 2.0);
1970 assert!(matches!(scaled[0].kind, FlatViewKind::Rect { corner_radius, .. } if corner_radius == 16.0));
1971 }
1972
1973 #[test]
1974 fn scale_flat_identity_at_1x() {
1975 let flat = vec![rect_flat(5.0, 10.0, 80.0, 40.0, 2.0)];
1976 let scaled = scale_flat(flat, 1.0);
1977 assert_eq!(scaled[0].layout.location.x, 5.0);
1978 assert_eq!(scaled[0].layout.size.width, 80.0);
1979 }
1980
1981 fn scroll_item(x: f32, y: f32, w: f32, h: f32, max_y: f32) -> ScrollItem {
1984 ScrollItem {
1985 cx: x, cy: y, w, h,
1986 offset_x: core_glyph::Signal::new(0.0f32),
1987 offset_y: core_glyph::Signal::new(0.0f32),
1988 max_x: 0.0,
1989 max_y,
1990 enclosing: Vec::new(),
1991 }
1992 }
1993
1994 #[test]
1995 fn apply_scroll_updates_offset_when_cursor_inside() {
1996 let item = scroll_item(0.0, 0.0, 400.0, 300.0, 500.0);
1997 let items = vec![item];
1998 apply_scroll(&items, 200.0, 150.0, 0.0, -20.0);
1999 assert_eq!(items[0].offset_y.get(), 20.0);
2000 }
2001
2002 #[test]
2003 fn apply_scroll_clamps_to_max() {
2004 let item = scroll_item(0.0, 0.0, 400.0, 300.0, 100.0);
2005 let items = vec![item];
2006 apply_scroll(&items, 200.0, 150.0, 0.0, -9999.0);
2007 assert_eq!(items[0].offset_y.get(), 100.0);
2008 }
2009
2010 #[test]
2011 fn apply_scroll_clamps_to_zero() {
2012 let item = scroll_item(0.0, 0.0, 400.0, 300.0, 100.0);
2013 item.offset_y.set(50.0);
2014 let items = vec![item];
2015 apply_scroll(&items, 200.0, 150.0, 0.0, 9999.0);
2016 assert_eq!(items[0].offset_y.get(), 0.0);
2017 }
2018
2019 #[test]
2020 fn apply_scroll_ignores_cursor_outside_bounds() {
2021 let item = scroll_item(0.0, 0.0, 400.0, 300.0, 500.0);
2022 let items = vec![item];
2023 apply_scroll(&items, 500.0, 150.0, 0.0, -20.0); assert_eq!(items[0].offset_y.get(), 0.0);
2025 }
2026
2027 #[test]
2028 fn apply_scroll_innermost_container_wins() {
2029 let outer = scroll_item(0.0, 0.0, 400.0, 300.0, 500.0);
2030 let inner = scroll_item(50.0, 50.0, 200.0, 150.0, 500.0);
2031 let items = vec![outer, inner];
2033 apply_scroll(&items, 100.0, 100.0, 0.0, -20.0);
2034 assert_eq!(items[1].offset_y.get(), 20.0); assert_eq!(items[0].offset_y.get(), 0.0); }
2037}
2038
2039#[cfg(feature = "hot-reload")]
2042pub struct HotApp {
2043 loader: hot_glyph::HotLoader,
2044 theme: Theme,
2045 title: String,
2046 width: f64,
2047 height: f64,
2048 state: Option<HotAppState>,
2049}
2050
2051#[cfg(feature = "hot-reload")]
2052struct HotAppState {
2053 window: Arc<Window>,
2054 renderer: Renderer,
2055 cursor_pos: (f32, f32),
2056 frame: u32,
2057}
2058
2059#[cfg(feature = "hot-reload")]
2060impl HotApp {
2061 pub fn run(
2062 src_dir: impl AsRef<std::path::Path>,
2063 lib_path: impl AsRef<std::path::Path>,
2064 package_name: &str,
2065 theme: Theme,
2066 title: impl Into<String>,
2067 width: f64,
2068 height: f64,
2069 ) {
2070 let loader = hot_glyph::HotLoader::new(src_dir.as_ref(), lib_path.as_ref(), package_name);
2071 let event_loop = EventLoop::new().expect("event loop");
2072 event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);
2073 let mut app = HotApp {
2074 loader,
2075 theme,
2076 title: title.into(),
2077 width,
2078 height,
2079 state: None,
2080 };
2081 event_loop.run_app(&mut app).expect("event loop run");
2082 }
2083}
2084
2085#[cfg(feature = "hot-reload")]
2086impl ApplicationHandler for HotApp {
2087 fn resumed(&mut self, event_loop: &ActiveEventLoop) {
2088 if self.state.is_some() {
2089 return;
2090 }
2091 let window = Arc::new(
2092 event_loop
2093 .create_window(
2094 Window::default_attributes()
2095 .with_title(&self.title)
2096 .with_inner_size(winit::dpi::LogicalSize::new(self.width, self.height)),
2097 )
2098 .expect("window"),
2099 );
2100 let size = window.inner_size();
2101 let ctx = pollster::block_on(GpuContext::new_with_window(Arc::clone(&window)));
2102 let (surface, surface_cfg) =
2103 ctx.create_surface(Arc::clone(&window), size.width.max(1), size.height.max(1));
2104 let renderer = Renderer::new(ctx, surface, surface_cfg);
2105 self.state = Some(HotAppState {
2106 window,
2107 renderer,
2108 cursor_pos: (0.0, 0.0),
2109 frame: 0,
2110 });
2111 }
2112
2113 fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
2114 let Some(state) = &mut self.state else { return };
2115 if self.loader.poll_reload() {
2116 state.window.request_redraw();
2117 }
2118 }
2119
2120 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) {
2121 let Some(state) = &mut self.state else { return };
2122 match event {
2123 WindowEvent::CloseRequested => event_loop.exit(),
2124
2125 WindowEvent::Resized(size) => {
2126 state.renderer.resize(size.width, size.height);
2127 state.window.request_redraw();
2128 }
2129
2130 WindowEvent::CursorMoved { position, .. } => {
2131 let scale = state.window.scale_factor() as f32;
2132 let (px, py) = (position.x as f32 / scale, position.y as f32 / scale);
2133 state.cursor_pos = (px, py);
2134 let w = state.renderer.surface_cfg.width as f32 / scale;
2135 let h = state.renderer.surface_cfg.height as f32 / scale;
2136 if let Some(view) = self.loader.build_view(&self.theme) {
2137 let flat =
2138 ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2139 let mut changed = false;
2140 for fv in &flat {
2141 let l = fv.layout.location.x;
2142 let t = fv.layout.location.y;
2143 let hit = px >= l
2144 && px <= l + fv.layout.size.width
2145 && py >= t
2146 && py <= t + fv.layout.size.height;
2147 if let FlatViewKind::Button {
2148 on_hover: Some(on_hover),
2149 ..
2150 } = &fv.kind
2151 {
2152 on_hover(hit);
2153 changed = true;
2154 }
2155 }
2156 if changed {
2157 state.window.request_redraw();
2158 }
2159 }
2160 }
2161
2162 WindowEvent::MouseWheel { delta, .. } => {
2163 let (dx, dy) = match delta {
2164 MouseScrollDelta::LineDelta(x, y) => (x * 20.0, y * 20.0),
2165 MouseScrollDelta::PixelDelta(pos) => (pos.x as f32, pos.y as f32),
2166 };
2167 let (cx, cy) = state.cursor_pos;
2168 let scale = state.window.scale_factor() as f32;
2169 let w = state.renderer.surface_cfg.width as f32 / scale;
2170 let h = state.renderer.surface_cfg.height as f32 / scale;
2171 if let Some(view) = self.loader.build_view(&self.theme) {
2172 let flat =
2173 ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2174 if let Some(view2) = self.loader.build_view(&self.theme) {
2175 let mut idx = 0;
2176 dispatch_scroll(&view2, &self.theme, &flat, cx, cy, dx, dy, &mut idx);
2177 }
2178 }
2179 if needs_redraw() {
2180 clear_redraw();
2181 state.window.request_redraw();
2182 }
2183 }
2184
2185 WindowEvent::MouseInput {
2186 state: ElementState::Pressed,
2187 button: MouseButton::Left,
2188 ..
2189 } => {
2190 let (cx, cy) = state.cursor_pos;
2191 let scale = state.window.scale_factor() as f32;
2192 let w = state.renderer.surface_cfg.width as f32 / scale;
2193 let h = state.renderer.surface_cfg.height as f32 / scale;
2194 if let Some(view) = self.loader.build_view(&self.theme) {
2195 let flat =
2196 ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2197 for fv in &flat {
2198 let l = fv.layout.location.x;
2199 let t = fv.layout.location.y;
2200 let hit = cx >= l
2201 && cx <= l + fv.layout.size.width
2202 && cy >= t
2203 && cy <= t + fv.layout.size.height;
2204 match &fv.kind {
2205 FlatViewKind::Button { on_click, .. } => {
2206 if hit {
2207 on_click();
2208 }
2209 }
2210 FlatViewKind::TextInput { focused, .. } => {
2211 focused.set(hit);
2212 if hit {
2213 state.frame = 0;
2214 }
2215 }
2216 _ => {}
2217 }
2218 }
2219 }
2220 if needs_redraw() {
2221 clear_redraw();
2222 }
2223 state.window.request_redraw();
2224 }
2225
2226 WindowEvent::KeyboardInput {
2227 event:
2228 KeyEvent {
2229 logical_key,
2230 state: ElementState::Pressed,
2231 ..
2232 },
2233 ..
2234 } => {
2235 let scale = state.window.scale_factor() as f32;
2236 let w = state.renderer.surface_cfg.width as f32 / scale;
2237 let h = state.renderer.surface_cfg.height as f32 / scale;
2238 if let Some(view) = self.loader.build_view(&self.theme) {
2239 let flat =
2240 ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2241 for fv in &flat {
2242 if let FlatViewKind::TextInput {
2243 value,
2244 focused,
2245 on_submit,
2246 ..
2247 } = &fv.kind
2248 {
2249 if !focused.get() {
2250 continue;
2251 }
2252 let mut s = value.get();
2253 match &logical_key {
2254 Key::Named(NamedKey::Backspace) => {
2255 if let Some((idx, _)) = s.char_indices().next_back() {
2256 s.truncate(idx);
2257 value.set(s);
2258 }
2259 }
2260 Key::Named(NamedKey::Delete) => {
2261 value.set(String::new());
2262 }
2263 Key::Named(NamedKey::Escape) => {
2264 focused.set(false);
2265 }
2266 Key::Named(NamedKey::Tab) => {
2267 focused.set(false);
2268 }
2269 Key::Named(NamedKey::Enter) => {
2270 if let Some(f) = on_submit {
2271 f(value.get());
2272 }
2273 focused.set(false);
2274 }
2275 Key::Character(ch) => {
2276 s.push_str(ch.as_str());
2277 value.set(s);
2278 }
2279 _ => {}
2280 }
2281 }
2282 }
2283 }
2284 if needs_redraw() {
2285 clear_redraw();
2286 }
2287 state.window.request_redraw();
2288 }
2289
2290 WindowEvent::RedrawRequested => {
2291 state.frame = state.frame.wrapping_add(1);
2292 let cursor_visible = (state.frame / 30) % 2 == 0;
2293 let scale = state.window.scale_factor() as f32;
2294 let w = state.renderer.surface_cfg.width as f32 / scale;
2295 let h = state.renderer.surface_cfg.height as f32 / scale;
2296 if let Some(view) = self.loader.build_view(&self.theme) {
2297 let flat =
2298 ViewTree::build(view, &self.theme, w, h, &mut state.renderer.measurer());
2299 let any_focused = flat.iter().any(|fv| {
2300 matches!(&fv.kind, FlatViewKind::TextInput { focused, .. } if focused.get())
2301 });
2302 if any_focused {
2303 state.window.request_redraw();
2304 }
2305 let flat = scale_flat(flat, scale);
2306 state
2307 .renderer
2308 .render(&flat, cursor_visible, self.theme.background, scale);
2309 }
2310 }
2311
2312 _ => {}
2313 }
2314 }
2315}