1use std::cell::RefCell;
2use std::collections::{BTreeMap, BTreeSet};
3use std::rc::Rc;
4use std::sync::{Arc, OnceLock};
5use std::time::Instant;
6
7use gpui::{
8 AlignSelf as GpuiAlignSelf, AnyElement, App, Background, Bounds, BoxShadow, ClickEvent,
9 ContentMask, Context, CursorStyle, Div, Element, ElementId, FocusHandle, FontFallbacks,
10 FontFeatures, FontStyle, FontWeight, GlobalElementId, HighlightStyle, Img, InspectorElementId,
11 InteractiveElement, IntoElement, LayoutId, Modifiers, MouseButton, MouseDownEvent,
12 MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollHandle,
13 ScrollWheelEvent, SharedString, Stateful, StatefulInteractiveElement, Styled, StyledText,
14 TextAlign, Window, auto, div, img, linear_color_stop, linear_gradient, point, px, relative,
15 rems, rgba,
16};
17
18use crate::overlay_element::{ScriptLayerElement, ScriptOverlayElement, WindowOverlayCoordinator};
19use crate::slot_runtime::NodeSlotRuntime;
20use crate::virtual_list_element::VirtualListEntityElement;
21use crate::{
22 Align, AssetRegistry, ColorValue, CursorKind, DisplayMode, EventPropagation, EventResponse,
23 FlexDirection, FlexWrapMode, FontSlant, HitTestBehavior, ImageSourceSpec, InteractionState,
24 Justify, LayoutLength, Length, MotionKey, MotionProperty, NodeId, OverflowMode,
25 OverlayNodeSpec, PositionMode, PrimitiveRegistry, PseudoState, RadiusToken, RetainedUiTree,
26 Rgba8, ScriptCallback, SignedLength, SpacingToken, Style, StyleProperties, TextAlignMode,
27 TextDirection, UiEventHandler, UiNode, UiNodeKind, UiValue, WhiteSpaceMode,
28};
29
30type DispatchFn = dyn Fn(
31 ScriptCallback,
32 UiValue,
33 Option<crate::GeometryBounds>,
34 &mut Window,
35 &mut App,
36) -> EventResponse;
37type NativeDispatchFn = dyn Fn(
38 crate::NativeHandlerRef,
39 String,
40 UiValue,
41 Option<crate::GeometryBounds>,
42 &mut Window,
43 &mut App,
44) -> EventResponse;
45type SignalWriteFn = dyn Fn(
46 Vec<(crate::NativeSignal, crate::SignalValue)>,
47 &mut App,
48) -> Result<bool, crate::SignalError>;
49type SignalReadFn =
50 dyn Fn(&crate::NativeSignal, &App) -> Result<crate::SignalValue, crate::SignalError>;
51type ElementBoundsFn = dyn Fn(&crate::ElementRef, &App) -> Option<crate::GeometryBounds>;
52type CanvasLocalPointFn = dyn Fn(&crate::ElementRef, (f64, f64), &App) -> Option<(f64, f64)>;
53
54#[derive(Clone)]
55pub struct NodeEventDispatcher {
56 script: Rc<DispatchFn>,
57 native: Rc<NativeDispatchFn>,
58 signal_write: Rc<SignalWriteFn>,
59 signal_read: Rc<SignalReadFn>,
60 element_bounds: Rc<ElementBoundsFn>,
61 canvas_bounds: Rc<ElementBoundsFn>,
62 canvas_local_point: Rc<CanvasLocalPointFn>,
63}
64
65impl NodeEventDispatcher {
66 #[must_use]
67 pub fn new<R>(
68 dispatch: impl Fn(
69 ScriptCallback,
70 UiValue,
71 Option<crate::GeometryBounds>,
72 &mut Window,
73 &mut App,
74 ) -> R
75 + 'static,
76 ) -> Self
77 where
78 R: Into<EventResponse>,
79 {
80 Self {
81 script: Rc::new(move |callback, payload, target, window, app| {
82 dispatch(callback, payload, target, window, app).into()
83 }),
84 native: Rc::new(|_, _, _, _, _, _| EventResponse::new().stop()),
85 signal_write: Rc::new(|updates, _| {
86 updates.first().map_or(Ok(false), |(signal, _)| {
87 Err(crate::SignalError::Stale(signal.id().clone()))
88 })
89 }),
90 signal_read: Rc::new(|signal, _| Err(crate::SignalError::Stale(signal.id().clone()))),
91 element_bounds: Rc::new(|_, _| None),
92 canvas_bounds: Rc::new(|_, _| None),
93 canvas_local_point: Rc::new(|_, _, _| None),
94 }
95 }
96
97 #[must_use]
98 pub fn with_native<R>(
99 mut self,
100 dispatch: impl Fn(
101 crate::NativeHandlerRef,
102 String,
103 UiValue,
104 Option<crate::GeometryBounds>,
105 &mut Window,
106 &mut App,
107 ) -> R
108 + 'static,
109 ) -> Self
110 where
111 R: Into<EventResponse>,
112 {
113 self.native = Rc::new(move |handler, event, payload, target, window, app| {
114 dispatch(handler, event, payload, target, window, app).into()
115 });
116 self
117 }
118
119 pub(crate) fn with_signal_write(
120 mut self,
121 write: impl Fn(
122 Vec<(crate::NativeSignal, crate::SignalValue)>,
123 &mut App,
124 ) -> Result<bool, crate::SignalError>
125 + 'static,
126 ) -> Self {
127 self.signal_write = Rc::new(write);
128 self
129 }
130
131 pub(crate) fn with_signal_read(
132 mut self,
133 read: impl Fn(&crate::NativeSignal, &App) -> Result<crate::SignalValue, crate::SignalError>
134 + 'static,
135 ) -> Self {
136 self.signal_read = Rc::new(read);
137 self
138 }
139
140 pub(crate) fn with_element_bounds(
141 mut self,
142 read: impl Fn(&crate::ElementRef, &App) -> Option<crate::GeometryBounds> + 'static,
143 ) -> Self {
144 self.element_bounds = Rc::new(read);
145 self
146 }
147
148 pub(crate) fn with_canvas_local_point(
149 mut self,
150 read: impl Fn(&crate::ElementRef, (f64, f64), &App) -> Option<(f64, f64)> + 'static,
151 ) -> Self {
152 self.canvas_local_point = Rc::new(read);
153 self
154 }
155
156 pub(crate) fn with_canvas_bounds(
157 mut self,
158 read: impl Fn(&crate::ElementRef, &App) -> Option<crate::GeometryBounds> + 'static,
159 ) -> Self {
160 self.canvas_bounds = Rc::new(read);
161 self
162 }
163
164 pub(crate) fn canvas_bounds(
165 &self,
166 reference: &crate::ElementRef,
167 app: &App,
168 ) -> Option<crate::GeometryBounds> {
169 (self.canvas_bounds)(reference, app)
170 }
171
172 pub(crate) fn dispatch(
173 &self,
174 callback: ScriptCallback,
175 payload: UiValue,
176 target: Option<crate::GeometryBounds>,
177 window: &mut Window,
178 cx: &mut App,
179 ) -> EventResponse {
180 (self.script)(callback, payload, target, window, cx)
181 }
182
183 pub(crate) fn dispatch_native(
184 &self,
185 handler: crate::NativeHandlerRef,
186 event: String,
187 payload: UiValue,
188 target: Option<crate::GeometryBounds>,
189 window: &mut Window,
190 app: &mut App,
191 ) -> EventResponse {
192 (self.native)(handler, event, payload, target, window, app)
193 }
194
195 pub(crate) fn write_signal(
196 &self,
197 signal: crate::NativeSignal,
198 value: crate::SignalValue,
199 app: &mut App,
200 ) -> Result<bool, crate::SignalError> {
201 self.write_signals(vec![(signal, value)], app)
202 }
203
204 pub(crate) fn write_signals(
205 &self,
206 updates: Vec<(crate::NativeSignal, crate::SignalValue)>,
207 app: &mut App,
208 ) -> Result<bool, crate::SignalError> {
209 if updates.is_empty() {
210 return Ok(false);
211 }
212 (self.signal_write)(updates, app)
213 }
214
215 pub(crate) fn read_signal(
216 &self,
217 signal: &crate::NativeSignal,
218 app: &App,
219 ) -> Result<crate::SignalValue, crate::SignalError> {
220 (self.signal_read)(signal, app)
221 }
222
223 pub(crate) fn element_bounds(
224 &self,
225 reference: &crate::ElementRef,
226 app: &App,
227 ) -> Option<crate::GeometryBounds> {
228 (self.element_bounds)(reference, app)
229 }
230
231 pub(crate) fn canvas_local_point(
232 &self,
233 reference: &crate::ElementRef,
234 point: (f64, f64),
235 app: &App,
236 ) -> Option<(f64, f64)> {
237 (self.canvas_local_point)(reference, point, app)
238 }
239}
240
241fn dispatch_ui_event(
242 handler: &UiEventHandler,
243 event: &str,
244 payload: UiValue,
245 target: Option<crate::GeometryBounds>,
246 window: &mut Window,
247 app: &mut App,
248 script_dispatcher: Option<&NodeEventDispatcher>,
249) -> EventResponse {
250 match handler {
251 UiEventHandler::Script(callback) => script_dispatcher.map_or_else(
252 || EventResponse::new().stop(),
253 |dispatcher| dispatcher.dispatch(callback.clone(), payload, target, window, app),
254 ),
255 UiEventHandler::Host(callback) => callback.invoke(payload, window, app),
256 UiEventHandler::Native(reference) => script_dispatcher.map_or_else(
257 || EventResponse::new().stop(),
258 |dispatcher| {
259 dispatcher.dispatch_native(
260 reference.clone(),
261 event.to_owned(),
262 payload,
263 target,
264 window,
265 app,
266 )
267 },
268 ),
269 }
270}
271
272fn dispatch_ui_handlers(
273 bindings: &[crate::UiEventBinding],
274 event: &str,
275 payload: &UiValue,
276 target: Option<crate::GeometryBounds>,
277 window: &mut Window,
278 app: &mut App,
279 script_dispatcher: Option<&NodeEventDispatcher>,
280) -> EventResponse {
281 dispatch_ui_handler_phases(
282 bindings,
283 event,
284 &[crate::EventPhase::Target],
285 payload,
286 EventRoute::new(target, script_dispatcher),
287 window,
288 app,
289 )
290}
291
292#[derive(Clone, Copy)]
293struct EventRoute<'a> {
294 target: Option<crate::GeometryBounds>,
295 dispatcher: Option<&'a NodeEventDispatcher>,
296}
297
298impl<'a> EventRoute<'a> {
299 const fn new(
300 target: Option<crate::GeometryBounds>,
301 dispatcher: Option<&'a NodeEventDispatcher>,
302 ) -> Self {
303 Self { target, dispatcher }
304 }
305}
306
307fn dispatch_ui_handler_phases(
308 bindings: &[crate::UiEventBinding],
309 event: &str,
310 phases: &[crate::EventPhase],
311 payload: &UiValue,
312 route: EventRoute<'_>,
313 window: &mut Window,
314 app: &mut App,
315) -> EventResponse {
316 let mut combined = EventResponse::new();
317 for phase in phases {
318 let mut stop_route = false;
319 for binding in bindings.iter().filter(|binding| binding.phase() == *phase) {
320 let response = dispatch_ui_event(
321 binding.handler(),
322 event,
323 payload.clone(),
324 route.target,
325 window,
326 app,
327 route.dispatcher,
328 );
329 combined.merge(response);
330 if matches!(
331 response.propagation(),
332 crate::PropagationControl::StopImmediate
333 ) {
334 return combined;
335 }
336 stop_route |= matches!(response.propagation(), crate::PropagationControl::Stop);
337 }
338 if stop_route {
339 return combined;
340 }
341 }
342 combined
343}
344
345fn apply_event_response(response: EventResponse, window: &mut Window, app: &mut App) {
346 if response.default_prevented() {
347 window.prevent_default();
348 }
349 if response.stops_propagation() {
350 app.stop_propagation();
351 }
352}
353
354fn apply_pointer_response(
355 response: EventResponse,
356 node: Option<NodeId>,
357 pointer_id: u64,
358 captures: &crate::PointerCaptureRegistry,
359 window: &mut Window,
360 app: &mut App,
361) {
362 match response.pointer_capture() {
363 crate::PointerCaptureDirective::Capture => {
364 if let Some(node) = node {
365 captures.capture(pointer_id, node);
366 }
367 }
368 crate::PointerCaptureDirective::Release => {
369 captures.release(pointer_id);
370 }
371 crate::PointerCaptureDirective::None => {}
372 }
373 apply_event_response(response, window, app);
374}
375
376fn key_handler_bindings(node: &UiNode) -> BTreeMap<String, (Vec<crate::UiEventBinding>, UiValue)> {
377 if is_disabled(node) {
378 return BTreeMap::new();
379 }
380 node.handlers()
381 .iter()
382 .filter_map(|(event, bindings)| {
383 event.strip_prefix("key:").map(|key| {
384 (
385 key.to_owned(),
386 (
387 bindings.clone(),
388 node.handler_payload(event)
389 .cloned()
390 .unwrap_or(UiValue::Null),
391 ),
392 )
393 })
394 })
395 .collect()
396}
397
398fn node_scrollable(node: &UiNode) -> bool {
399 matches!(node.style().base.overflow_x, Some(OverflowMode::Scroll))
400 || matches!(node.style().base.overflow_y, Some(OverflowMode::Scroll))
401}
402
403fn scroll_handles_for_node(
404 tree: Option<&RetainedUiTree>,
405 node: Option<NodeId>,
406 handles: &BTreeMap<NodeId, ScrollHandle>,
407) -> Vec<ScrollHandle> {
408 let (Some(tree), Some(mut node)) = (tree, node) else {
409 return Vec::new();
410 };
411 let mut resolved = Vec::new();
412 while let Some(retained) = tree.node(node) {
413 if retained.scrollable()
414 && let Some(handle) = handles.get(&node)
415 {
416 resolved.push(handle.clone());
417 }
418 let Some(parent) = retained.parent() else {
419 break;
420 };
421 node = parent;
422 }
423 resolved
424}
425
426fn node_has_raw_pointer_handlers(node: &UiNode) -> bool {
427 ["pointer_down", "pointer_up", "pointer_move", "wheel"]
428 .into_iter()
429 .any(|event| !node.event_handlers(event).is_empty())
430}
431
432#[derive(Clone)]
433struct PointerPayloadContext {
434 node: Option<NodeId>,
435 geometry: crate::GeometryRegistry,
436 canvas: Option<crate::CanvasScene>,
437 scroll_handles: Vec<ScrollHandle>,
438}
439
440#[derive(Clone)]
441struct EventTargetContext {
442 node: Option<NodeId>,
443 geometry: crate::GeometryRegistry,
444}
445
446impl EventTargetContext {
447 fn new(node: Option<NodeId>, geometry: crate::GeometryRegistry) -> Self {
448 Self { node, geometry }
449 }
450
451 fn snapshot(&self) -> Option<crate::GeometryBounds> {
452 self.node
453 .and_then(|node| self.geometry.get(node))
454 .map(|geometry| geometry.visual)
455 }
456}
457
458impl PointerPayloadContext {
459 fn target_bounds(&self) -> Option<crate::GeometryBounds> {
460 self.node
461 .and_then(|node| self.geometry.get(node))
462 .map(|geometry| geometry.visual)
463 }
464
465 fn new(
466 node: &UiNode,
467 retained_id: Option<NodeId>,
468 geometry: crate::GeometryRegistry,
469 scroll_handles: Vec<ScrollHandle>,
470 ) -> Self {
471 let canvas = match node.kind() {
472 UiNodeKind::Canvas { scene } => Some(scene.clone()),
473 _ => None,
474 };
475 Self {
476 node: retained_id,
477 geometry,
478 canvas,
479 scroll_handles,
480 }
481 }
482
483 fn retained(
484 node: NodeId,
485 geometry: crate::GeometryRegistry,
486 canvas: Option<crate::CanvasScene>,
487 scroll_handles: Vec<ScrollHandle>,
488 ) -> Self {
489 Self {
490 node: Some(node),
491 geometry,
492 canvas,
493 scroll_handles,
494 }
495 }
496
497 fn enrich(&self, payload: UiValue) -> UiValue {
498 let UiValue::Map(mut payload) = payload else {
499 return payload;
500 };
501 let geometry = self.node.and_then(|node| self.geometry.get(node));
502 payload.insert(
503 "target".to_owned(),
504 geometry.map_or(UiValue::Null, |geometry| geometry.visual.into_value()),
505 );
506 let window = payload.get("window").and_then(value_point);
507 let local = geometry.and_then(|geometry| {
508 window.map(|(x, y)| (x - geometry.visual.x, y - geometry.visual.y))
509 });
510 if let Some((x, y)) = local {
511 payload.insert("local".to_owned(), logical_point_value(x, y));
512 let offset = self
513 .scroll_handles
514 .iter()
515 .map(ScrollHandle::offset)
516 .fold(point(px(0.0), px(0.0)), |total, offset| {
517 point(total.x + offset.x, total.y + offset.y)
518 });
519 payload.insert(
520 "content".to_owned(),
521 logical_point_value(x - f64::from(offset.x), y - f64::from(offset.y)),
522 );
523 payload.insert(
524 "canvas_key".to_owned(),
525 self.canvas
526 .as_ref()
527 .and_then(|scene| {
528 let geometry = self
529 .node
530 .and_then(|node| self.geometry.canvas_drawable(node))
531 .or(geometry)?;
532 let (window_x, window_y) = window?;
533 let transform = self
534 .node
535 .map(|node| self.geometry.canvas_transform(node))
536 .unwrap_or_default();
537 scene.hit_test_presented(
538 window_x - geometry.visual.x,
539 window_y - geometry.visual.y,
540 geometry.layout.width,
541 geometry.layout.height,
542 transform,
543 )
544 })
545 .map_or(UiValue::Null, |key| UiValue::String(key.to_owned())),
546 );
547 }
548 UiValue::Map(payload)
549 }
550}
551
552fn value_point(value: &UiValue) -> Option<(f64, f64)> {
553 let UiValue::Map(point) = value else {
554 return None;
555 };
556 match (point.get("x"), point.get("y")) {
557 (Some(UiValue::Float(x)), Some(UiValue::Float(y))) => Some((*x, *y)),
558 _ => None,
559 }
560}
561
562fn logical_point_value(x: f64, y: f64) -> UiValue {
563 UiValue::Map(BTreeMap::from([
564 ("x".to_owned(), UiValue::Float(x)),
565 ("y".to_owned(), UiValue::Float(y)),
566 ]))
567}
568
569fn apply_scroll_behavior(
570 mut element: Stateful<Div>,
571 node: &UiNode,
572 retained_id: Option<NodeId>,
573 handles: &BTreeMap<NodeId, ScrollHandle>,
574 anchors: &BTreeMap<NodeId, gpui::ScrollAnchor>,
575) -> Stateful<Div> {
576 let scrolls_x = matches!(node.style().base.overflow_x, Some(OverflowMode::Scroll));
577 let scrolls_y = matches!(node.style().base.overflow_y, Some(OverflowMode::Scroll));
578 if scrolls_x {
579 element = element.overflow_x_scroll();
580 }
581 if scrolls_y {
582 element = element.overflow_y_scroll();
583 }
584 if scrolls_x ^ scrolls_y {
588 element = element.restrict_scroll_to_axis();
589 }
590 if let Some(handle) = retained_id.and_then(|node| handles.get(&node)) {
591 element = element.track_scroll(handle);
592 }
593 if let Some(anchor) = retained_id.and_then(|node| anchors.get(&node)) {
594 element = element.anchor_scroll(Some(anchor.clone()));
595 }
596 element
597}
598
599fn apply_raw_pointer_handlers(
600 element: Stateful<Div>,
601 node: &UiNode,
602 dispatcher: Option<&NodeEventDispatcher>,
603 retained_id: Option<NodeId>,
604 captures: &crate::PointerCaptureRegistry,
605 geometry: &crate::GeometryRegistry,
606 scroll_handles: Vec<ScrollHandle>,
607) -> Stateful<Div> {
608 let payload = PointerPayloadContext::new(node, retained_id, geometry.clone(), scroll_handles);
609 let element =
610 apply_pointer_down_handlers(element, node, dispatcher, retained_id, captures, &payload);
611 let element =
612 apply_pointer_up_handlers(element, node, dispatcher, retained_id, captures, &payload);
613 apply_pointer_motion_handlers(
614 element,
615 node,
616 dispatcher.cloned(),
617 retained_id,
618 captures.clone(),
619 &payload,
620 )
621}
622
623fn apply_hover_handler(
624 element: Stateful<Div>,
625 hover: Option<(Vec<crate::UiEventBinding>, Option<UiValue>)>,
626 dispatcher: Option<NodeEventDispatcher>,
627 target: EventTargetContext,
628) -> Stateful<Div> {
629 element.on_hover(move |hovered, window, cx| {
630 if let Some((bindings, value)) = &hover {
631 let payload = value.as_ref().map_or_else(
632 || UiValue::Bool(*hovered),
633 |value| {
634 UiValue::Map(BTreeMap::from([
635 ("hovered".to_owned(), UiValue::Bool(*hovered)),
636 ("value".to_owned(), value.clone()),
637 ]))
638 },
639 );
640 let response = dispatch_ui_handlers(
641 bindings,
642 "hover_change",
643 &payload,
644 target.snapshot(),
645 window,
646 cx,
647 dispatcher.as_ref(),
648 );
649 apply_event_response(response, window, cx);
650 }
651 })
652}
653
654fn apply_motion_trigger_handlers(
655 mut element: Stateful<Div>,
656 node: Option<NodeId>,
657 bindings: &[crate::MotionProgressBinding],
658 geometry: &crate::GeometryRegistry,
659) -> Stateful<Div> {
660 let Some(node) = node else {
661 return element;
662 };
663 if bindings
664 .iter()
665 .any(|binding| binding.driver == crate::MotionProgressDriver::Hover)
666 {
667 let geometry = geometry.clone();
668 element = element.on_hover(move |hovered, window, _| {
669 geometry.set_motion_trigger(node, crate::MotionProgressDriver::Hover, *hovered);
670 window.request_animation_frame();
671 });
672 }
673 if bindings
674 .iter()
675 .any(|binding| binding.driver == crate::MotionProgressDriver::Press)
676 {
677 let down_geometry = geometry.clone();
678 element = element.on_any_mouse_down(move |_, window, _| {
679 down_geometry.set_motion_trigger(node, crate::MotionProgressDriver::Press, true);
680 window.request_animation_frame();
681 });
682 for button in MouseButton::all() {
683 let up_geometry = geometry.clone();
684 element = element.on_mouse_up(button, move |_, window, _| {
685 up_geometry.set_motion_trigger(node, crate::MotionProgressDriver::Press, false);
686 window.request_animation_frame();
687 });
688 }
689 }
690 element
691}
692
693fn apply_pointer_down_handlers(
694 mut element: Stateful<Div>,
695 node: &UiNode,
696 dispatcher: Option<&NodeEventDispatcher>,
697 retained_id: Option<NodeId>,
698 captures: &crate::PointerCaptureRegistry,
699 payload_context: &PointerPayloadContext,
700) -> Stateful<Div> {
701 let pointer_down = node.event_handlers("pointer_down").to_vec();
702 if pointer_down
703 .iter()
704 .any(|binding| binding.phase() == crate::EventPhase::Capture)
705 {
706 let bindings = pointer_down.clone();
707 let dispatcher = dispatcher.cloned();
708 let captures = captures.clone();
709 let payload_context = (*payload_context).clone();
710 element = element.capture_any_mouse_down(move |event, window, app| {
711 let payload = payload_context.enrich(mouse_down_payload(event));
712 let response = dispatch_ui_handler_phases(
713 &bindings,
714 "pointer_down",
715 &[crate::EventPhase::Capture],
716 &payload,
717 EventRoute::new(payload_context.target_bounds(), dispatcher.as_ref()),
718 window,
719 app,
720 );
721 apply_pointer_response(response, retained_id, 0, &captures, window, app);
722 });
723 }
724 if pointer_down.iter().any(|binding| {
725 matches!(
726 binding.phase(),
727 crate::EventPhase::Target | crate::EventPhase::Bubble
728 )
729 }) {
730 let bindings = pointer_down;
731 let dispatcher = dispatcher.cloned();
732 let captures = captures.clone();
733 let payload_context = (*payload_context).clone();
734 element = element.on_any_mouse_down(move |event, window, app| {
735 let payload = payload_context.enrich(mouse_down_payload(event));
736 let response = dispatch_ui_handler_phases(
737 &bindings,
738 "pointer_down",
739 &[crate::EventPhase::Target, crate::EventPhase::Bubble],
740 &payload,
741 EventRoute::new(payload_context.target_bounds(), dispatcher.as_ref()),
742 window,
743 app,
744 );
745 apply_pointer_response(response, retained_id, 0, &captures, window, app);
746 });
747 }
748 element
749}
750
751fn apply_pointer_up_handlers(
752 mut element: Stateful<Div>,
753 node: &UiNode,
754 dispatcher: Option<&NodeEventDispatcher>,
755 retained_id: Option<NodeId>,
756 captures: &crate::PointerCaptureRegistry,
757 payload_context: &PointerPayloadContext,
758) -> Stateful<Div> {
759 let pointer_up = node.event_handlers("pointer_up").to_vec();
760 if pointer_up
761 .iter()
762 .any(|binding| binding.phase() == crate::EventPhase::Capture)
763 {
764 let bindings = pointer_up.clone();
765 let dispatcher = dispatcher.cloned();
766 let captures = captures.clone();
767 let payload_context = (*payload_context).clone();
768 element = element.capture_any_mouse_up(move |event, window, app| {
769 let payload = payload_context.enrich(mouse_up_payload(event));
770 let response = dispatch_ui_handler_phases(
771 &bindings,
772 "pointer_up",
773 &[crate::EventPhase::Capture],
774 &payload,
775 EventRoute::new(payload_context.target_bounds(), dispatcher.as_ref()),
776 window,
777 app,
778 );
779 apply_pointer_response(response, retained_id, 0, &captures, window, app);
780 captures.release(0);
781 });
782 }
783 if pointer_up.iter().any(|binding| {
784 matches!(
785 binding.phase(),
786 crate::EventPhase::Target | crate::EventPhase::Bubble
787 )
788 }) {
789 for button in MouseButton::all() {
790 let bindings = pointer_up.clone();
791 let dispatcher = dispatcher.cloned();
792 let captures = captures.clone();
793 let payload_context = (*payload_context).clone();
794 element = element.on_mouse_up(button, move |event, window, app| {
795 let payload = payload_context.enrich(mouse_up_payload(event));
796 let response = dispatch_ui_handler_phases(
797 &bindings,
798 "pointer_up",
799 &[crate::EventPhase::Target, crate::EventPhase::Bubble],
800 &payload,
801 EventRoute::new(payload_context.target_bounds(), dispatcher.as_ref()),
802 window,
803 app,
804 );
805 apply_pointer_response(response, retained_id, 0, &captures, window, app);
806 captures.release(0);
807 });
808 }
809 }
810 element
811}
812
813fn apply_pointer_motion_handlers(
814 mut element: Stateful<Div>,
815 node: &UiNode,
816 dispatcher: Option<NodeEventDispatcher>,
817 retained_id: Option<NodeId>,
818 captures: crate::PointerCaptureRegistry,
819 payload_context: &PointerPayloadContext,
820) -> Stateful<Div> {
821 let pointer_move = node.event_handlers("pointer_move").to_vec();
822 if !pointer_move.is_empty() {
823 let dispatcher = dispatcher.clone();
824 let captures = captures.clone();
825 let payload_context = (*payload_context).clone();
826 let last_pointer = Rc::new(RefCell::new(None::<(crate::LogicalPoint, f64)>));
827 element = element.on_mouse_move(move |event, window, app| {
828 let payload = pointer_motion_payload(
829 mouse_move_payload(event),
830 logical_point(event.position),
831 &last_pointer,
832 );
833 let payload = payload_context.enrich(payload);
834 let response = dispatch_ui_handler_phases(
835 &pointer_move,
836 "pointer_move",
837 &[crate::EventPhase::Target, crate::EventPhase::Bubble],
838 &payload,
839 EventRoute::new(payload_context.target_bounds(), dispatcher.as_ref()),
840 window,
841 app,
842 );
843 apply_pointer_response(response, retained_id, 0, &captures, window, app);
844 });
845 }
846
847 let wheel = node.event_handlers("wheel").to_vec();
848 if !wheel.is_empty() {
849 let payload_context = (*payload_context).clone();
850 element = element.on_scroll_wheel(move |event, window, app| {
851 let payload = payload_context.enrich(wheel_payload(event));
852 let response = dispatch_ui_handler_phases(
853 &wheel,
854 "wheel",
855 &[crate::EventPhase::Target, crate::EventPhase::Bubble],
856 &payload,
857 EventRoute::new(payload_context.target_bounds(), dispatcher.as_ref()),
858 window,
859 app,
860 );
861 apply_pointer_response(response, retained_id, 0, &captures, window, app);
862 });
863 }
864 element
865}
866
867fn pointer_motion_payload(
868 payload: UiValue,
869 position: crate::LogicalPoint,
870 last: &Rc<RefCell<Option<(crate::LogicalPoint, f64)>>>,
871) -> UiValue {
872 let UiValue::Map(mut payload) = payload else {
873 return payload;
874 };
875 let timestamp = event_timestamp_ms();
876 let previous = last.borrow_mut().replace((position, timestamp));
877 let (movement, velocity) = previous.map_or_else(
878 || {
879 (
880 crate::LogicalPoint::default(),
881 crate::LogicalPoint::default(),
882 )
883 },
884 |(previous, previous_timestamp)| {
885 let movement = crate::LogicalPoint {
886 x: position.x - previous.x,
887 y: position.y - previous.y,
888 };
889 let elapsed = (timestamp - previous_timestamp).max(0.001) / 1_000.0;
890 (
891 movement,
892 crate::LogicalPoint {
893 x: movement.x / elapsed,
894 y: movement.y / elapsed,
895 },
896 )
897 },
898 );
899 payload.insert(
900 "movement".to_owned(),
901 logical_point_value(movement.x, movement.y),
902 );
903 payload.insert(
904 "velocity".to_owned(),
905 logical_point_value(velocity.x, velocity.y),
906 );
907 payload.insert("timestamp_ms".to_owned(), UiValue::Float(timestamp));
908 UiValue::Map(payload)
909}
910
911fn mouse_down_payload(event: &MouseDownEvent) -> UiValue {
912 pointer_payload(
913 event.position,
914 Some(event.button),
915 vec![event.button],
916 event.modifiers,
917 event.click_count,
918 false,
919 )
920}
921
922fn mouse_up_payload(event: &MouseUpEvent) -> UiValue {
923 mouse_up_payload_with_capture(event, false)
924}
925
926fn mouse_up_payload_with_capture(event: &MouseUpEvent, captured: bool) -> UiValue {
927 pointer_payload(
928 event.position,
929 Some(event.button),
930 Vec::new(),
931 event.modifiers,
932 event.click_count,
933 captured,
934 )
935}
936
937fn mouse_move_payload(event: &MouseMoveEvent) -> UiValue {
938 mouse_move_payload_with_capture(event, false)
939}
940
941fn mouse_move_payload_with_capture(event: &MouseMoveEvent, captured: bool) -> UiValue {
942 pointer_payload(
943 event.position,
944 None,
945 event.pressed_button.into_iter().collect(),
946 event.modifiers,
947 0,
948 captured,
949 )
950}
951
952fn pointer_payload(
953 position: Point<Pixels>,
954 button: Option<MouseButton>,
955 buttons: Vec<MouseButton>,
956 modifiers: Modifiers,
957 click_count: usize,
958 captured: bool,
959) -> UiValue {
960 let position = logical_point(position);
961 crate::PointerEventData {
962 pointer_id: 0,
963 pointer_type: "mouse".to_owned(),
964 window: position,
965 local: position,
966 content: position,
967 movement: crate::LogicalPoint::default(),
968 velocity: crate::LogicalPoint::default(),
969 button: button.map(mouse_button_name),
970 buttons: buttons.into_iter().map(mouse_button_name).collect(),
971 modifiers: event_modifiers(modifiers),
972 click_count,
973 timestamp_ms: event_timestamp_ms(),
974 captured,
975 target: None,
976 }
977 .into_value()
978}
979
980fn wheel_payload(event: &ScrollWheelEvent) -> UiValue {
981 let position = logical_point(event.position);
982 let delta = event.delta.pixel_delta(px(16.0));
983 crate::WheelEventData {
984 window: position,
985 local: position,
986 content: position,
987 delta: logical_point(delta),
988 precise: event.delta.precise(),
989 modifiers: event_modifiers(event.modifiers),
990 timestamp_ms: event_timestamp_ms(),
991 target: None,
992 }
993 .into_value()
994}
995
996fn logical_point(point: Point<Pixels>) -> crate::LogicalPoint {
997 crate::LogicalPoint {
998 x: f64::from(point.x),
999 y: f64::from(point.y),
1000 }
1001}
1002
1003fn event_modifiers(modifiers: Modifiers) -> crate::EventModifiers {
1004 crate::EventModifiers {
1005 control: modifiers.control,
1006 alt: modifiers.alt,
1007 shift: modifiers.shift,
1008 platform: modifiers.platform,
1009 function: modifiers.function,
1010 }
1011}
1012
1013fn mouse_button_name(button: MouseButton) -> String {
1014 match button {
1015 MouseButton::Left => "left",
1016 MouseButton::Right => "right",
1017 MouseButton::Middle => "middle",
1018 MouseButton::Navigate(gpui::NavigationDirection::Back) => "back",
1019 MouseButton::Navigate(gpui::NavigationDirection::Forward) => "forward",
1020 }
1021 .to_owned()
1022}
1023
1024fn event_timestamp_ms() -> f64 {
1025 static START: OnceLock<Instant> = OnceLock::new();
1026 START.get_or_init(Instant::now).elapsed().as_secs_f64() * 1_000.0
1027}
1028
1029#[allow(clippy::too_many_arguments)]
1030pub(crate) fn pointer_capture_router_element(
1031 child: AnyElement,
1032 view_id: &str,
1033 tree: &RetainedUiTree,
1034 dispatcher: &NodeEventDispatcher,
1035 captures: &crate::PointerCaptureRegistry,
1036 geometry: &crate::GeometryRegistry,
1037 scroll_handles: &BTreeMap<NodeId, ScrollHandle>,
1038 interactions: crate::interaction::WindowInteractionCoordinator,
1039) -> AnyElement {
1040 PointerCaptureRouterElement {
1041 child: Some(child),
1042 interactions,
1043 routes: Some(PointerCaptureRoutes {
1044 view_id: view_id.to_owned(),
1045 move_handlers: retained_handlers(tree, "pointer_move"),
1046 up_handlers: retained_handlers(tree, "pointer_up"),
1047 dispatcher: dispatcher.clone(),
1048 captures: captures.clone(),
1049 payload_contexts: tree
1050 .nodes()
1051 .map(|node| {
1052 (
1053 node.id(),
1054 PointerPayloadContext::retained(
1055 node.id(),
1056 geometry.clone(),
1057 node.canvas_scene().cloned(),
1058 scroll_handles_for_node(Some(tree), Some(node.id()), scroll_handles),
1059 ),
1060 )
1061 })
1062 .collect(),
1063 }),
1064 }
1065 .into_any_element()
1066}
1067
1068struct PointerCaptureRoutes {
1069 view_id: String,
1070 move_handlers: BTreeMap<NodeId, Vec<crate::UiEventBinding>>,
1071 up_handlers: BTreeMap<NodeId, Vec<crate::UiEventBinding>>,
1072 dispatcher: NodeEventDispatcher,
1073 captures: crate::PointerCaptureRegistry,
1074 payload_contexts: BTreeMap<NodeId, PointerPayloadContext>,
1075}
1076
1077impl PointerCaptureRoutes {
1078 fn register(self, interactions: &crate::interaction::WindowInteractionCoordinator) {
1079 let Self {
1080 view_id,
1081 move_handlers,
1082 up_handlers,
1083 dispatcher,
1084 captures,
1085 payload_contexts,
1086 } = self;
1087 let move_dispatcher = dispatcher.clone();
1088 let up_dispatcher = dispatcher;
1089 let move_captures = captures.clone();
1090 let up_captures = captures;
1091 let move_payload_contexts = payload_contexts.clone();
1092 interactions.set_pointer_routes(
1093 view_id,
1094 move |event: &MouseMoveEvent, window, app| {
1095 let Some(node) = move_captures.captured(0) else {
1096 return false;
1097 };
1098 let Some(bindings) = move_handlers.get(&node) else {
1099 return false;
1100 };
1101 let payload = move_payload_contexts.get(&node).map_or_else(
1102 || mouse_move_payload_with_capture(event, true),
1103 |context| context.enrich(mouse_move_payload_with_capture(event, true)),
1104 );
1105 let target = move_payload_contexts
1106 .get(&node)
1107 .and_then(PointerPayloadContext::target_bounds);
1108 let response = dispatch_ui_handler_phases(
1109 bindings,
1110 "pointer_move",
1111 &[crate::EventPhase::Target, crate::EventPhase::Bubble],
1112 &payload,
1113 EventRoute::new(target, Some(&move_dispatcher)),
1114 window,
1115 app,
1116 );
1117 apply_pointer_response(response, Some(node), 0, &move_captures, window, app);
1118 true
1119 },
1120 move |event: &MouseUpEvent, window, app| {
1121 let Some(node) = up_captures.captured(0) else {
1122 return false;
1123 };
1124 if let Some(bindings) = up_handlers.get(&node) {
1125 let payload = payload_contexts.get(&node).map_or_else(
1126 || mouse_up_payload_with_capture(event, true),
1127 |context| context.enrich(mouse_up_payload_with_capture(event, true)),
1128 );
1129 let target = payload_contexts
1130 .get(&node)
1131 .and_then(PointerPayloadContext::target_bounds);
1132 let response = dispatch_ui_handler_phases(
1133 bindings,
1134 "pointer_up",
1135 &[crate::EventPhase::Target, crate::EventPhase::Bubble],
1136 &payload,
1137 EventRoute::new(target, Some(&up_dispatcher)),
1138 window,
1139 app,
1140 );
1141 apply_pointer_response(response, Some(node), 0, &up_captures, window, app);
1142 }
1143 up_captures.release(0);
1144 true
1145 },
1146 );
1147 }
1148}
1149
1150struct PointerCaptureRouterElement {
1151 child: Option<AnyElement>,
1152 routes: Option<PointerCaptureRoutes>,
1153 interactions: crate::interaction::WindowInteractionCoordinator,
1154}
1155
1156impl Element for PointerCaptureRouterElement {
1157 type RequestLayoutState = AnyElement;
1158 type PrepaintState = ();
1159
1160 fn id(&self) -> Option<ElementId> {
1161 None
1162 }
1163
1164 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1165 None
1166 }
1167
1168 fn request_layout(
1169 &mut self,
1170 _id: Option<&GlobalElementId>,
1171 _inspector_id: Option<&InspectorElementId>,
1172 window: &mut Window,
1173 cx: &mut App,
1174 ) -> (LayoutId, Self::RequestLayoutState) {
1175 let mut child = self.child.take().expect("pointer router renders once");
1176 let layout = child.request_layout(window, cx);
1177 (layout, child)
1178 }
1179
1180 fn prepaint(
1181 &mut self,
1182 _id: Option<&GlobalElementId>,
1183 _inspector_id: Option<&InspectorElementId>,
1184 _bounds: Bounds<Pixels>,
1185 child: &mut Self::RequestLayoutState,
1186 window: &mut Window,
1187 cx: &mut App,
1188 ) {
1189 child.prepaint(window, cx);
1190 }
1191
1192 fn paint(
1193 &mut self,
1194 _id: Option<&GlobalElementId>,
1195 _inspector_id: Option<&InspectorElementId>,
1196 _bounds: Bounds<Pixels>,
1197 child: &mut Self::RequestLayoutState,
1198 _prepaint: &mut Self::PrepaintState,
1199 window: &mut Window,
1200 cx: &mut App,
1201 ) {
1202 self.routes
1203 .take()
1204 .expect("pointer router paints once")
1205 .register(&self.interactions);
1206 child.paint(window, cx);
1207 }
1208}
1209
1210impl IntoElement for PointerCaptureRouterElement {
1211 type Element = Self;
1212
1213 fn into_element(self) -> Self::Element {
1214 self
1215 }
1216}
1217
1218fn retained_handlers(
1219 tree: &RetainedUiTree,
1220 event: &str,
1221) -> BTreeMap<NodeId, Vec<crate::UiEventBinding>> {
1222 tree.nodes()
1223 .filter_map(|node| {
1224 let handlers = node.event_handlers(event);
1225 (!handlers.is_empty()).then(|| (node.id(), handlers.to_vec()))
1226 })
1227 .collect()
1228}
1229
1230pub trait ColorResolver {
1231 fn resolve(&self, color: &ColorValue) -> Option<Rgba8>;
1232
1233 fn color_snapshot(&self) -> BTreeMap<String, Rgba8> {
1234 crate::primitive::RUNTIME_THEME_COLOR_TOKENS
1235 .iter()
1236 .filter_map(|token| {
1237 self.resolve(&ColorValue::Token((*token).to_owned()))
1238 .map(|value| ((*token).to_owned(), value))
1239 })
1240 .collect()
1241 }
1242
1243 fn resolve_typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
1244 default_typography(role)
1245 }
1246
1247 fn resolve_length(&self, length: Length) -> Option<Length> {
1248 match length {
1249 Length::Pixels(_) | Length::Rems(_) | Length::Relative(_) => Some(length),
1250 Length::ThemeSpacing(_) | Length::ThemeRadius(_) => None,
1251 }
1252 }
1253
1254 fn resolve_motion(&self) -> crate::ThemeMotion {
1255 crate::ThemeMotion::default()
1256 }
1257}
1258
1259fn default_typography(role: &str) -> Option<crate::ResolvedTypography> {
1260 let (size, line_height, weight) = match role {
1261 "caption" => (11.0, 16.0, 400),
1262 "body_small" => (12.0, 16.0, 400),
1263 "body" => (13.0, 18.0, 400),
1264 "subtitle" => (14.0, 20.0, 400),
1265 "title" => (16.0, 22.0, 700),
1266 "heading" => (18.0, 24.0, 700),
1267 "display" => (24.0, 32.0, 700),
1268 "display_large" => (28.0, 36.0, 700),
1269 _ => return None,
1270 };
1271 Some(crate::ResolvedTypography {
1272 family: None,
1273 fallbacks: Vec::new(),
1274 size: Length::Pixels(size),
1275 line_height: Length::Pixels(line_height),
1276 weight,
1277 })
1278}
1279
1280#[derive(Clone, Copy, Debug, Default)]
1281pub struct LiteralColorResolver;
1282
1283impl ColorResolver for LiteralColorResolver {
1284 fn resolve(&self, color: &ColorValue) -> Option<Rgba8> {
1285 match color {
1286 ColorValue::Literal(color) => Some(*color),
1287 ColorValue::Token(_) => None,
1288 }
1289 }
1290}
1291
1292#[derive(Clone, Debug, Default)]
1293pub(crate) struct OwnedColorResolver {
1294 tokens: BTreeMap<String, Rgba8>,
1295 spacing: BTreeMap<SpacingToken, Length>,
1296 radii: BTreeMap<RadiusToken, Length>,
1297 typography: BTreeMap<String, crate::ResolvedTypography>,
1298 motion: crate::ThemeMotion,
1299}
1300
1301impl OwnedColorResolver {
1302 fn capture(colors: &impl ColorResolver) -> Self {
1303 let spacing = crate::primitive::RUNTIME_THEME_SPACING_TOKENS
1304 .iter()
1305 .copied()
1306 .filter_map(|token| {
1307 colors
1308 .resolve_length(Length::ThemeSpacing(token))
1309 .map(|value| (token, value))
1310 })
1311 .collect();
1312 let radii = crate::primitive::RUNTIME_THEME_RADIUS_TOKENS
1313 .iter()
1314 .copied()
1315 .filter_map(|token| {
1316 colors
1317 .resolve_length(Length::ThemeRadius(token))
1318 .map(|value| (token, value))
1319 })
1320 .collect();
1321 let typography = crate::REQUIRED_TYPOGRAPHY
1322 .iter()
1323 .filter_map(|role| {
1324 colors
1325 .resolve_typography(role)
1326 .map(|value| ((*role).to_owned(), value))
1327 })
1328 .collect();
1329 Self {
1330 tokens: colors.color_snapshot(),
1331 spacing,
1332 radii,
1333 typography,
1334 motion: colors.resolve_motion(),
1335 }
1336 }
1337}
1338
1339impl ColorResolver for OwnedColorResolver {
1340 fn resolve(&self, color: &ColorValue) -> Option<Rgba8> {
1341 match color {
1342 ColorValue::Literal(color) => Some(*color),
1343 ColorValue::Token(token) => self.tokens.get(token).copied(),
1344 }
1345 }
1346
1347 fn resolve_length(&self, length: Length) -> Option<Length> {
1348 match length {
1349 Length::ThemeSpacing(token) => self.spacing.get(&token).copied(),
1350 Length::ThemeRadius(token) => self.radii.get(&token).copied(),
1351 Length::Pixels(_) | Length::Rems(_) | Length::Relative(_) => Some(length),
1352 }
1353 }
1354
1355 fn color_snapshot(&self) -> BTreeMap<String, Rgba8> {
1356 self.tokens.clone()
1357 }
1358
1359 fn resolve_motion(&self) -> crate::ThemeMotion {
1360 self.motion.clone()
1361 }
1362
1363 fn resolve_typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
1364 self.typography.get(role).cloned()
1365 }
1366}
1367
1368#[derive(Clone, Copy, Debug, Default)]
1370pub struct GpuiNodeRenderer;
1371
1372#[derive(Clone, Copy)]
1373struct RenderEnvironment<'a, C> {
1374 now: Instant,
1375 clock: &'a crate::RuntimeClock,
1376 motion_preference: crate::MotionPreference,
1377 motion_quality: crate::MotionQuality,
1378 colors: &'a C,
1379 interaction: &'a InteractionState,
1380 primitives: &'a PrimitiveRegistry,
1381 dispatcher: Option<&'a NodeEventDispatcher>,
1382 assets: Option<&'a AssetRegistry>,
1383 overlays: &'a WindowOverlayCoordinator,
1384 interactions: &'a crate::interaction::WindowInteractionCoordinator,
1385 motions: &'a BTreeMap<MotionKey, f64>,
1386 signals: &'a crate::SignalRegistry,
1387 geometry: &'a crate::GeometryRegistry,
1388 pointer_capture: &'a crate::PointerCaptureRegistry,
1389 focus_handles: &'a BTreeMap<NodeId, FocusHandle>,
1390 scroll_handles: &'a BTreeMap<NodeId, ScrollHandle>,
1391 scroll_anchors: &'a BTreeMap<NodeId, gpui::ScrollAnchor>,
1392 virtual_requests: &'a crate::VirtualRequestRegistry,
1393 text_selection: &'a TextSelectionRegistry,
1394 host_focus: Option<&'a FocusHandle>,
1395 direction: TextDirection,
1396 locale: &'a str,
1397 number: Option<&'a crate::NumberMetadata>,
1398 ambient_text_color: Option<Rgba8>,
1399 view_id: &'a str,
1400 retained: Option<&'a RetainedUiTree>,
1401 retained_links: Option<&'a BTreeMap<NodeId, Vec<crate::RetainedChildLink>>>,
1402 semantics: Option<&'a crate::CommittedSemanticFrame>,
1403 a11y_active: bool,
1404}
1405
1406impl<C: ColorResolver> RenderEnvironment<'_, C> {
1407 fn with_resolved_text_color(&self, style: &StyleProperties) -> Self {
1408 Self {
1409 ambient_text_color: resolve_ambient_text_color(
1410 style,
1411 self.colors,
1412 self.ambient_text_color,
1413 ),
1414 ..*self
1415 }
1416 }
1417}
1418
1419pub(crate) struct WindowRenderResources<'a> {
1420 pub now: Instant,
1421 pub clock: &'a crate::RuntimeClock,
1422 pub motion_preference: crate::MotionPreference,
1423 pub motion_quality: crate::MotionQuality,
1424 pub assets: &'a AssetRegistry,
1425 pub dispatcher: &'a NodeEventDispatcher,
1426 pub overlays: &'a WindowOverlayCoordinator,
1427 pub interactions: &'a crate::interaction::WindowInteractionCoordinator,
1428 pub motions: &'a BTreeMap<MotionKey, f64>,
1429 pub signals: &'a crate::SignalRegistry,
1430 pub geometry: &'a crate::GeometryRegistry,
1431 pub pointer_capture: &'a crate::PointerCaptureRegistry,
1432 pub focus_handles: &'a BTreeMap<NodeId, FocusHandle>,
1433 pub scroll_handles: &'a BTreeMap<NodeId, ScrollHandle>,
1434 pub scroll_anchors: &'a BTreeMap<NodeId, gpui::ScrollAnchor>,
1435 pub virtual_requests: &'a crate::VirtualRequestRegistry,
1436 pub text_selection: &'a TextSelectionRegistry,
1437 pub host_focus: Option<&'a FocusHandle>,
1438 pub direction: TextDirection,
1439 pub locale: &'a str,
1440 pub number: Option<&'a crate::NumberMetadata>,
1441 pub ambient_text_color: Option<Rgba8>,
1442 pub root_path: &'a str,
1443 pub view_id: &'a str,
1444 pub semantics: &'a crate::CommittedSemanticFrame,
1445 pub a11y_active: bool,
1446}
1447
1448#[derive(Clone, Copy)]
1449pub(crate) struct RetainedSubtree<'a> {
1450 pub root: Option<NodeId>,
1451 pub links: &'a BTreeMap<NodeId, Vec<crate::RetainedChildLink>>,
1452}
1453
1454impl GpuiNodeRenderer {
1455 #[must_use]
1456 pub fn render(node: &UiNode) -> AnyElement {
1457 Self::render_with_primitives(
1458 node,
1459 &LiteralColorResolver,
1460 &InteractionState::default(),
1461 &PrimitiveRegistry::new(),
1462 )
1463 }
1464
1465 #[must_use]
1466 pub fn render_with(
1467 node: &UiNode,
1468 colors: &impl ColorResolver,
1469 interaction: &InteractionState,
1470 ) -> AnyElement {
1471 Self::render_with_primitives(node, colors, interaction, &PrimitiveRegistry::new())
1472 }
1473
1474 #[must_use]
1475 pub fn render_with_primitives(
1476 node: &UiNode,
1477 colors: &impl ColorResolver,
1478 interaction: &InteractionState,
1479 primitives: &PrimitiveRegistry,
1480 ) -> AnyElement {
1481 let overlays = WindowOverlayCoordinator::default();
1482 let interactions = crate::interaction::WindowInteractionCoordinator::default();
1483 let motions = BTreeMap::new();
1484 let signals = crate::SignalRegistry::new();
1485 let geometry = crate::GeometryRegistry::new();
1486 let pointer_capture = crate::PointerCaptureRegistry::new();
1487 let focus_handles = BTreeMap::new();
1488 let scroll_handles = BTreeMap::new();
1489 let scroll_anchors = BTreeMap::new();
1490 let virtual_requests = crate::VirtualRequestRegistry::new();
1491 let text_selection = TextSelectionRegistry::default();
1492 let environment = RenderEnvironment {
1493 now: Instant::now(),
1494 clock: &crate::RuntimeClock::default(),
1495 motion_preference: crate::MotionPreference::Normal,
1496 motion_quality: crate::MotionQuality::High,
1497 colors,
1498 interaction,
1499 primitives,
1500 dispatcher: None,
1501 assets: None,
1502 overlays: &overlays,
1503 interactions: &interactions,
1504 motions: &motions,
1505 signals: &signals,
1506 geometry: &geometry,
1507 pointer_capture: &pointer_capture,
1508 focus_handles: &focus_handles,
1509 scroll_handles: &scroll_handles,
1510 scroll_anchors: &scroll_anchors,
1511 virtual_requests: &virtual_requests,
1512 text_selection: &text_selection,
1513 host_focus: None,
1514 direction: TextDirection::LeftToRight,
1515 locale: "en",
1516 number: None,
1517 ambient_text_color: None,
1518 view_id: "standalone",
1519 retained: None,
1520 retained_links: None,
1521 semantics: None,
1522 a11y_active: false,
1523 };
1524 Self::render_internal(node, &environment, None, "root", None)
1525 }
1526
1527 #[must_use]
1528 pub fn render_retained_with_primitives(
1529 tree: &RetainedUiTree,
1530 colors: &impl ColorResolver,
1531 interaction: &InteractionState,
1532 primitives: &PrimitiveRegistry,
1533 ) -> AnyElement {
1534 match crate::CommittedSemanticFrame::from_retained(tree) {
1535 Ok(semantics) => Self::render_retained_with_committed_semantics(
1536 tree,
1537 &semantics,
1538 false,
1539 colors,
1540 interaction,
1541 primitives,
1542 ),
1543 Err(error) => div()
1544 .child(format!("Invalid accessibility semantics: {error}"))
1545 .into_any_element(),
1546 }
1547 }
1548
1549 fn render_retained_with_committed_semantics(
1550 tree: &RetainedUiTree,
1551 semantics: &crate::CommittedSemanticFrame,
1552 a11y_active: bool,
1553 colors: &impl ColorResolver,
1554 interaction: &InteractionState,
1555 primitives: &PrimitiveRegistry,
1556 ) -> AnyElement {
1557 let overlays = WindowOverlayCoordinator::default();
1558 let interactions = crate::interaction::WindowInteractionCoordinator::default();
1559 let motions = BTreeMap::new();
1560 let signals = crate::SignalRegistry::new();
1561 let geometry = crate::GeometryRegistry::new();
1562 let pointer_capture = crate::PointerCaptureRegistry::new();
1563 let focus_handles = BTreeMap::new();
1564 let scroll_handles = BTreeMap::new();
1565 let scroll_anchors = BTreeMap::new();
1566 let virtual_requests = crate::VirtualRequestRegistry::new();
1567 let text_selection = TextSelectionRegistry::default();
1568 let environment = RenderEnvironment {
1569 now: Instant::now(),
1570 clock: &crate::RuntimeClock::default(),
1571 motion_preference: crate::MotionPreference::Normal,
1572 motion_quality: crate::MotionQuality::High,
1573 colors,
1574 interaction,
1575 primitives,
1576 dispatcher: None,
1577 assets: None,
1578 overlays: &overlays,
1579 interactions: &interactions,
1580 motions: &motions,
1581 signals: &signals,
1582 geometry: &geometry,
1583 pointer_capture: &pointer_capture,
1584 focus_handles: &focus_handles,
1585 scroll_handles: &scroll_handles,
1586 scroll_anchors: &scroll_anchors,
1587 virtual_requests: &virtual_requests,
1588 text_selection: &text_selection,
1589 host_focus: None,
1590 direction: TextDirection::LeftToRight,
1591 locale: "en",
1592 number: None,
1593 ambient_text_color: None,
1594 view_id: "standalone",
1595 retained: Some(tree),
1596 retained_links: None,
1597 semantics: Some(semantics),
1598 a11y_active,
1599 };
1600 tree.root().map_or_else(
1601 || {
1602 div()
1603 .child("Retained UI tree has no root")
1604 .into_any_element()
1605 },
1606 |root| Self::render_internal(root, &environment, None, "root", tree.root_id()),
1607 )
1608 }
1609
1610 #[must_use]
1611 pub fn render_retained_with_dispatcher(
1612 tree: &RetainedUiTree,
1613 colors: &impl ColorResolver,
1614 interaction: &InteractionState,
1615 primitives: &PrimitiveRegistry,
1616 dispatcher: &NodeEventDispatcher,
1617 ) -> AnyElement {
1618 let semantics = match crate::CommittedSemanticFrame::from_retained(tree) {
1619 Ok(semantics) => semantics,
1620 Err(error) => {
1621 return div()
1622 .child(format!("Invalid accessibility semantics: {error}"))
1623 .into_any_element();
1624 }
1625 };
1626 let overlays = WindowOverlayCoordinator::default();
1627 let interactions = crate::interaction::WindowInteractionCoordinator::default();
1628 let motions = BTreeMap::new();
1629 let signals = crate::SignalRegistry::new();
1630 let geometry = crate::GeometryRegistry::new();
1631 let pointer_capture = crate::PointerCaptureRegistry::new();
1632 let focus_handles = BTreeMap::new();
1633 let scroll_handles = BTreeMap::new();
1634 let scroll_anchors = BTreeMap::new();
1635 let virtual_requests = crate::VirtualRequestRegistry::new();
1636 let text_selection = TextSelectionRegistry::default();
1637 let environment = RenderEnvironment {
1638 now: Instant::now(),
1639 clock: &crate::RuntimeClock::default(),
1640 motion_preference: crate::MotionPreference::Normal,
1641 motion_quality: crate::MotionQuality::High,
1642 colors,
1643 interaction,
1644 primitives,
1645 dispatcher: Some(dispatcher),
1646 assets: None,
1647 overlays: &overlays,
1648 interactions: &interactions,
1649 motions: &motions,
1650 signals: &signals,
1651 geometry: &geometry,
1652 pointer_capture: &pointer_capture,
1653 focus_handles: &focus_handles,
1654 scroll_handles: &scroll_handles,
1655 scroll_anchors: &scroll_anchors,
1656 virtual_requests: &virtual_requests,
1657 text_selection: &text_selection,
1658 host_focus: None,
1659 direction: TextDirection::LeftToRight,
1660 locale: "en",
1661 number: None,
1662 ambient_text_color: None,
1663 view_id: "standalone",
1664 retained: Some(tree),
1665 retained_links: None,
1666 semantics: Some(&semantics),
1667 a11y_active: false,
1668 };
1669 tree.root().map_or_else(
1670 || {
1671 div()
1672 .child("Retained UI tree has no root")
1673 .into_any_element()
1674 },
1675 |root| Self::render_internal(root, &environment, None, "root", tree.root_id()),
1676 )
1677 }
1678
1679 #[must_use]
1680 pub fn render_with_dispatcher(
1681 node: &UiNode,
1682 colors: &impl ColorResolver,
1683 interaction: &InteractionState,
1684 primitives: &PrimitiveRegistry,
1685 dispatcher: &NodeEventDispatcher,
1686 ) -> AnyElement {
1687 let overlays = WindowOverlayCoordinator::default();
1688 let interactions = crate::interaction::WindowInteractionCoordinator::default();
1689 let motions = BTreeMap::new();
1690 let signals = crate::SignalRegistry::new();
1691 let geometry = crate::GeometryRegistry::new();
1692 let pointer_capture = crate::PointerCaptureRegistry::new();
1693 let focus_handles = BTreeMap::new();
1694 let scroll_handles = BTreeMap::new();
1695 let scroll_anchors = BTreeMap::new();
1696 let virtual_requests = crate::VirtualRequestRegistry::new();
1697 let text_selection = TextSelectionRegistry::default();
1698 let environment = RenderEnvironment {
1699 now: Instant::now(),
1700 clock: &crate::RuntimeClock::default(),
1701 motion_preference: crate::MotionPreference::Normal,
1702 motion_quality: crate::MotionQuality::High,
1703 colors,
1704 interaction,
1705 primitives,
1706 dispatcher: Some(dispatcher),
1707 assets: None,
1708 overlays: &overlays,
1709 interactions: &interactions,
1710 motions: &motions,
1711 signals: &signals,
1712 geometry: &geometry,
1713 pointer_capture: &pointer_capture,
1714 focus_handles: &focus_handles,
1715 scroll_handles: &scroll_handles,
1716 scroll_anchors: &scroll_anchors,
1717 virtual_requests: &virtual_requests,
1718 text_selection: &text_selection,
1719 host_focus: None,
1720 direction: TextDirection::LeftToRight,
1721 locale: "en",
1722 number: None,
1723 ambient_text_color: None,
1724 view_id: "standalone",
1725 retained: None,
1726 retained_links: None,
1727 semantics: None,
1728 a11y_active: false,
1729 };
1730 Self::render_internal(node, &environment, None, "root", None)
1731 }
1732
1733 #[must_use]
1734 pub fn render_with_runtime(
1735 node: &UiNode,
1736 colors: &impl ColorResolver,
1737 interaction: &InteractionState,
1738 primitives: &PrimitiveRegistry,
1739 assets: &AssetRegistry,
1740 dispatcher: &NodeEventDispatcher,
1741 ) -> AnyElement {
1742 let overlays = WindowOverlayCoordinator::default();
1743 let interactions = crate::interaction::WindowInteractionCoordinator::default();
1744 let motions = BTreeMap::new();
1745 let signals = crate::SignalRegistry::new();
1746 let geometry = crate::GeometryRegistry::new();
1747 let pointer_capture = crate::PointerCaptureRegistry::new();
1748 let focus_handles = BTreeMap::new();
1749 let scroll_handles = BTreeMap::new();
1750 let scroll_anchors = BTreeMap::new();
1751 let virtual_requests = crate::VirtualRequestRegistry::new();
1752 let text_selection = TextSelectionRegistry::default();
1753 let resources = WindowRenderResources {
1754 now: Instant::now(),
1755 clock: &crate::RuntimeClock::default(),
1756 motion_preference: crate::MotionPreference::Normal,
1757 motion_quality: crate::MotionQuality::High,
1758 assets,
1759 dispatcher,
1760 overlays: &overlays,
1761 interactions: &interactions,
1762 motions: &motions,
1763 signals: &signals,
1764 geometry: &geometry,
1765 pointer_capture: &pointer_capture,
1766 focus_handles: &focus_handles,
1767 scroll_handles: &scroll_handles,
1768 scroll_anchors: &scroll_anchors,
1769 virtual_requests: &virtual_requests,
1770 text_selection: &text_selection,
1771 host_focus: None,
1772 direction: TextDirection::LeftToRight,
1773 locale: "en",
1774 number: None,
1775 ambient_text_color: None,
1776 root_path: "root",
1777 view_id: "standalone",
1778 semantics: &crate::CommittedSemanticFrame::default(),
1779 a11y_active: false,
1780 };
1781 Self::render_with_window_runtime(node, colors, interaction, primitives, &resources)
1782 }
1783
1784 pub(crate) fn render_with_window_runtime(
1785 node: &UiNode,
1786 colors: &impl ColorResolver,
1787 interaction: &InteractionState,
1788 primitives: &PrimitiveRegistry,
1789 resources: &WindowRenderResources<'_>,
1790 ) -> AnyElement {
1791 Self::render_subtree_with_window_runtime(
1792 node,
1793 colors,
1794 interaction,
1795 primitives,
1796 resources,
1797 resources.root_path,
1798 )
1799 }
1800
1801 pub(crate) fn render_retained_with_window_runtime(
1802 tree: &RetainedUiTree,
1803 colors: &impl ColorResolver,
1804 interaction: &InteractionState,
1805 primitives: &PrimitiveRegistry,
1806 resources: &WindowRenderResources<'_>,
1807 ) -> AnyElement {
1808 let environment = RenderEnvironment {
1809 now: resources.now,
1810 clock: resources.clock,
1811 motion_preference: resources.motion_preference,
1812 motion_quality: resources.motion_quality,
1813 colors,
1814 interaction,
1815 primitives,
1816 dispatcher: Some(resources.dispatcher),
1817 assets: Some(resources.assets),
1818 overlays: resources.overlays,
1819 interactions: resources.interactions,
1820 motions: resources.motions,
1821 signals: resources.signals,
1822 geometry: resources.geometry,
1823 pointer_capture: resources.pointer_capture,
1824 focus_handles: resources.focus_handles,
1825 scroll_handles: resources.scroll_handles,
1826 scroll_anchors: resources.scroll_anchors,
1827 virtual_requests: resources.virtual_requests,
1828 text_selection: resources.text_selection,
1829 host_focus: resources.host_focus,
1830 direction: resources.direction,
1831 locale: resources.locale,
1832 number: resources.number,
1833 ambient_text_color: resources.ambient_text_color,
1834 view_id: resources.view_id,
1835 retained: Some(tree),
1836 retained_links: None,
1837 semantics: Some(resources.semantics),
1838 a11y_active: resources.a11y_active,
1839 };
1840 tree.root().map_or_else(
1841 || {
1842 div()
1843 .child("Retained UI tree has no root")
1844 .into_any_element()
1845 },
1846 |root| {
1847 Self::render_internal(
1848 root,
1849 &environment,
1850 None,
1851 resources.root_path,
1852 tree.root_id(),
1853 )
1854 },
1855 )
1856 }
1857
1858 pub(crate) fn render_subtree_with_window_runtime(
1859 node: &UiNode,
1860 colors: &impl ColorResolver,
1861 interaction: &InteractionState,
1862 primitives: &PrimitiveRegistry,
1863 resources: &WindowRenderResources<'_>,
1864 path: &str,
1865 ) -> AnyElement {
1866 let environment = RenderEnvironment {
1867 now: resources.now,
1868 clock: resources.clock,
1869 motion_preference: resources.motion_preference,
1870 motion_quality: resources.motion_quality,
1871 colors,
1872 interaction,
1873 primitives,
1874 dispatcher: Some(resources.dispatcher),
1875 assets: Some(resources.assets),
1876 overlays: resources.overlays,
1877 interactions: resources.interactions,
1878 motions: resources.motions,
1879 signals: resources.signals,
1880 geometry: resources.geometry,
1881 pointer_capture: resources.pointer_capture,
1882 focus_handles: resources.focus_handles,
1883 scroll_handles: resources.scroll_handles,
1884 scroll_anchors: resources.scroll_anchors,
1885 virtual_requests: resources.virtual_requests,
1886 text_selection: resources.text_selection,
1887 host_focus: resources.host_focus,
1888 direction: resources.direction,
1889 locale: resources.locale,
1890 number: resources.number,
1891 ambient_text_color: resources.ambient_text_color,
1892 view_id: resources.view_id,
1893 retained: None,
1894 retained_links: None,
1895 semantics: Some(resources.semantics),
1896 a11y_active: resources.a11y_active,
1897 };
1898 Self::render_internal(node, &environment, None, path, None)
1899 }
1900
1901 pub(crate) fn render_subtree_with_window_runtime_at(
1902 node: &UiNode,
1903 colors: &impl ColorResolver,
1904 interaction: &InteractionState,
1905 primitives: &PrimitiveRegistry,
1906 resources: &WindowRenderResources<'_>,
1907 path: &str,
1908 retained: RetainedSubtree<'_>,
1909 ) -> AnyElement {
1910 let environment = RenderEnvironment {
1911 now: resources.now,
1912 clock: resources.clock,
1913 motion_preference: resources.motion_preference,
1914 motion_quality: resources.motion_quality,
1915 colors,
1916 interaction,
1917 primitives,
1918 dispatcher: Some(resources.dispatcher),
1919 assets: Some(resources.assets),
1920 overlays: resources.overlays,
1921 interactions: resources.interactions,
1922 motions: resources.motions,
1923 signals: resources.signals,
1924 geometry: resources.geometry,
1925 pointer_capture: resources.pointer_capture,
1926 focus_handles: resources.focus_handles,
1927 scroll_handles: resources.scroll_handles,
1928 scroll_anchors: resources.scroll_anchors,
1929 virtual_requests: resources.virtual_requests,
1930 text_selection: resources.text_selection,
1931 host_focus: resources.host_focus,
1932 direction: resources.direction,
1933 locale: resources.locale,
1934 number: resources.number,
1935 ambient_text_color: resources.ambient_text_color,
1936 view_id: resources.view_id,
1937 retained: None,
1938 retained_links: Some(retained.links),
1939 semantics: Some(resources.semantics),
1940 a11y_active: resources.a11y_active,
1941 };
1942 Self::render_internal(node, &environment, None, path, retained.root)
1943 }
1944
1945 fn render_internal<C: ColorResolver>(
1946 node: &UiNode,
1947 environment: &RenderEnvironment<'_, C>,
1948 boundary_fallback: Option<&UiNode>,
1949 path: &str,
1950 retained_id: Option<NodeId>,
1951 ) -> AnyElement {
1952 if let Some(table) = render_table_layout(node, environment, path, retained_id) {
1953 return table;
1954 }
1955 let local_interaction = if is_disabled(node) {
1956 environment.interaction.clone().with(PseudoState::Disabled)
1957 } else {
1958 environment.interaction.clone()
1959 };
1960 let motion_path = retained_id.map_or_else(
1961 || path.to_owned(),
1962 |node| crate::motion::retained_node_path(path, node),
1963 );
1964 let mut animation = node_motion(environment.motions, &motion_path);
1965 if let Some(retained_id) = retained_id {
1966 for binding in node.progress_motions() {
1967 if let Some(value) = environment
1968 .geometry
1969 .motion_progress(retained_id, binding.property())
1970 {
1971 animation.set(binding.property(), value);
1972 }
1973 }
1974 }
1975 let signals = node_signals(environment.signals, node);
1976 if matches!(node.kind(), UiNodeKind::Canvas { .. }) {
1977 animation = apply_canvas_signal_transform(animation, &signals);
1978 if let Some(retained_id) = retained_id {
1979 environment
1980 .geometry
1981 .update_canvas_transform(retained_id, animation.canvas_transform());
1982 }
1983 }
1984 let mut resolved_style = node.style().resolve(&local_interaction);
1985 apply_motion_dimensions(&mut resolved_style, animation);
1986 apply_signal_style(&mut resolved_style, &signals);
1987 normalize_text_content_layout(node, &mut resolved_style);
1988 let local_environment = environment.with_resolved_text_color(&resolved_style);
1989 let mut element = apply_style(
1990 div(),
1991 &resolved_style,
1992 environment.colors,
1993 environment.direction,
1994 );
1995 if matches!(
1996 node.kind(),
1997 UiNodeKind::VirtualCollection { spec } if spec.height.is_none()
1998 ) {
1999 element = element.flex_1().min_h(px(0.0));
2000 }
2001 let focus_handle =
2002 retained_id.and_then(|node| environment.focus_handles.get(&node).cloned());
2003 element =
2004 apply_node_focus_tracking(element, node, focus_handle.as_ref(), environment.primitives);
2005 if let Some(opacity) = signals.opacity.or(animation.opacity) {
2006 element = element.opacity(f64_to_f32(opacity.clamp(0.0, 1.0)));
2007 }
2008 if animation.clip_height.is_some() || resolved_style.clip == Some(true) {
2009 element = element.overflow_hidden();
2010 }
2011 let populated = Self::populate_with_interactions(
2012 element,
2013 node,
2014 &local_environment,
2015 boundary_fallback,
2016 path,
2017 retained_id,
2018 animation,
2019 );
2020 let translate_x = signals
2021 .translate_x
2022 .or(animation.translate_x)
2023 .or(resolved_style.translate_x);
2024 let translate_y = signals
2025 .translate_y
2026 .or(animation.translate_y)
2027 .or(resolved_style.translate_y);
2028 let element = translated(populated, translate_x, translate_y);
2029 let layout_motion = layout_motion_spec(node, environment.now);
2030 let progress_motions = node.progress_motions().to_vec();
2031 match retained_id {
2032 Some(node) => GeometryTrackedElement {
2033 child: Some(element),
2034 node,
2035 registry: environment.geometry.clone(),
2036 translate_x: translate_x.unwrap_or(0.0),
2037 translate_y: translate_y.unwrap_or(0.0),
2038 layout_motion,
2039 progress_motions,
2040 scroll_handles: scroll_handles_for_node(
2041 environment.retained,
2042 retained_id,
2043 environment.scroll_handles,
2044 ),
2045 focus_handle,
2046 now: environment.now,
2047 motion_preference: environment.motion_preference,
2048 }
2049 .into_any_element(),
2050 None => element,
2051 }
2052 }
2053
2054 #[allow(clippy::too_many_lines)]
2055 fn populate_with_interactions<C: ColorResolver>(
2056 element: Div,
2057 node: &UiNode,
2058 environment: &RenderEnvironment<'_, C>,
2059 boundary_fallback: Option<&UiNode>,
2060 path: &str,
2061 retained_id: Option<NodeId>,
2062 motion: NodeMotionValues,
2063 ) -> AnyElement {
2064 let click = (!is_disabled(node) && !node.event_handlers("click").is_empty()).then(|| {
2065 (
2066 node.event_handlers("click").to_vec(),
2067 node.handler_payload("click")
2068 .cloned()
2069 .unwrap_or(UiValue::Null),
2070 )
2071 });
2072 let hover = (!node.event_handlers("hover_change").is_empty()).then(|| {
2073 (
2074 node.event_handlers("hover_change").to_vec(),
2075 node.handler_payload("hover_change").cloned(),
2076 )
2077 });
2078 let key_handlers = key_handler_bindings(node);
2079 let hit_test = resolved_hit_test(node, environment);
2080 let semantic = environment
2081 .a11y_active
2082 .then(|| {
2083 retained_id.and_then(|id| environment.semantics.and_then(|frame| frame.node(id)))
2084 })
2085 .flatten()
2086 .filter(|semantic| {
2087 crate::accessibility::native_role(&semantic.role)
2088 .ok()
2089 .flatten()
2090 .is_some()
2091 })
2092 .cloned();
2093 let needs = u8::from(click.is_some())
2094 | u8::from(hover.is_some()) << 1
2095 | u8::from(!key_handlers.is_empty()) << 2
2096 | u8::from(hit_test.is_some()) << 3
2097 | u8::from(node.progress_motions().iter().any(|binding| {
2098 matches!(
2099 binding.driver,
2100 crate::MotionProgressDriver::Hover
2101 | crate::MotionProgressDriver::Press
2102 | crate::MotionProgressDriver::Focus
2103 )
2104 })) << 4
2105 | u8::from(semantic.is_some()) << 5;
2106 if !node_needs_interaction_wrapper(node, needs) {
2107 return Self::populate(
2108 element,
2109 node,
2110 environment,
2111 boundary_fallback,
2112 path,
2113 retained_id,
2114 motion,
2115 );
2116 }
2117
2118 let click_dispatcher = environment.dispatcher.cloned();
2119 let hover_dispatcher = environment.dispatcher.cloned();
2120 let keyboard_dispatcher = environment.dispatcher.cloned();
2121 let event_target = EventTargetContext::new(retained_id, environment.geometry.clone());
2122 let click_target = event_target.clone();
2123 let hover_target = event_target.clone();
2124 let keyboard_target = event_target;
2125 let keyboard_click = click.clone();
2126 let text_direction = environment.direction;
2127 let stable_id = interaction_element_id(retained_id, path);
2128 let debug_path = path.to_owned();
2129 let element = apply_pseudo_backgrounds(
2130 element
2131 .id(SharedString::from(stable_id))
2132 .debug_selector(move || debug_path.clone()),
2133 node.style(),
2134 environment.colors,
2135 );
2136 let element = apply_native_semantics(element, semantic.as_ref());
2137 let element = apply_primitive_accessibility_actions(
2138 element,
2139 node,
2140 retained_id,
2141 semantic.as_ref(),
2142 environment,
2143 );
2144 let element = apply_hit_test(element, hit_test);
2145 let element =
2146 apply_tab_behavior(element, node, click.is_some() || !key_handlers.is_empty());
2147 let element = apply_environment_scroll(element, node, retained_id, environment);
2148 let element = apply_motion_trigger_handlers(
2149 element,
2150 retained_id,
2151 node.progress_motions(),
2152 environment.geometry,
2153 );
2154 let element = if let Some((bindings, payload)) = click {
2155 element.on_click(move |event, window, cx| {
2156 if !matches!(event, ClickEvent::Mouse(_)) {
2157 return;
2158 }
2159 let response = dispatch_ui_handlers(
2160 &bindings,
2161 "click",
2162 &payload,
2163 click_target.snapshot(),
2164 window,
2165 cx,
2166 click_dispatcher.as_ref(),
2167 );
2168 apply_event_response(response, window, cx);
2169 })
2170 } else {
2171 element
2172 };
2173 let element = apply_hover_handler(element, hover, hover_dispatcher, hover_target);
2174 let element = if key_handlers.values().any(|(bindings, _)| {
2175 bindings
2176 .iter()
2177 .any(|binding| binding.phase() == crate::EventPhase::Capture)
2178 }) {
2179 let handlers = key_handlers.clone();
2180 let dispatcher = keyboard_dispatcher.clone();
2181 let target = keyboard_target.clone();
2182 element.capture_key_down(move |event, window, cx| {
2183 let key = logical_keyboard_key(event.keystroke.key.as_str(), text_direction);
2184 if let Some((bindings, payload)) = handlers.get(key) {
2185 let response = dispatch_ui_handler_phases(
2186 bindings,
2187 "key",
2188 &[crate::EventPhase::Capture],
2189 payload,
2190 EventRoute::new(target.snapshot(), dispatcher.as_ref()),
2191 window,
2192 cx,
2193 );
2194 apply_event_response(response, window, cx);
2195 }
2196 })
2197 } else {
2198 element
2199 };
2200 let element = element.on_key_down(move |event, window, cx| {
2201 let semantic_key = logical_keyboard_key(event.keystroke.key.as_str(), text_direction);
2202 let explicit = key_handlers.get(semantic_key);
2203 let semantic = explicit.or_else(|| {
2204 matches!(event.keystroke.key.as_str(), "enter" | "space")
2205 .then_some(())
2206 .and(keyboard_click.as_ref())
2207 });
2208 if let Some((bindings, payload)) = semantic {
2209 let phases = if explicit.is_some() {
2210 &[crate::EventPhase::Target, crate::EventPhase::Bubble][..]
2211 } else {
2212 &[crate::EventPhase::Target][..]
2215 };
2216 let response = dispatch_ui_handler_phases(
2217 bindings,
2218 "key",
2219 phases,
2220 payload,
2221 EventRoute::new(keyboard_target.snapshot(), keyboard_dispatcher.as_ref()),
2222 window,
2223 cx,
2224 );
2225 apply_event_response(response, window, cx);
2226 }
2227 });
2228 let element = apply_environment_raw_pointer(element, node, retained_id, environment);
2229 Self::populate(
2230 element,
2231 node,
2232 environment,
2233 boundary_fallback,
2234 path,
2235 retained_id,
2236 motion,
2237 )
2238 }
2239
2240 #[allow(clippy::too_many_lines)]
2241 fn populate<C: ColorResolver>(
2242 element: impl ParentElement + IntoElement,
2243 node: &UiNode,
2244 environment: &RenderEnvironment<'_, C>,
2245 boundary_fallback: Option<&UiNode>,
2246 path: &str,
2247 retained_id: Option<NodeId>,
2248 motion: NodeMotionValues,
2249 ) -> AnyElement {
2250 match node.kind() {
2251 UiNodeKind::Text { text } => {
2252 render_text_node(element, node, text.as_str(), environment, path, retained_id)
2253 }
2254 UiNodeKind::RichText { text, spans } => element
2255 .child(styled_text(
2256 text.as_str(),
2257 spans,
2258 environment.colors,
2259 environment.motions,
2260 &retained_id.map_or_else(
2261 || path.to_owned(),
2262 |node| crate::motion::retained_node_path(path, node),
2263 ),
2264 ))
2265 .into_any_element(),
2266 UiNodeKind::Canvas { scene } => render_canvas(
2267 element,
2268 scene,
2269 environment.colors,
2270 motion,
2271 retained_id,
2272 environment.geometry,
2273 ),
2274 UiNodeKind::Svg { source } => render_inline_svg(element, node, source, environment),
2275 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
2276 let children = render_flattened_children(
2277 children,
2278 environment,
2279 boundary_fallback,
2280 path,
2281 retained_id,
2282 );
2283 let element = if let Some(crate::table_layout::TableLayout::Resolved { extent }) =
2284 node.table_layout()
2285 {
2286 let mut track = div()
2287 .flex()
2288 .flex_col()
2289 .w(px(f64_to_f32(*extent)))
2290 .min_w(px(f64_to_f32(*extent)))
2291 .flex_shrink_0();
2292 if node.style().base.flex_grow == Some(true) {
2293 track = track.flex_1().min_h(px(0.0));
2294 }
2295 element.child(track.children(children))
2296 } else {
2297 element.children(children)
2298 };
2299 decorate_scrollbars(element, node, environment, path, retained_id)
2300 .into_any_element()
2301 }
2302 UiNodeKind::Custom { primitive } => element
2303 .child(
2304 environment.primitives.element(
2305 primitive.clone(),
2306 retained_id.zip(node.key().map(|key| key.as_str().to_owned())),
2307 environment
2308 .primitives
2309 .uses_primary_focus(&primitive.primitive)
2310 .then(|| {
2311 primitive_focus_owner(
2312 environment.retained,
2313 retained_id,
2314 environment.focus_handles,
2315 )
2316 })
2317 .flatten(),
2318 boundary_fallback.cloned(),
2319 crate::primitive::PrimitiveWindowContext::new(
2320 environment.dispatcher.cloned(),
2321 environment.interactions.clone(),
2322 scroll_handles_for_node(
2323 environment.retained,
2324 retained_id,
2325 environment.scroll_handles,
2326 ),
2327 environment.view_id,
2328 ),
2329 crate::PrimitiveTheme::capture_with_environment(
2330 environment.colors,
2331 environment.direction,
2332 environment.locale,
2333 environment.number,
2334 environment.clock.clone(),
2335 environment.motion_preference,
2336 environment.motion_quality,
2337 ),
2338 ),
2339 )
2340 .into_any_element(),
2341 UiNodeKind::Image { source } => render_image(element, source, environment),
2342 UiNodeKind::DirectionalImage {
2343 left_to_right,
2344 right_to_left,
2345 } => {
2346 let source = match environment.direction {
2347 TextDirection::LeftToRight => left_to_right,
2348 TextDirection::RightToLeft => right_to_left,
2349 };
2350 render_image(element, source, environment)
2351 }
2352 UiNodeKind::Overlay {
2353 trigger,
2354 content,
2355 spec,
2356 } => element
2357 .child(native_overlay_element(
2358 node,
2359 trigger,
2360 content,
2361 spec,
2362 environment,
2363 boundary_fallback,
2364 (path, retained_id),
2365 ))
2366 .into_any_element(),
2367 UiNodeKind::Layer { content, spec } => render_layer_node(
2368 element,
2369 content,
2370 spec,
2371 environment,
2372 boundary_fallback,
2373 path,
2374 retained_id,
2375 ),
2376 UiNodeKind::VirtualCollection { spec } => {
2377 render_virtual_collection(element, spec, environment, path, retained_id)
2378 }
2379 UiNodeKind::ErrorBoundary { child, fallback } => element
2380 .child(Self::render_internal(
2381 child,
2382 environment,
2383 Some(fallback),
2384 &format!("{path}/boundary"),
2385 retained_child_id(
2386 environment.retained,
2387 environment.retained_links,
2388 retained_id,
2389 "child",
2390 0,
2391 ),
2392 ))
2393 .into_any_element(),
2394 }
2395 }
2396}
2397
2398fn apply_node_focus_tracking(
2399 element: Div,
2400 node: &UiNode,
2401 handle: Option<&FocusHandle>,
2402 primitives: &PrimitiveRegistry,
2403) -> Div {
2404 let Some(handle) = handle else {
2405 return element;
2406 };
2407 if matches!(node.kind(), UiNodeKind::Custom { primitive }
2408 if primitives.uses_primary_focus(&primitive.primitive))
2409 {
2410 element.track_focus(&handle.clone().tab_stop(false))
2414 } else {
2415 element.track_focus(handle)
2416 }
2417}
2418
2419fn primitive_focus_owner(
2420 retained: Option<&RetainedUiTree>,
2421 retained_id: Option<NodeId>,
2422 focus_handles: &BTreeMap<NodeId, FocusHandle>,
2423) -> Option<FocusHandle> {
2424 let retained_id = retained_id?;
2425 if let Some(handle) = focus_handles.get(&retained_id) {
2426 return Some(handle.clone());
2427 }
2428 let retained = retained?;
2429 let mut cursor = Some(retained_id);
2430 while let Some(node_id) = cursor {
2431 let node = retained.node(node_id)?;
2432 if node.focus_styled()
2433 && let Some(handle) = focus_handles.get(&node_id)
2434 {
2435 return Some(handle.clone());
2436 }
2437 cursor = node.parent();
2438 }
2439 None
2440}
2441
2442pub(crate) fn render_motion_ghost(
2443 ghost: &crate::motion::MotionGhost,
2444 colors: &impl ColorResolver,
2445 primitives: &PrimitiveRegistry,
2446 resources: &WindowRenderResources<'_>,
2447) -> AnyElement {
2448 let child = GpuiNodeRenderer::render_subtree_with_window_runtime(
2449 &ghost.node,
2450 colors,
2451 &InteractionState::default(),
2452 primitives,
2453 resources,
2454 &ghost.path,
2455 );
2456 div()
2457 .absolute()
2458 .left(px(f64_to_f32(ghost.bounds.x)))
2459 .top(px(f64_to_f32(ghost.bounds.y)))
2460 .w(px(f64_to_f32(ghost.bounds.width)))
2461 .h(px(f64_to_f32(ghost.bounds.height)))
2462 .child(child)
2463 .into_any_element()
2464}
2465
2466fn resolve_ambient_text_color<C: ColorResolver>(
2467 style: &StyleProperties,
2468 colors: &C,
2469 inherited: Option<Rgba8>,
2470) -> Option<Rgba8> {
2471 style
2472 .text_color
2473 .as_ref()
2474 .and_then(|color| colors.resolve(color))
2475 .or(inherited)
2476}
2477
2478fn render_layer_node<C: ColorResolver>(
2479 element: impl ParentElement + IntoElement,
2480 content: &UiNode,
2481 spec: &crate::LayerNodeSpec,
2482 environment: &RenderEnvironment<'_, C>,
2483 boundary_fallback: Option<&UiNode>,
2484 path: &str,
2485 retained_id: Option<NodeId>,
2486) -> AnyElement {
2487 let content = GpuiNodeRenderer::render_internal(
2488 content,
2489 environment,
2490 boundary_fallback,
2491 &format!("{path}/content"),
2492 retained_child_id(
2493 environment.retained,
2494 environment.retained_links,
2495 retained_id,
2496 "content",
2497 0,
2498 ),
2499 );
2500 element
2501 .child(ScriptLayerElement::new(
2502 path,
2503 content,
2504 spec.clone(),
2505 environment.overlays.clone(),
2506 environment.view_id,
2507 ))
2508 .into_any_element()
2509}
2510
2511fn decorate_scrollbars<C, E>(
2512 element: E,
2513 node: &UiNode,
2514 environment: &RenderEnvironment<'_, C>,
2515 path: &str,
2516 retained_id: Option<NodeId>,
2517) -> E
2518where
2519 C: ColorResolver,
2520 E: ParentElement + IntoElement,
2521{
2522 let (Some(spec), Some(handle)) = (
2523 crate::ScrollbarSpec::from_node(node),
2524 retained_id.and_then(|node| environment.scroll_handles.get(&node)),
2525 ) else {
2526 return element;
2527 };
2528 let part_color = |part, token, fallback| {
2529 node.part_style(part)
2530 .and_then(|style| {
2531 style
2532 .resolve(&InteractionState::default())
2533 .background
2534 .as_ref()
2535 .and_then(|color| environment.colors.resolve(color))
2536 })
2537 .unwrap_or_else(|| semantic_color(environment.colors, token, fallback))
2538 };
2539 element.child(crate::scrollbar::ThemedScrollbar::new(
2540 format!("{path}/scrollbars"),
2541 crate::interaction::InteractionOwner::new(environment.view_id, format!("scrollbar:{path}")),
2542 environment.interactions.clone(),
2543 handle.clone(),
2544 spec,
2545 environment.direction,
2546 (
2547 part_color("scrollbar_track", "surface_raised", 0x0027_272aff),
2548 part_color("scrollbar_thumb", "text_muted", 0x0071_717aff),
2549 part_color("scrollbar_thumb_hover", "accent", 0x003b_82f6ff),
2550 ),
2551 ))
2552}
2553
2554fn normalize_text_content_layout(node: &UiNode, style: &mut StyleProperties) {
2555 if matches!(
2556 node.kind(),
2557 UiNodeKind::Text { .. } | UiNodeKind::RichText { .. }
2558 ) && style.display.is_none()
2559 && (style.align.is_some() || style.justify.is_some())
2560 {
2561 style.display = Some(DisplayMode::Flex);
2562 }
2563}
2564
2565fn render_text_node<C: ColorResolver>(
2566 element: impl ParentElement + IntoElement,
2567 node: &UiNode,
2568 text: &str,
2569 environment: &RenderEnvironment<'_, C>,
2570 path: &str,
2571 retained_id: Option<NodeId>,
2572) -> AnyElement {
2573 if !node_selectable(node) {
2574 return element.child(text.to_owned()).into_any_element();
2575 }
2576 let highlight = environment
2577 .colors
2578 .resolve(&ColorValue::Token("selection".to_owned()))
2579 .unwrap_or(Rgba8::from_rgba_hex(0x3b82_f655));
2580 let selection_id = retained_id.map_or_else(
2581 || path.to_owned(),
2582 |node| format!("{}:{node}", environment.view_id),
2583 );
2584 element
2585 .child(SelectableText::new(
2586 selection_id,
2587 retained_id,
2588 text,
2589 highlight,
2590 environment.text_selection.clone(),
2591 environment.host_focus.cloned(),
2592 ))
2593 .into_any_element()
2594}
2595
2596fn render_virtual_collection<C: ColorResolver>(
2597 element: impl ParentElement + IntoElement,
2598 spec: &crate::VirtualCollectionNodeSpec,
2599 environment: &RenderEnvironment<'_, C>,
2600 path: &str,
2601 retained_id: Option<NodeId>,
2602) -> AnyElement {
2603 element
2604 .child(native_virtual_collection_element(
2605 spec,
2606 environment,
2607 path,
2608 retained_id,
2609 ))
2610 .into_any_element()
2611}
2612
2613fn render_flattened_children<C: ColorResolver>(
2614 children: &[UiNode],
2615 environment: &RenderEnvironment<'_, C>,
2616 boundary_fallback: Option<&UiNode>,
2617 path: &str,
2618 retained_id: Option<NodeId>,
2619) -> Vec<AnyElement> {
2620 let mut rendered = Vec::new();
2621 for (index, child) in children.iter().enumerate() {
2622 let child_path = child.key().map_or_else(
2623 || format!("{path}/{index}"),
2624 |key| format!("{path}/{}", key.as_str()),
2625 );
2626 let child_id = retained_child_id(
2627 environment.retained,
2628 environment.retained_links,
2629 retained_id,
2630 "children",
2631 index,
2632 );
2633 if let UiNodeKind::Fragment { children } = child.kind() {
2634 rendered.extend(render_flattened_children(
2635 children,
2636 environment,
2637 boundary_fallback,
2638 &child_path,
2639 child_id,
2640 ));
2641 } else {
2642 rendered.push(GpuiNodeRenderer::render_internal(
2643 child,
2644 environment,
2645 boundary_fallback,
2646 &child_path,
2647 child_id,
2648 ));
2649 }
2650 }
2651 rendered
2652}
2653
2654fn node_needs_interaction_wrapper(node: &UiNode, needs: u8) -> bool {
2655 needs & 0b10_1000 != 0
2656 || !is_disabled(node)
2657 && (needs & 0b0111 != 0
2658 || node_has_focus_declaration(node)
2659 || node.style().hover.is_some()
2660 || node.style().active.is_some()
2661 || node.style().focus.is_some()
2662 || node_has_raw_pointer_handlers(node)
2663 || node_scrollable(node))
2664}
2665
2666fn apply_native_semantics(
2667 mut element: Stateful<Div>,
2668 semantic: Option<&crate::AccessibilityNode>,
2669) -> Stateful<Div> {
2670 let Some(semantic) = semantic else {
2671 return element;
2672 };
2673 let Some(role) = crate::accessibility::native_role(&semantic.role)
2674 .expect("committed semantic roles are validated")
2675 else {
2676 return element;
2677 };
2678 element = element.role(role);
2679 if let Some(id) = &semantic.semantic_id {
2680 element = element.accessibility_id(id.clone());
2681 }
2682 if !semantic.name.is_empty() {
2683 if semantic.role == "text" {
2684 element = element.aria_value(semantic.name.clone());
2688 } else {
2689 element = element.aria_label(semantic.name.clone());
2690 }
2691 }
2692 if !semantic.description.is_empty() {
2693 element = element.aria_description(semantic.description.clone());
2694 }
2695 if let Some(selected) = semantic
2696 .selected
2697 .or_else(|| selected_from_checked(role, semantic.checked.as_ref()))
2698 {
2699 element = element.aria_selected(selected);
2700 }
2701 if let Some(expanded) = semantic
2702 .expanded
2703 .or_else(|| expanded_from_checked(role, semantic.checked.as_ref()))
2704 {
2705 element = element.aria_expanded(expanded);
2706 }
2707 if let Some(toggled) = toggled_from_semantics(role, semantic) {
2708 element = element.aria_toggled(toggled);
2709 }
2710 if let Some(value) = semantic.value.as_ref().and_then(accessibility_value_text) {
2711 element = element.aria_value(value);
2712 }
2713 if let Some(placeholder) = &semantic.placeholder {
2714 element = element.aria_placeholder(placeholder.clone());
2715 }
2716 if let Some(key_shortcuts) = &semantic.key_shortcuts {
2717 element = element.aria_keyshortcuts(key_shortcuts.clone());
2718 }
2719 if let Some(value) = semantic.value.as_ref().and_then(accessibility_number_value) {
2720 element = element.aria_numeric_value(value);
2721 }
2722 if let Some(value) = semantic.value_min {
2723 element = element.aria_min_numeric_value(value);
2724 }
2725 if let Some(value) = semantic.value_max {
2726 element = element.aria_max_numeric_value(value);
2727 }
2728 if let Some(orientation) = semantic.orientation.as_deref() {
2729 element = element.aria_orientation(match orientation {
2730 "horizontal" => gpui::Orientation::Horizontal,
2731 "vertical" => gpui::Orientation::Vertical,
2732 _ => unreachable!("committed accessibility orientation is validated"),
2733 });
2734 }
2735 if let Some(level) = semantic.level {
2736 element = element.aria_level(level);
2737 }
2738 if let Some(position) = semantic.position_in_set {
2739 element = element.aria_position_in_set(position);
2740 }
2741 if let Some(size) = semantic.size_of_set {
2742 element = element.aria_size_of_set(size);
2743 }
2744 if let Some(index) = semantic.row_index {
2745 element = element.aria_row_index(index);
2746 }
2747 if let Some(index) = semantic.column_index {
2748 element = element.aria_column_index(index);
2749 }
2750 if let Some(count) = semantic.row_count {
2751 element = element.aria_row_count(count);
2752 }
2753 if let Some(count) = semantic.column_count {
2754 element = element.aria_column_count(count);
2755 }
2756 apply_native_semantic_flags(element, semantic)
2757}
2758
2759fn apply_native_semantic_flags(
2760 mut element: Stateful<Div>,
2761 semantic: &crate::AccessibilityNode,
2762) -> Stateful<Div> {
2763 let required = semantic.required;
2764 let disabled = semantic.disabled;
2765 let read_only = semantic.read_only;
2766 let invalid = semantic.invalid;
2767 let current = semantic.current.clone();
2768 if !(required || disabled || read_only || invalid || current.is_some()) {
2769 return element;
2770 }
2771 element = element.a11y_synthetic_children(move |builder| {
2772 let node = builder.parent_node();
2773 if required {
2774 node.set_required();
2775 }
2776 if disabled {
2777 node.set_disabled();
2778 }
2779 if read_only {
2780 node.set_read_only();
2781 }
2782 if invalid {
2783 node.set_invalid(gpui::accesskit::Invalid::True);
2784 }
2785 if let Some(current) = current.as_deref() {
2786 node.set_aria_current(match current {
2787 "page" => gpui::accesskit::AriaCurrent::Page,
2788 "step" => gpui::accesskit::AriaCurrent::Step,
2789 "location" => gpui::accesskit::AriaCurrent::Location,
2790 "date" => gpui::accesskit::AriaCurrent::Date,
2791 "time" => gpui::accesskit::AriaCurrent::Time,
2792 "true" => gpui::accesskit::AriaCurrent::True,
2793 _ => unreachable!("committed aria-current values are validated"),
2794 });
2795 }
2796 });
2797 element
2798}
2799
2800fn apply_primitive_accessibility_actions<C: ColorResolver>(
2801 mut element: Stateful<Div>,
2802 node: &UiNode,
2803 retained_id: Option<NodeId>,
2804 semantic: Option<&crate::AccessibilityNode>,
2805 environment: &RenderEnvironment<'_, C>,
2806) -> Stateful<Div> {
2807 let Some(semantic) = semantic else {
2808 return element;
2809 };
2810 if semantic.disabled {
2811 return element;
2812 }
2813 let (UiNodeKind::Custom { primitive }, Some(node), Some(key)) =
2814 (node.kind(), retained_id, node.key())
2815 else {
2816 return element;
2817 };
2818 let instance =
2819 crate::PrimitiveInstanceId::new(primitive.primitive.clone(), key.as_str().to_owned(), node);
2820 for action in environment
2821 .primitives
2822 .accessibility_actions(&instance)
2823 .into_iter()
2824 .filter(|action| !(semantic.read_only && *action == gpui::AccessibleAction::SetValue))
2825 {
2826 let registry = environment.primitives.clone();
2827 let instance = instance.clone();
2828 element = element.on_a11y_action(action, move |data, window, cx| {
2829 let _ = registry.perform_accessibility_action(&instance, action, data, window, cx);
2832 });
2833 }
2834 element
2835}
2836
2837fn selected_from_checked(role: gpui::Role, checked: Option<&UiValue>) -> Option<bool> {
2838 matches!(
2839 role,
2840 gpui::Role::Tab
2841 | gpui::Role::ListBoxOption
2842 | gpui::Role::Row
2843 | gpui::Role::GridCell
2844 | gpui::Role::MenuItem
2845 )
2846 .then(|| checked.and_then(accessibility_bool_value))
2847 .flatten()
2848}
2849
2850fn expanded_from_checked(role: gpui::Role, checked: Option<&UiValue>) -> Option<bool> {
2851 matches!(role, gpui::Role::ComboBox)
2852 .then(|| checked.and_then(accessibility_bool_value))
2853 .flatten()
2854}
2855
2856fn toggled_from_semantics(
2857 role: gpui::Role,
2858 semantic: &crate::AccessibilityNode,
2859) -> Option<gpui::Toggled> {
2860 if let Some(pressed) = semantic.pressed {
2861 return Some(pressed.into());
2862 }
2863 if matches!(
2864 role,
2865 gpui::Role::CheckBox
2866 | gpui::Role::RadioButton
2867 | gpui::Role::Switch
2868 | gpui::Role::MenuItemCheckBox
2869 | gpui::Role::MenuItemRadio
2870 ) {
2871 return match semantic.checked.as_ref() {
2872 Some(UiValue::Bool(value)) => Some((*value).into()),
2873 Some(UiValue::String(value)) if value == "mixed" => Some(gpui::Toggled::Mixed),
2874 _ => None,
2875 };
2876 }
2877 None
2878}
2879
2880fn accessibility_bool_value(value: &UiValue) -> Option<bool> {
2881 match value {
2882 UiValue::Bool(value) => Some(*value),
2883 _ => None,
2884 }
2885}
2886
2887fn accessibility_number_value(value: &UiValue) -> Option<f64> {
2888 match value {
2889 UiValue::Float(value) => Some(*value),
2890 UiValue::Integer(value) => value.to_string().parse().ok(),
2891 _ => None,
2892 }
2893}
2894
2895fn accessibility_value_text(value: &UiValue) -> Option<String> {
2896 match value {
2897 UiValue::Null | UiValue::Handle(_) => None,
2898 UiValue::Bool(value) => Some(value.to_string()),
2899 UiValue::Integer(value) => Some(value.to_string()),
2900 UiValue::Float(value) => Some(value.to_string()),
2901 UiValue::String(value) => Some(value.clone()),
2902 UiValue::Array(_) | UiValue::Map(_) => serde_json::to_string(value).ok(),
2903 }
2904}
2905
2906fn resolved_hit_test<C: ColorResolver>(
2907 node: &UiNode,
2908 environment: &RenderEnvironment<'_, C>,
2909) -> Option<HitTestBehavior> {
2910 let interaction = if is_disabled(node) {
2911 environment.interaction.clone().with(PseudoState::Disabled)
2912 } else {
2913 environment.interaction.clone()
2914 };
2915 node.style().resolve(&interaction).hit_test
2916}
2917
2918fn apply_hit_test(element: Stateful<Div>, hit_test: Option<HitTestBehavior>) -> Stateful<Div> {
2919 match hit_test {
2920 Some(HitTestBehavior::Block) => element.occlude(),
2921 Some(HitTestBehavior::BlockExceptScroll) => element.block_mouse_except_scroll(),
2922 None => element,
2923 }
2924}
2925
2926fn node_has_focus_declaration(node: &UiNode) -> bool {
2927 node.attributes().contains_key("tab_index")
2928 || node.attributes().get("tab_group") == Some(&UiValue::Bool(true))
2929 || node.attributes().contains_key("tab_stop")
2930}
2931
2932fn apply_environment_scroll<C: ColorResolver>(
2933 element: Stateful<Div>,
2934 node: &UiNode,
2935 retained_id: Option<NodeId>,
2936 environment: &RenderEnvironment<'_, C>,
2937) -> Stateful<Div> {
2938 apply_scroll_behavior(
2939 element,
2940 node,
2941 retained_id,
2942 environment.scroll_handles,
2943 environment.scroll_anchors,
2944 )
2945}
2946
2947fn apply_environment_raw_pointer<C: ColorResolver>(
2948 element: Stateful<Div>,
2949 node: &UiNode,
2950 retained_id: Option<NodeId>,
2951 environment: &RenderEnvironment<'_, C>,
2952) -> Stateful<Div> {
2953 apply_raw_pointer_handlers(
2954 element,
2955 node,
2956 environment.dispatcher,
2957 retained_id,
2958 environment.pointer_capture,
2959 environment.geometry,
2960 scroll_handles_for_node(
2961 environment.retained,
2962 retained_id,
2963 environment.scroll_handles,
2964 ),
2965 )
2966}
2967
2968fn node_tab_stop(node: &UiNode) -> bool {
2969 !matches!(
2970 node.attributes().get("tab_stop"),
2971 Some(UiValue::Bool(false))
2972 )
2973}
2974
2975fn node_selectable(node: &UiNode) -> bool {
2976 matches!(
2977 node.attributes().get("selectable"),
2978 Some(UiValue::Bool(true))
2979 )
2980}
2981
2982fn node_tab_index(node: &UiNode) -> isize {
2983 match node.attributes().get("tab_index") {
2984 Some(UiValue::Integer(index)) => isize::try_from(*index).unwrap_or_default(),
2985 _ => 0,
2986 }
2987}
2988
2989fn apply_tab_behavior(
2990 mut element: Stateful<Div>,
2991 node: &UiNode,
2992 implicit_tab_stop: bool,
2993) -> Stateful<Div> {
2994 element =
2995 element
2996 .tab_index(node_tab_index(node))
2997 .tab_stop(if node_has_focus_declaration(node) {
2998 node_tab_stop(node)
2999 } else {
3000 implicit_tab_stop
3001 });
3002 if node.attributes().get("tab_group") == Some(&UiValue::Bool(true)) {
3003 element = element.tab_group();
3004 }
3005 element
3006}
3007
3008fn interaction_element_id(retained_id: Option<NodeId>, path: &str) -> String {
3009 retained_id.map_or_else(|| path.to_owned(), |id| format!("gpui-rhai-node-{id}"))
3010}
3011
3012fn retained_child_id(
3013 tree: Option<&RetainedUiTree>,
3014 links: Option<&BTreeMap<NodeId, Vec<crate::RetainedChildLink>>>,
3015 parent: Option<NodeId>,
3016 group: &str,
3017 index: usize,
3018) -> Option<NodeId> {
3019 let parent = parent?;
3020 tree.and_then(|tree| tree.node(parent))
3021 .and_then(|node| {
3022 node.children()
3023 .filter(|child| child.group() == group)
3024 .nth(index)
3025 })
3026 .or_else(|| {
3027 links
3028 .and_then(|links| links.get(&parent))
3029 .and_then(|children| {
3030 children
3031 .iter()
3032 .filter(|child| child.group() == group)
3033 .nth(index)
3034 })
3035 })
3036 .map(crate::RetainedChildLink::node)
3037}
3038
3039fn styled_text(
3040 text: &str,
3041 spans: &[crate::Span],
3042 colors: &impl ColorResolver,
3043 motions: &BTreeMap<MotionKey, f64>,
3044 path: &str,
3045) -> StyledText {
3046 let mut offset = 0usize;
3047 let highlights = spans.iter().filter_map(|span| {
3048 let start = offset;
3049 offset = offset.saturating_add(span.text().len());
3050 let style = HighlightStyle {
3051 color: span
3052 .color_value()
3053 .and_then(|color| colors.resolve(color))
3054 .map(|color| rgba(color.as_rgba_hex()).into()),
3055 font_weight: span.is_bold().then_some(FontWeight::BOLD),
3056 font_style: span.is_italic().then_some(FontStyle::Italic),
3057 fade_out: span.key().and_then(|key| {
3058 motions
3059 .get(&MotionKey::for_node(
3060 &crate::motion::span_motion_path(path, key),
3061 MotionProperty::Opacity,
3062 ))
3063 .map(|opacity| f64_to_f32(1.0 - opacity.clamp(0.0, 1.0)))
3064 }),
3065 ..HighlightStyle::default()
3066 };
3067 (style != HighlightStyle::default()).then_some((start..offset, style))
3068 });
3069 StyledText::new(text.to_owned()).with_highlights(highlights)
3070}
3071
3072fn render_canvas(
3073 element: impl ParentElement + IntoElement,
3074 scene: &crate::CanvasScene,
3075 colors: &impl ColorResolver,
3076 motion: NodeMotionValues,
3077 retained_id: Option<NodeId>,
3078 geometry: &crate::GeometryRegistry,
3079) -> AnyElement {
3080 let scene = scene.clone();
3081 let colors = OwnedColorResolver::capture(colors);
3082 let geometry = geometry.clone();
3083 let canvas = gpui::canvas(
3084 move |bounds, window, _| {
3085 if let Some(node) = retained_id
3086 && let Ok(drawable) = crate::GeometryBounds::new(
3087 f64::from(bounds.origin.x),
3088 f64::from(bounds.origin.y),
3089 f64::from(bounds.size.width),
3090 f64::from(bounds.size.height),
3091 )
3092 {
3093 let offset = window.pixel_snap_point(window.element_offset());
3094 geometry.update_canvas_drawable(
3095 node,
3096 drawable,
3097 (f64::from(offset.x), f64::from(offset.y)),
3098 );
3099 }
3100 },
3101 move |bounds, (), window, _| {
3102 paint_canvas_scene(bounds, &scene, &colors, motion, window);
3103 },
3104 )
3105 .size_full();
3106 element.child(canvas).into_any_element()
3107}
3108
3109fn paint_canvas_scene(
3110 bounds: Bounds<Pixels>,
3111 scene: &crate::CanvasScene,
3112 colors: &impl ColorResolver,
3113 motion: NodeMotionValues,
3114 window: &mut Window,
3115) {
3116 for command in scene.commands() {
3117 match command {
3118 crate::CanvasCommand::Rect {
3119 x,
3120 y,
3121 width,
3122 height,
3123 fill: color,
3124 ..
3125 } => {
3126 if let Some(color) = colors.resolve(color) {
3127 paint_canvas_polygon(
3128 [
3129 (*x, *y),
3130 (x + width, *y),
3131 (x + width, y + height),
3132 (*x, y + height),
3133 ],
3134 bounds,
3135 motion,
3136 rgba(color.as_rgba_hex()),
3137 window,
3138 );
3139 }
3140 }
3141 crate::CanvasCommand::Circle {
3142 center_x,
3143 center_y,
3144 radius,
3145 fill: color,
3146 ..
3147 } => {
3148 if let Some(color) = colors.resolve(color) {
3149 let points = (0..48).map(|index| {
3150 let angle = std::f64::consts::TAU * f64::from(index) / 48.0;
3151 (
3152 center_x + radius * angle.cos(),
3153 center_y + radius * angle.sin(),
3154 )
3155 });
3156 paint_canvas_polygon(points, bounds, motion, rgba(color.as_rgba_hex()), window);
3157 }
3158 }
3159 crate::CanvasCommand::Line {
3160 from_x,
3161 from_y,
3162 to_x,
3163 to_y,
3164 width,
3165 color,
3166 ..
3167 } => {
3168 if let Some(color) = colors.resolve(color) {
3169 let mut path = gpui::PathBuilder::stroke(px(f64_to_f32(*width)));
3170 path.move_to(node_canvas_point(bounds, *from_x, *from_y, motion));
3171 path.line_to(node_canvas_point(bounds, *to_x, *to_y, motion));
3172 if let Ok(path) = path.build() {
3173 window.paint_path(path, rgba(color.as_rgba_hex()));
3174 }
3175 }
3176 }
3177 crate::CanvasCommand::Path { .. } | crate::CanvasCommand::MorphPath { .. } => {
3178 paint_canvas_path(bounds, command, colors, motion, window);
3179 }
3180 }
3181 }
3182}
3183
3184#[allow(clippy::too_many_lines)]
3185fn paint_canvas_path(
3186 bounds: Bounds<Pixels>,
3187 command: &crate::CanvasCommand,
3188 colors: &impl ColorResolver,
3189 motion: NodeMotionValues,
3190 window: &mut Window,
3191) {
3192 let interpolated;
3193 let (segments, fill, stroke, transform, clip, is_morph) = match command {
3194 crate::CanvasCommand::Path {
3195 segments,
3196 fill,
3197 stroke,
3198 transform,
3199 clip,
3200 ..
3201 } => (
3202 segments.as_slice(),
3203 fill.as_ref(),
3204 stroke.as_ref(),
3205 transform,
3206 clip,
3207 false,
3208 ),
3209 crate::CanvasCommand::MorphPath {
3210 from,
3211 to,
3212 stroke,
3213 transform,
3214 clip,
3215 ..
3216 } => {
3217 interpolated =
3218 crate::canvas::interpolate_path(from, to, motion.path_progress.unwrap_or(0.0))
3219 .unwrap_or_else(|| from.clone());
3220 (
3221 interpolated.as_slice(),
3222 None,
3223 Some(stroke),
3224 transform,
3225 clip,
3226 true,
3227 )
3228 }
3229 _ => return,
3230 };
3231 let mut builder = stroke.map_or_else(gpui::PathBuilder::fill, |(_, width)| {
3232 gpui::PathBuilder::stroke(px(f64_to_f32(*width)))
3233 });
3234 if !is_morph && stroke.is_some() && motion.path_progress.is_some_and(|progress| progress < 1.0)
3235 {
3236 let paths = crate::canvas::trimmed_canvas_paths(
3237 crate::canvas::flatten_path(segments, *transform),
3238 motion.path_progress.unwrap_or(1.0).clamp(0.0, 1.0),
3239 );
3240 for path in paths {
3241 if let Some((first, rest)) = path.split_first() {
3242 builder.move_to(node_canvas_point(bounds, first.0, first.1, motion));
3243 for point in rest {
3244 builder.line_to(node_canvas_point(bounds, point.0, point.1, motion));
3245 }
3246 }
3247 }
3248 } else {
3249 for segment in segments {
3250 match segment {
3251 crate::CanvasPathSegment::Move { x, y } => {
3252 builder.move_to(canvas_path_point(bounds, *transform, *x, *y, motion));
3253 }
3254 crate::CanvasPathSegment::Line { x, y } => {
3255 builder.line_to(canvas_path_point(bounds, *transform, *x, *y, motion));
3256 }
3257 crate::CanvasPathSegment::Quadratic {
3258 x,
3259 y,
3260 control_x,
3261 control_y,
3262 } => builder.curve_to(
3263 canvas_path_point(bounds, *transform, *x, *y, motion),
3264 canvas_path_point(bounds, *transform, *control_x, *control_y, motion),
3265 ),
3266 crate::CanvasPathSegment::Cubic {
3267 x,
3268 y,
3269 control_a_x,
3270 control_a_y,
3271 control_b_x,
3272 control_b_y,
3273 } => builder.cubic_bezier_to(
3274 canvas_path_point(bounds, *transform, *x, *y, motion),
3275 canvas_path_point(bounds, *transform, *control_a_x, *control_a_y, motion),
3276 canvas_path_point(bounds, *transform, *control_b_x, *control_b_y, motion),
3277 ),
3278 crate::CanvasPathSegment::Close => builder.close(),
3279 }
3280 }
3281 }
3282 let Ok(path) = builder.build() else {
3283 return;
3284 };
3285 let paint = stroke
3286 .and_then(|(color, _)| {
3287 colors
3288 .resolve(color)
3289 .map(|color| Background::from(rgba(color.as_rgba_hex())))
3290 })
3291 .or_else(|| fill.and_then(|fill| canvas_fill(fill, colors)));
3292 let Some(paint) = paint else {
3293 return;
3294 };
3295 if let Some(clip) = clip {
3296 let mask = ContentMask {
3297 bounds: Bounds::new(
3298 point(
3299 bounds.origin.x + px(f64_to_f32(clip.x)),
3300 bounds.origin.y + px(f64_to_f32(clip.y)),
3301 ),
3302 gpui::size(px(f64_to_f32(clip.width)), px(f64_to_f32(clip.height))),
3303 ),
3304 };
3305 window.with_content_mask(Some(mask), |window| window.paint_path(path, paint));
3306 } else {
3307 window.paint_path(path, paint);
3308 }
3309}
3310
3311fn paint_canvas_polygon(
3312 points: impl IntoIterator<Item = (f64, f64)>,
3313 bounds: Bounds<Pixels>,
3314 motion: NodeMotionValues,
3315 paint: impl Into<Background>,
3316 window: &mut Window,
3317) {
3318 let mut points = points.into_iter();
3319 let Some((x, y)) = points.next() else {
3320 return;
3321 };
3322 let mut path = gpui::PathBuilder::fill();
3323 path.move_to(node_canvas_point(bounds, x, y, motion));
3324 for (x, y) in points {
3325 path.line_to(node_canvas_point(bounds, x, y, motion));
3326 }
3327 path.close();
3328 if let Ok(path) = path.build() {
3329 window.paint_path(path, paint.into());
3330 }
3331}
3332
3333fn canvas_fill(fill: &crate::CanvasFill, colors: &impl ColorResolver) -> Option<Background> {
3334 match fill {
3335 crate::CanvasFill::Solid(color) => colors
3336 .resolve(color)
3337 .map(|color| Background::from(rgba(color.as_rgba_hex()))),
3338 crate::CanvasFill::LinearGradient(gradient) => {
3339 let from = colors.resolve(&gradient.from)?;
3340 let to = colors.resolve(&gradient.to)?;
3341 Some(linear_gradient(
3342 f64_to_f32(gradient.angle_degrees),
3343 linear_color_stop(rgba(from.as_rgba_hex()), 0.0),
3344 linear_color_stop(rgba(to.as_rgba_hex()), 1.0),
3345 ))
3346 }
3347 }
3348}
3349
3350fn canvas_path_point(
3351 bounds: Bounds<Pixels>,
3352 transform: crate::CanvasTransform,
3353 x: f64,
3354 y: f64,
3355 motion: NodeMotionValues,
3356) -> Point<Pixels> {
3357 let (transformed_x, transformed_y) = crate::canvas::canvas_transform_point(transform, x, y);
3358 node_canvas_point(bounds, transformed_x, transformed_y, motion)
3359}
3360
3361fn node_canvas_point(
3362 bounds: Bounds<Pixels>,
3363 x: f64,
3364 y: f64,
3365 motion: NodeMotionValues,
3366) -> Point<Pixels> {
3367 let (transformed_x, transformed_y) = crate::canvas::canvas_motion_point(
3368 f64::from(bounds.size.width),
3369 f64::from(bounds.size.height),
3370 x,
3371 y,
3372 motion.canvas_transform(),
3373 );
3374 point(
3375 bounds.origin.x + px(f64_to_f32(transformed_x)),
3376 bounds.origin.y + px(f64_to_f32(transformed_y)),
3377 )
3378}
3379
3380fn render_image<C: ColorResolver>(
3381 element: impl ParentElement + IntoElement,
3382 source: &ImageSourceSpec,
3383 environment: &RenderEnvironment<'_, C>,
3384) -> AnyElement {
3385 let tint = environment.ambient_text_color;
3386 environment.assets.map_or_else(
3387 || div().child("Image registry unavailable").into_any_element(),
3388 |assets| {
3389 let handle = match source {
3390 ImageSourceSpec::Handle(handle) => Ok(handle.clone()),
3391 ImageSourceSpec::Asset(asset) => assets
3392 .cached_image(asset)
3393 .map(|image| image.opaque().clone()),
3394 };
3395 match handle.and_then(|handle| assets.image_source_tinted(&handle, tint)) {
3396 Ok(source) => element.child(img(source).size_full()).into_any_element(),
3397 Err(error) => div()
3398 .child(format!("Image error: {error}"))
3399 .into_any_element(),
3400 }
3401 },
3402 )
3403}
3404
3405fn render_inline_svg<C: ColorResolver>(
3406 element: impl ParentElement + IntoElement,
3407 node: &UiNode,
3408 source: &crate::InlineSvg,
3409 environment: &RenderEnvironment<'_, C>,
3410) -> AnyElement {
3411 let interaction = if is_disabled(node) {
3412 environment.interaction.clone().with(PseudoState::Disabled)
3413 } else {
3414 environment.interaction.clone()
3415 };
3416 let resolved_style = node.style().resolve(&interaction);
3417 let color = environment.ambient_text_color;
3418 let fills_styled_box = resolved_style.width.is_some() || resolved_style.height.is_some();
3419 let image = environment.assets.map_or_else(
3420 || AssetRegistry::new().inline_svg_source(source.shared_source(), color),
3421 |assets| assets.inline_svg_source(source.shared_source(), color),
3422 );
3423 element
3424 .child(inline_svg_image(image, fills_styled_box))
3425 .into_any_element()
3426}
3427
3428fn inline_svg_image(source: impl Into<gpui::ImageSource>, fills_styled_box: bool) -> Img {
3429 let image = img(source);
3430 if fills_styled_box {
3431 image.size_full()
3432 } else {
3433 image
3434 }
3435}
3436
3437fn scoped_overlay_spec(
3438 spec: &OverlayNodeSpec,
3439 view_id: &str,
3440 direction: TextDirection,
3441) -> OverlayNodeSpec {
3442 let mut rendered = spec.clone();
3443 rendered.id = WindowOverlayCoordinator::scoped_id(view_id, &rendered.id);
3444 rendered.parent = rendered
3445 .parent
3446 .as_ref()
3447 .map(|parent| WindowOverlayCoordinator::scoped_id(view_id, parent));
3448 rendered.placement = match (rendered.placement, direction) {
3449 (crate::OverlayPlacement::Start, TextDirection::LeftToRight)
3450 | (crate::OverlayPlacement::End, TextDirection::RightToLeft) => {
3451 crate::OverlayPlacement::Left
3452 }
3453 (crate::OverlayPlacement::End, TextDirection::LeftToRight)
3454 | (crate::OverlayPlacement::Start, TextDirection::RightToLeft) => {
3455 crate::OverlayPlacement::Right
3456 }
3457 (placement, _) => placement,
3458 };
3459 rendered
3460}
3461
3462fn native_overlay_element<C: ColorResolver>(
3463 node: &UiNode,
3464 trigger: &UiNode,
3465 content: &UiNode,
3466 spec: &OverlayNodeSpec,
3467 environment: &RenderEnvironment<'_, C>,
3468 boundary_fallback: Option<&UiNode>,
3469 (path, retained_id): (&str, Option<NodeId>),
3470) -> ScriptOverlayElement {
3471 let mut rendered_spec = scoped_overlay_spec(spec, environment.view_id, environment.direction);
3472 if rendered_spec.kind == crate::OverlayKind::Tooltip {
3473 rendered_spec.open = environment
3474 .overlays
3475 .tooltip_visible(&rendered_spec.id, std::time::Instant::now());
3476 }
3477 let trigger = GpuiNodeRenderer::render_internal(
3478 trigger,
3479 environment,
3480 boundary_fallback,
3481 &format!("{path}/trigger"),
3482 retained_child_id(
3483 environment.retained,
3484 environment.retained_links,
3485 retained_id,
3486 "trigger",
3487 0,
3488 ),
3489 );
3490 let content = GpuiNodeRenderer::render_internal(
3491 content,
3492 environment,
3493 boundary_fallback,
3494 &format!("{path}/content"),
3495 retained_child_id(
3496 environment.retained,
3497 environment.retained_links,
3498 retained_id,
3499 "content",
3500 0,
3501 ),
3502 );
3503 let event_target = EventTargetContext::new(retained_id, environment.geometry.clone());
3504 let open_target = event_target.clone();
3505 let open_change = node.handler("open_change").map(|handler| {
3506 let handler = handler.clone();
3507 let dispatcher = environment.dispatcher.cloned();
3508 Rc::new(move |open, window: &mut Window, cx: &mut App| {
3509 dispatch_ui_event(
3510 &handler,
3511 "open_change",
3512 UiValue::Bool(open),
3513 open_target.snapshot(),
3514 window,
3515 cx,
3516 dispatcher.as_ref(),
3517 );
3518 }) as crate::overlay_element::OpenChangeHandler
3519 });
3520 let panel_key = overlay_panel_key(
3521 node,
3522 environment.dispatcher,
3523 environment.direction,
3524 event_target,
3525 );
3526 let restore_focus_on_close =
3527 rendered_spec.modal || rendered_spec.kind == crate::OverlayKind::Menu;
3528 let overlay = ScriptOverlayElement::new(
3529 path,
3530 trigger,
3531 content,
3532 rendered_spec,
3533 open_change,
3534 panel_key,
3535 environment.overlays.clone(),
3536 );
3537 let backdrop_style = node.part_style("backdrop").map(|style| {
3538 let style = style.clone();
3539 let colors = OwnedColorResolver::capture(environment.colors);
3540 let direction = environment.direction;
3541 Rc::new(move |backdrop: Div| apply_style_override(backdrop, &style, &colors, direction))
3542 as crate::overlay_element::BackdropStyleHandler
3543 });
3544 overlay
3545 .with_backdrop_style(backdrop_style)
3546 .with_focus_ring(semantic_color(
3547 environment.colors,
3548 "focus_ring",
3549 0x003b_82f6,
3550 ))
3551 .with_focus_surface(semantic_color(environment.colors, "surface", 0x0018_181b))
3552 .restore_focus_on_close(restore_focus_on_close)
3553}
3554
3555fn overlay_panel_key(
3556 node: &UiNode,
3557 dispatcher: Option<&NodeEventDispatcher>,
3558 direction: TextDirection,
3559 target: EventTargetContext,
3560) -> Option<crate::overlay_element::PanelKeyHandler> {
3561 let handlers = node
3562 .handlers()
3563 .keys()
3564 .filter_map(|event| {
3565 event.strip_prefix("key:").and_then(|key| {
3566 node.handler(event)
3567 .cloned()
3568 .map(|handler| (key.to_owned(), handler))
3569 })
3570 })
3571 .collect::<BTreeMap<_, _>>();
3572 (!handlers.is_empty()).then(|| {
3573 let dispatcher = dispatcher.cloned();
3574 let payloads = node
3575 .handlers()
3576 .keys()
3577 .filter_map(|event| {
3578 event
3579 .strip_prefix("key:")
3580 .map(|key| (key.to_owned(), node.handler_payload(event).cloned()))
3581 })
3582 .collect::<BTreeMap<_, _>>();
3583 Rc::new(
3584 move |event: &gpui::KeyDownEvent, window: &mut Window, cx: &mut App| {
3585 let key = logical_keyboard_key(event.keystroke.key.as_str(), direction);
3586 let Some(callback) = handlers.get(key) else {
3587 return false;
3588 };
3589 dispatch_ui_event(
3590 callback,
3591 "key",
3592 payloads
3593 .get(key)
3594 .and_then(Clone::clone)
3595 .unwrap_or(UiValue::Null),
3596 target.snapshot(),
3597 window,
3598 cx,
3599 dispatcher.as_ref(),
3600 );
3601 true
3602 },
3603 ) as crate::overlay_element::PanelKeyHandler
3604 })
3605}
3606
3607fn native_virtual_collection_element<C: ColorResolver>(
3608 spec: &crate::VirtualCollectionNodeSpec,
3609 environment: &RenderEnvironment<'_, C>,
3610 path: &str,
3611 retained_id: Option<NodeId>,
3612) -> VirtualListEntityElement {
3613 let retained_roots: BTreeMap<String, NodeId> = retained_id
3614 .map(|node| {
3615 retained_children(environment.retained, environment.retained_links, node)
3616 .into_iter()
3617 .filter(|child| child.group() == "items")
3618 .zip(spec.realized.keys())
3619 .filter_map(|(child, index)| {
3620 crate::virtual_list_element::collection_item_key(spec, *index)
3621 .map(|key| (format!("item:{key}"), child.node()))
3622 })
3623 .collect()
3624 })
3625 .unwrap_or_default();
3626 let runtime = owned_slot_runtime(environment, path, retained_roots);
3627 VirtualListEntityElement::new_collection(path, spec.clone(), runtime, retained_id)
3628}
3629
3630fn owned_slot_runtime<C: ColorResolver>(
3631 environment: &RenderEnvironment<'_, C>,
3632 path: &str,
3633 retained_roots: BTreeMap<String, NodeId>,
3634) -> NodeSlotRuntime {
3635 let retained_links = retained_link_subtrees(
3636 environment.retained,
3637 environment.retained_links,
3638 retained_roots.values().copied(),
3639 );
3640 let mut focus_handles = environment.focus_handles.clone();
3641 if let Some(tree) = environment.retained {
3642 let nodes = retained_links
3643 .iter()
3644 .flat_map(|(parent, children)| {
3645 std::iter::once(*parent).chain(children.iter().map(crate::RetainedChildLink::node))
3646 })
3647 .collect::<BTreeSet<_>>();
3648 for node in nodes {
3649 if let Some(focus) =
3650 primitive_focus_owner(Some(tree), Some(node), environment.focus_handles)
3651 {
3652 focus_handles.insert(node, focus);
3653 }
3654 }
3655 }
3656 NodeSlotRuntime {
3657 now: environment.now,
3658 clock: environment.clock.clone(),
3659 colors: OwnedColorResolver::capture(environment.colors),
3660 primitives: environment.primitives.clone(),
3661 assets: environment.assets.cloned().unwrap_or_default(),
3662 dispatcher: environment
3663 .dispatcher
3664 .cloned()
3665 .unwrap_or_else(|| NodeEventDispatcher::new(|_, _, _, _, _| EventPropagation::Handled)),
3666 overlays: environment.overlays.clone(),
3667 interactions: environment.interactions.clone(),
3668 motions: environment.motions.clone(),
3669 motion_preference: environment.motion_preference,
3670 motion_quality: environment.motion_quality,
3671 signals: environment.signals.clone(),
3672 geometry: environment.geometry.clone(),
3673 pointer_capture: environment.pointer_capture.clone(),
3674 focus_handles,
3675 scroll_handles: environment.scroll_handles.clone(),
3676 scroll_anchors: environment.scroll_anchors.clone(),
3677 virtual_requests: environment.virtual_requests.clone(),
3678 text_selection: environment.text_selection.clone(),
3679 host_focus: environment.host_focus.cloned(),
3680 direction: environment.direction,
3681 locale: environment.locale.to_owned(),
3682 number: environment.number.cloned(),
3683 ambient_text_color: environment.ambient_text_color,
3684 base_path: path.to_owned(),
3685 view_id: environment.view_id.to_owned(),
3686 semantics: environment.semantics.cloned().unwrap_or_default(),
3687 a11y_active: environment.a11y_active,
3688 retained_roots,
3689 retained_links,
3690 }
3691}
3692
3693fn render_table_layout<C: ColorResolver>(
3694 node: &UiNode,
3695 environment: &RenderEnvironment<'_, C>,
3696 path: &str,
3697 retained_id: Option<NodeId>,
3698) -> Option<AnyElement> {
3699 let crate::table_layout::TableLayout::Columns(columns) = node.table_layout()? else {
3700 return None;
3701 };
3702 let mut columns = columns.clone();
3703 collect_table_column_minima(node, &mut columns, environment, true);
3704 let mut style = node.style().resolve(environment.interaction);
3705 resolve_style_lengths(&mut style, environment.colors);
3706 let roots = retained_id
3707 .into_iter()
3708 .map(|root| ("table".to_owned(), root))
3709 .collect();
3710 let runtime = owned_slot_runtime(environment, path, roots);
3711 let table = node.clone();
3712 let root_path = path.to_owned();
3713 Some(
3714 crate::table_layout::TableViewportElement::new(
3715 &interaction_element_id(retained_id, path),
3716 columns,
3717 environment.signals.clone(),
3718 environment.direction,
3719 style,
3720 retained_id.and_then(|root| environment.scroll_handles.get(&root).cloned()),
3721 move |plan, border| {
3722 let mut table = table;
3723 table.resolve_table_layout(&plan, border);
3724 runtime.render_at(&table, &root_path, retained_id)
3725 },
3726 )
3727 .into_any_element(),
3728 )
3729}
3730
3731fn collect_table_column_minima<C: ColorResolver>(
3732 node: &UiNode,
3733 columns: &mut [crate::table_layout::TableColumn],
3734 environment: &RenderEnvironment<'_, C>,
3735 root: bool,
3736) {
3737 if !root && node.table_layout().is_some() {
3738 return;
3739 }
3740 if let Some(column) = node.table_column().and_then(|index| columns.get_mut(index)) {
3741 let mut style = node.style().resolve(environment.interaction);
3742 resolve_style_lengths(&mut style, environment.colors);
3743 column.include_minimum_style(&style, environment.direction);
3744 }
3745 match node.kind() {
3746 UiNodeKind::Box { children } | UiNodeKind::Fragment { children } => {
3747 for child in children {
3748 collect_table_column_minima(child, columns, environment, false);
3749 }
3750 }
3751 UiNodeKind::VirtualCollection { spec } => {
3752 for child in spec.realized.values() {
3753 collect_table_column_minima(child, columns, environment, false);
3754 }
3755 }
3756 _ => {}
3757 }
3758}
3759
3760fn retained_link_subtrees(
3761 tree: Option<&RetainedUiTree>,
3762 links: Option<&BTreeMap<NodeId, Vec<crate::RetainedChildLink>>>,
3763 roots: impl IntoIterator<Item = NodeId>,
3764) -> BTreeMap<NodeId, Vec<crate::RetainedChildLink>> {
3765 let mut result = BTreeMap::new();
3766 let mut pending = roots.into_iter().collect::<Vec<_>>();
3767 while let Some(node) = pending.pop() {
3768 let children = retained_children(tree, links, node);
3769 pending.extend(children.iter().map(crate::RetainedChildLink::node));
3770 result.insert(node, children);
3771 }
3772 result
3773}
3774
3775fn retained_children(
3776 tree: Option<&RetainedUiTree>,
3777 links: Option<&BTreeMap<NodeId, Vec<crate::RetainedChildLink>>>,
3778 node: NodeId,
3779) -> Vec<crate::RetainedChildLink> {
3780 tree.and_then(|tree| tree.node(node))
3781 .map(|node| node.children().cloned().collect())
3782 .or_else(|| links.and_then(|links| links.get(&node)).cloned())
3783 .unwrap_or_default()
3784}
3785
3786#[derive(Clone, Copy, Default)]
3787struct NodeMotionValues {
3788 opacity: Option<f64>,
3789 translate_x: Option<f64>,
3790 translate_y: Option<f64>,
3791 rotate: Option<f64>,
3792 scale_x: Option<f64>,
3793 scale_y: Option<f64>,
3794 skew_x: Option<f64>,
3795 skew_y: Option<f64>,
3796 width: Option<f64>,
3797 height: Option<f64>,
3798 clip_height: Option<f64>,
3799 path_progress: Option<f64>,
3800}
3801
3802impl NodeMotionValues {
3803 fn set(&mut self, property: MotionProperty, value: f64) {
3804 let target = match property {
3805 MotionProperty::Opacity => &mut self.opacity,
3806 MotionProperty::TranslateX => &mut self.translate_x,
3807 MotionProperty::TranslateY => &mut self.translate_y,
3808 MotionProperty::Rotate => &mut self.rotate,
3809 MotionProperty::ScaleX => &mut self.scale_x,
3810 MotionProperty::ScaleY => &mut self.scale_y,
3811 MotionProperty::SkewX => &mut self.skew_x,
3812 MotionProperty::SkewY => &mut self.skew_y,
3813 MotionProperty::Width => &mut self.width,
3814 MotionProperty::Height => &mut self.height,
3815 MotionProperty::ClipHeight => &mut self.clip_height,
3816 MotionProperty::PathProgress => &mut self.path_progress,
3817 };
3818 *target = Some(value);
3819 }
3820
3821 fn canvas_transform(self) -> crate::geometry::CanvasMotionTransform {
3822 crate::geometry::CanvasMotionTransform {
3823 rotate: self.rotate.unwrap_or(0.0),
3824 scale_x: self.scale_x.unwrap_or(1.0),
3825 scale_y: self.scale_y.unwrap_or(1.0),
3826 skew_x: self.skew_x.unwrap_or(0.0),
3827 skew_y: self.skew_y.unwrap_or(0.0),
3828 path_progress: self.path_progress,
3829 }
3830 }
3831}
3832
3833#[derive(Clone, Debug, Default)]
3834struct NodeSignalValues {
3835 opacity: Option<f64>,
3836 translate_x: Option<f64>,
3837 translate_y: Option<f64>,
3838 rotate: Option<f64>,
3839 scale_x: Option<f64>,
3840 scale_y: Option<f64>,
3841 width: Option<f64>,
3842 width_override: Option<f64>,
3843 height: Option<f64>,
3844 height_override: Option<f64>,
3845 background: Option<ColorValue>,
3846 text_color: Option<ColorValue>,
3847 border_color: Option<ColorValue>,
3848}
3849
3850fn node_signals(registry: &crate::SignalRegistry, node: &UiNode) -> NodeSignalValues {
3851 let mut values = NodeSignalValues::default();
3852 for (property, signal) in node.signal_bindings() {
3853 let Ok(value) = registry.read(signal) else {
3854 continue;
3855 };
3856 match (property, value) {
3857 (crate::SignalProperty::Opacity, crate::SignalValue::Float(value)) => {
3858 values.opacity = Some(value);
3859 }
3860 (crate::SignalProperty::TranslateX, crate::SignalValue::Float(value)) => {
3861 values.translate_x = Some(value);
3862 }
3863 (
3864 crate::SignalProperty::TranslateXOverride,
3865 crate::SignalValue::OptionalFloat(value),
3866 ) => {
3867 values.translate_x = value;
3868 }
3869 (crate::SignalProperty::TranslateY, crate::SignalValue::Float(value)) => {
3870 values.translate_y = Some(value);
3871 }
3872 (
3873 crate::SignalProperty::TranslateYOverride,
3874 crate::SignalValue::OptionalFloat(value),
3875 ) => {
3876 values.translate_y = value;
3877 }
3878 (crate::SignalProperty::Rotate, crate::SignalValue::Float(value)) => {
3879 values.rotate = Some(value);
3880 }
3881 (crate::SignalProperty::RotateOverride, crate::SignalValue::OptionalFloat(value)) => {
3882 values.rotate = value;
3883 }
3884 (crate::SignalProperty::ScaleX, crate::SignalValue::Float(value)) => {
3885 values.scale_x = Some(value);
3886 }
3887 (crate::SignalProperty::ScaleXOverride, crate::SignalValue::OptionalFloat(value)) => {
3888 values.scale_x = value;
3889 }
3890 (crate::SignalProperty::ScaleY, crate::SignalValue::Float(value)) => {
3891 values.scale_y = Some(value);
3892 }
3893 (crate::SignalProperty::ScaleYOverride, crate::SignalValue::OptionalFloat(value)) => {
3894 values.scale_y = value;
3895 }
3896 (crate::SignalProperty::Width, crate::SignalValue::Float(value)) => {
3897 values.width = Some(value);
3898 }
3899 (crate::SignalProperty::WidthOverride, crate::SignalValue::OptionalFloat(value)) => {
3900 values.width_override = value;
3901 }
3902 (crate::SignalProperty::Height, crate::SignalValue::Float(value)) => {
3903 values.height = Some(value);
3904 }
3905 (crate::SignalProperty::HeightOverride, crate::SignalValue::OptionalFloat(value)) => {
3906 values.height_override = value;
3907 }
3908 (crate::SignalProperty::Background, crate::SignalValue::Color(value)) => {
3909 values.background = Some(value);
3910 }
3911 (crate::SignalProperty::TextColor, crate::SignalValue::Color(value)) => {
3912 values.text_color = Some(value);
3913 }
3914 (crate::SignalProperty::BorderColor, crate::SignalValue::Color(value)) => {
3915 values.border_color = Some(value);
3916 }
3917 _ => debug_assert!(false, "validated signal binding changed type"),
3918 }
3919 }
3920 values
3921}
3922
3923fn apply_canvas_signal_transform(
3924 mut motion: NodeMotionValues,
3925 signals: &NodeSignalValues,
3926) -> NodeMotionValues {
3927 motion.rotate = signals.rotate.or(motion.rotate);
3928 motion.scale_x = signals.scale_x.or(motion.scale_x);
3929 motion.scale_y = signals.scale_y.or(motion.scale_y);
3930 motion
3931}
3932
3933fn apply_signal_style(style: &mut StyleProperties, values: &NodeSignalValues) {
3934 if let Some(width) = values.width {
3935 style.width = Some(Length::Pixels(width.max(0.0)).into());
3936 }
3937 if let Some(width) = values.width_override {
3938 style.width = Some(Length::Pixels(width.max(0.0)).into());
3939 style.flex_basis = None;
3940 style.flex_grow = Some(false);
3941 style.flex_grow_weight = None;
3942 style.flex_shrink = Some(false);
3943 }
3944 if let Some(height) = values.height {
3945 style.height = Some(Length::Pixels(height.max(0.0)).into());
3946 }
3947 if let Some(height) = values.height_override {
3948 style.height = Some(Length::Pixels(height.max(0.0)).into());
3949 style.flex_basis = None;
3950 style.flex_grow = Some(false);
3951 style.flex_grow_weight = None;
3952 style.flex_shrink = Some(false);
3953 }
3954 if let Some(background) = &values.background {
3955 style.background = Some(background.clone());
3956 }
3957 if let Some(text_color) = &values.text_color {
3958 style.text_color = Some(text_color.clone());
3959 }
3960 if let Some(border_color) = &values.border_color {
3961 style.border_color = Some(border_color.clone());
3962 }
3963}
3964
3965fn node_motion(values: &BTreeMap<MotionKey, f64>, path: &str) -> NodeMotionValues {
3966 let value = |property| values.get(&MotionKey::for_node(path, property)).copied();
3967 NodeMotionValues {
3968 opacity: value(MotionProperty::Opacity),
3969 translate_x: value(MotionProperty::TranslateX),
3970 translate_y: value(MotionProperty::TranslateY),
3971 rotate: value(MotionProperty::Rotate),
3972 scale_x: value(MotionProperty::ScaleX),
3973 scale_y: value(MotionProperty::ScaleY),
3974 skew_x: value(MotionProperty::SkewX),
3975 skew_y: value(MotionProperty::SkewY),
3976 width: value(MotionProperty::Width),
3977 height: value(MotionProperty::Height),
3978 clip_height: value(MotionProperty::ClipHeight),
3979 path_progress: value(MotionProperty::PathProgress),
3980 }
3981}
3982
3983fn apply_motion_dimensions(style: &mut StyleProperties, values: NodeMotionValues) {
3984 if let Some(width) = values.width {
3985 style.width = Some(Length::Pixels(width.max(0.0)).into());
3986 }
3987 if let Some(height) = values.height {
3988 style.height = Some(Length::Pixels(height.max(0.0)).into());
3989 }
3990 if let Some(height) = values.clip_height {
3991 style.height = Some(Length::Pixels(height.max(0.0)).into());
3992 }
3993}
3994
3995fn translated(element: AnyElement, x: Option<f64>, y: Option<f64>) -> AnyElement {
3996 let offset = point(
3997 px(f64_to_f32(x.unwrap_or(0.0))),
3998 px(f64_to_f32(y.unwrap_or(0.0))),
3999 );
4000 if offset == Point::default() {
4001 element
4002 } else {
4003 TranslatedElement {
4004 child: Some(element),
4005 offset,
4006 }
4007 .into_any_element()
4008 }
4009}
4010
4011pub(crate) fn f64_to_f32(value: f64) -> f32 {
4012 value.to_string().parse().unwrap_or_else(|_| {
4013 if value.is_sign_negative() {
4014 f32::MIN
4015 } else {
4016 f32::MAX
4017 }
4018 })
4019}
4020
4021struct TranslatedElement {
4022 child: Option<AnyElement>,
4023 offset: Point<Pixels>,
4024}
4025
4026#[derive(Clone, Debug)]
4027struct ActiveTextSelection {
4028 owner: String,
4029 node: Option<NodeId>,
4030 text: String,
4031 range: Option<(usize, usize)>,
4032 bounds: Bounds<Pixels>,
4033}
4034
4035#[derive(Clone, Debug, Default)]
4037pub(crate) struct TextSelectionRegistry {
4038 active: Rc<RefCell<Option<ActiveTextSelection>>>,
4039}
4040
4041impl TextSelectionRegistry {
4042 fn begin(&self, owner: String, node: Option<NodeId>, text: String, bounds: Bounds<Pixels>) {
4043 self.active.borrow_mut().replace(ActiveTextSelection {
4044 owner,
4045 node,
4046 text,
4047 range: None,
4048 bounds,
4049 });
4050 }
4051
4052 fn update(&self, owner: &str, text: &str, bounds: Bounds<Pixels>, range: (usize, usize)) {
4053 let mut active = self.active.borrow_mut();
4054 let Some(selection) = active.as_mut().filter(|selection| selection.owner == owner) else {
4055 return;
4056 };
4057 text.clone_into(&mut selection.text);
4058 selection.range = (range.1 > range.0).then_some(range);
4059 selection.bounds = bounds;
4060 }
4061
4062 fn range(&self, owner: &str, text: &str) -> Option<(usize, usize)> {
4063 let mut active = self.active.borrow_mut();
4064 let selection = active.as_ref()?;
4065 if selection.owner != owner {
4066 return None;
4067 }
4068 if selection.text != text {
4069 active.take();
4070 return None;
4071 }
4072 selection.range
4073 }
4074
4075 pub(crate) fn selected_text(&self) -> Option<String> {
4076 let active = self.active.borrow();
4077 let selection = active.as_ref()?;
4078 let (start, end) = selection.range?;
4079 selection.text.get(start..end).map(ToOwned::to_owned)
4080 }
4081
4082 pub(crate) fn clear_outside(&self, position: Point<Pixels>) -> bool {
4083 let mut active = self.active.borrow_mut();
4084 if active
4085 .as_ref()
4086 .is_some_and(|selection| !selection.bounds.contains(&position))
4087 {
4088 active.take();
4089 true
4090 } else {
4091 false
4092 }
4093 }
4094
4095 pub(crate) fn retain(&self, tree: &RetainedUiTree) {
4096 let mut active = self.active.borrow_mut();
4097 if active
4098 .as_ref()
4099 .and_then(|selection| selection.node)
4100 .is_some_and(|node| tree.node(node).is_none())
4101 {
4102 active.take();
4103 }
4104 }
4105}
4106
4107struct SelectableText {
4111 id: ElementId,
4112 owner: String,
4113 node: Option<NodeId>,
4114 text: StyledText,
4115 highlight: Rgba8,
4116 selection: TextSelectionRegistry,
4117 host_focus: Option<FocusHandle>,
4118}
4119
4120#[derive(Default)]
4121struct SelectableTextState {
4122 anchor: Rc<std::cell::Cell<Option<usize>>>,
4123}
4124
4125impl SelectableText {
4126 fn new(
4127 owner: String,
4128 node: Option<NodeId>,
4129 text: &str,
4130 highlight: Rgba8,
4131 selection: TextSelectionRegistry,
4132 host_focus: Option<FocusHandle>,
4133 ) -> Self {
4134 Self {
4135 id: ElementId::Name(SharedString::from(format!("{owner}/selectable"))),
4136 owner,
4137 node,
4138 text: StyledText::new(text.to_owned()),
4139 highlight,
4140 selection,
4141 host_focus,
4142 }
4143 }
4144}
4145
4146pub(crate) fn selectable_text_element(
4147 owner: String,
4148 text: &str,
4149 highlight: Rgba8,
4150 selection: TextSelectionRegistry,
4151 host_focus: Option<FocusHandle>,
4152) -> AnyElement {
4153 SelectableText::new(owner, None, text, highlight, selection, host_focus).into_any_element()
4154}
4155
4156fn selection_rects(layout: &gpui::TextLayout, start: usize, end: usize) -> Vec<Bounds<Pixels>> {
4159 let text = layout.text();
4160 let Some(selected) = text.get(start..end) else {
4161 return Vec::new();
4162 };
4163 let mut rows: Vec<(Pixels, Pixels, Pixels)> = Vec::new();
4164 for index in selected
4165 .char_indices()
4166 .map(|(offset, _)| start + offset)
4167 .chain(std::iter::once(end))
4168 {
4169 let Some(position) = layout.position_for_index(index) else {
4170 continue;
4171 };
4172 if let Some((_, left, right)) = rows.iter_mut().find(|(y, _, _)| *y == position.y) {
4173 *left = (*left).min(position.x);
4174 *right = (*right).max(position.x);
4175 } else {
4176 rows.push((position.y, position.x, position.x));
4177 }
4178 }
4179 let line_height = layout.line_height();
4180 rows.into_iter()
4181 .filter(|(_, left, right)| right > left)
4182 .map(|(y, left, right)| Bounds::from_corners(point(left, y), point(right, y + line_height)))
4183 .collect()
4184}
4185
4186impl Element for SelectableText {
4187 type RequestLayoutState = ();
4188 type PrepaintState = gpui::Hitbox;
4189
4190 fn id(&self) -> Option<ElementId> {
4191 Some(self.id.clone())
4192 }
4193
4194 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
4195 None
4196 }
4197
4198 fn request_layout(
4199 &mut self,
4200 _id: Option<&GlobalElementId>,
4201 inspector_id: Option<&InspectorElementId>,
4202 window: &mut Window,
4203 cx: &mut App,
4204 ) -> (LayoutId, Self::RequestLayoutState) {
4205 self.text.request_layout(None, inspector_id, window, cx)
4206 }
4207
4208 #[allow(clippy::too_many_lines)]
4209 fn prepaint(
4210 &mut self,
4211 _id: Option<&GlobalElementId>,
4212 inspector_id: Option<&InspectorElementId>,
4213 bounds: Bounds<Pixels>,
4214 state: &mut Self::RequestLayoutState,
4215 window: &mut Window,
4216 cx: &mut App,
4217 ) -> gpui::Hitbox {
4218 self.text
4219 .prepaint(None, inspector_id, bounds, state, window, cx);
4220 window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)
4221 }
4222
4223 fn paint(
4224 &mut self,
4225 global_id: Option<&GlobalElementId>,
4226 inspector_id: Option<&InspectorElementId>,
4227 bounds: Bounds<Pixels>,
4228 state: &mut Self::RequestLayoutState,
4229 hitbox: &mut Self::PrepaintState,
4230 window: &mut Window,
4231 cx: &mut App,
4232 ) {
4233 let Some(global_id) = global_id else {
4234 self.text
4235 .paint(None, inspector_id, bounds, state, &mut (), window, cx);
4236 return;
4237 };
4238 let layout = self.text.layout().clone();
4239 let rendered_text = layout.text();
4240 let highlight = rgba(self.highlight.as_rgba_hex());
4241 let anchor = window.with_element_state::<SelectableTextState, _>(global_id, |prev, _| {
4242 let prev = prev.unwrap_or_default();
4243 (prev.anchor.clone(), prev)
4244 });
4245
4246 if let Some((start, end)) = self.selection.range(&self.owner, &rendered_text) {
4248 for rect in selection_rects(&layout, start, end) {
4249 window.paint_quad(gpui::fill(rect, highlight));
4250 }
4251 }
4252 self.text
4253 .paint(None, inspector_id, bounds, state, &mut (), window, cx);
4254 window.set_cursor_style(CursorStyle::IBeam, hitbox);
4255
4256 let clamp = |index: Result<usize, usize>| match index {
4257 Ok(ix) | Err(ix) => ix,
4258 };
4259
4260 {
4261 let anchor = anchor.clone();
4262 let layout = layout.clone();
4263 let hitbox = hitbox.clone();
4264 let owner = self.owner.clone();
4265 let node = self.node;
4266 let selection = self.selection.clone();
4267 let host_focus = self.host_focus.clone();
4268 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
4269 if phase.bubble() && event.button == MouseButton::Left && hitbox.is_hovered(window)
4270 {
4271 let index = clamp(layout.index_for_position(event.position));
4272 anchor.set(Some(index));
4273 selection.begin(owner.clone(), node, layout.text(), hitbox.bounds);
4274 if let Some(focus) = &host_focus {
4275 focus.focus(window, cx);
4276 }
4277 window.refresh();
4278 }
4279 });
4280 }
4281 {
4282 let anchor = anchor.clone();
4283 let layout = layout.clone();
4284 let owner = self.owner.clone();
4285 let selection = self.selection.clone();
4286 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
4287 if phase.bubble()
4288 && event.pressed_button == Some(MouseButton::Left)
4289 && let Some(from) = anchor.get()
4290 {
4291 let to = clamp(layout.index_for_position(event.position));
4292 let range = (from.min(to), from.max(to));
4293 selection.update(&owner, &layout.text(), layout.bounds(), range);
4294 if range.1 > range.0 {
4295 cx.stop_propagation();
4296 }
4297 window.refresh();
4298 }
4299 });
4300 }
4301 {
4302 let owner = self.owner.clone();
4303 let selection = self.selection.clone();
4304 window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
4305 if phase.bubble() && event.button == MouseButton::Left && anchor.take().is_some() {
4306 if selection.range(&owner, &layout.text()).is_some() {
4307 cx.stop_propagation();
4308 }
4309 window.refresh();
4310 }
4311 });
4312 }
4313 }
4314}
4315
4316impl IntoElement for SelectableText {
4317 type Element = Self;
4318
4319 fn into_element(self) -> Self::Element {
4320 self
4321 }
4322}
4323
4324fn layout_motion_spec(node: &UiNode, now: Instant) -> Option<LayoutMotionRenderSpec> {
4325 let duration_ms = match node.attributes().get("layout_motion_duration_ms") {
4326 Some(UiValue::Integer(value)) => u64::try_from(*value).ok()?,
4327 _ => return None,
4328 };
4329 let easing = match node.attributes().get("layout_motion_easing") {
4330 Some(UiValue::String(value)) => crate::MotionEasing::parse(value).ok()?,
4331 _ => crate::MotionEasing::EaseOut,
4332 };
4333 let shared = match (
4334 node.attributes().get("shared_layout_group"),
4335 node.attributes().get("shared_layout_id"),
4336 ) {
4337 (Some(UiValue::String(group)), Some(UiValue::String(id))) => {
4338 Some((group.clone(), id.clone()))
4339 }
4340 _ => None,
4341 };
4342 Some(LayoutMotionRenderSpec {
4343 now,
4344 duration: std::time::Duration::from_millis(duration_ms),
4345 easing,
4346 shared,
4347 })
4348}
4349
4350struct GeometryTrackedElement {
4351 child: Option<AnyElement>,
4352 node: NodeId,
4353 registry: crate::GeometryRegistry,
4354 translate_x: f64,
4355 translate_y: f64,
4356 layout_motion: Option<LayoutMotionRenderSpec>,
4357 progress_motions: Vec<crate::MotionProgressBinding>,
4358 scroll_handles: Vec<ScrollHandle>,
4359 focus_handle: Option<FocusHandle>,
4360 now: Instant,
4361 motion_preference: crate::MotionPreference,
4362}
4363
4364#[derive(Clone, Debug)]
4365struct LayoutMotionRenderSpec {
4366 now: Instant,
4367 duration: std::time::Duration,
4368 easing: crate::MotionEasing,
4369 shared: Option<(String, String)>,
4370}
4371
4372impl Element for GeometryTrackedElement {
4373 type RequestLayoutState = AnyElement;
4374 type PrepaintState = ();
4375
4376 fn id(&self) -> Option<ElementId> {
4377 None
4378 }
4379
4380 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
4381 None
4382 }
4383
4384 fn request_layout(
4385 &mut self,
4386 _id: Option<&GlobalElementId>,
4387 _inspector_id: Option<&InspectorElementId>,
4388 window: &mut Window,
4389 cx: &mut App,
4390 ) -> (LayoutId, Self::RequestLayoutState) {
4391 let mut child = self.child.take().expect("tracked element renders once");
4392 let layout = child.request_layout(window, cx);
4393 (layout, child)
4394 }
4395
4396 #[allow(clippy::too_many_lines)]
4397 fn prepaint(
4398 &mut self,
4399 _id: Option<&GlobalElementId>,
4400 _inspector_id: Option<&InspectorElementId>,
4401 bounds: Bounds<Pixels>,
4402 child: &mut Self::RequestLayoutState,
4403 window: &mut Window,
4404 cx: &mut App,
4405 ) {
4406 let base_offset = window.pixel_snap_point(window.element_offset());
4407 self.registry.record_element_offset(
4408 self.node,
4409 (f64::from(base_offset.x), f64::from(base_offset.y)),
4410 );
4411 if let Ok(layout) = crate::GeometryBounds::new(
4412 f64::from(bounds.origin.x),
4413 f64::from(bounds.origin.y),
4414 f64::from(bounds.size.width),
4415 f64::from(bounds.size.height),
4416 ) {
4417 let viewport = window.viewport_size();
4418 for binding in &self.progress_motions {
4419 if binding.driver == crate::MotionProgressDriver::Focus {
4420 self.registry.set_motion_trigger(
4421 self.node,
4422 crate::MotionProgressDriver::Focus,
4423 self.focus_handle
4424 .as_ref()
4425 .is_some_and(|handle| handle.contains_focused(window, cx)),
4426 );
4427 }
4428 if matches!(
4429 binding.driver,
4430 crate::MotionProgressDriver::Hover
4431 | crate::MotionProgressDriver::Press
4432 | crate::MotionProgressDriver::Focus
4433 ) {
4434 let sample = self.registry.sample_motion_trigger(
4435 self.node,
4436 binding,
4437 self.motion_preference,
4438 self.now,
4439 );
4440 let changed = self.registry.update_motion_progress(
4441 self.node,
4442 binding.property(),
4443 sample.value,
4444 );
4445 if changed || sample.active {
4446 window.request_animation_frame();
4447 }
4448 continue;
4449 }
4450 let progress = match binding.driver {
4451 crate::MotionProgressDriver::InView => {
4452 let visible_width = (layout.x + layout.width)
4453 .min(f64::from(viewport.width))
4454 .max(0.0)
4455 - layout.x.max(0.0);
4456 let visible_height = (layout.y + layout.height)
4457 .min(f64::from(viewport.height))
4458 .max(0.0)
4459 - layout.y.max(0.0);
4460 if layout.width <= f64::EPSILON || layout.height <= f64::EPSILON {
4461 0.0
4462 } else {
4463 (visible_width.max(0.0) * visible_height.max(0.0)
4464 / (layout.width * layout.height))
4465 .clamp(0.0, 1.0)
4466 }
4467 }
4468 crate::MotionProgressDriver::Viewport => ((f64::from(viewport.height)
4469 - layout.y)
4470 / (f64::from(viewport.height) + layout.height).max(1.0))
4471 .clamp(0.0, 1.0),
4472 crate::MotionProgressDriver::ScrollX => {
4473 self.scroll_handles.first().map_or(0.0, |handle| {
4474 let maximum = f64::from(handle.max_offset().x).abs();
4475 if maximum <= f64::EPSILON {
4476 0.0
4477 } else {
4478 (-f64::from(handle.offset().x) / maximum).clamp(0.0, 1.0)
4479 }
4480 })
4481 }
4482 crate::MotionProgressDriver::ScrollY => {
4483 self.scroll_handles.first().map_or(0.0, |handle| {
4484 let maximum = f64::from(handle.max_offset().y).abs();
4485 if maximum <= f64::EPSILON {
4486 0.0
4487 } else {
4488 (-f64::from(handle.offset().y) / maximum).clamp(0.0, 1.0)
4489 }
4490 })
4491 }
4492 crate::MotionProgressDriver::Hover
4493 | crate::MotionProgressDriver::Press
4494 | crate::MotionProgressDriver::Focus => unreachable!(),
4495 };
4496 let value = crate::motion::sample_progress_source(&binding.source, progress);
4497 if self
4498 .registry
4499 .update_motion_progress(self.node, binding.property(), value)
4500 {
4501 window.request_animation_frame();
4502 }
4503 }
4504 let sample = self.layout_motion.as_ref().map_or_else(
4505 crate::geometry::LayoutMotionSample::default,
4506 |spec| {
4507 self.registry.sample_layout_motion(
4508 self.node,
4509 layout,
4510 crate::geometry::LayoutMotionRequest {
4511 shared: spec
4512 .shared
4513 .as_ref()
4514 .map(|(group, id)| (group.as_str(), id.as_str())),
4515 duration: spec.duration,
4516 easing: spec.easing,
4517 preference: self.motion_preference,
4518 now: spec.now,
4519 },
4520 )
4521 },
4522 );
4523 if sample.active {
4524 window.request_animation_frame();
4525 }
4526 self.registry.update(
4527 self.node,
4528 crate::ElementGeometry {
4529 layout,
4530 visual: crate::GeometryBounds {
4531 x: layout.x + self.translate_x + sample.offset_x,
4532 y: layout.y + self.translate_y + sample.offset_y,
4533 width: layout.width * sample.scale_x,
4534 height: layout.height * sample.scale_y,
4535 },
4536 clip: None,
4537 },
4538 );
4539 let offset = point(
4540 px(f64_to_f32(sample.offset_x)),
4541 px(f64_to_f32(sample.offset_y)),
4542 );
4543 if offset != Point::default() {
4544 window.with_element_offset(offset, |window| child.prepaint(window, cx));
4545 return;
4546 }
4547 }
4548 child.prepaint(window, cx);
4549 }
4550
4551 fn paint(
4552 &mut self,
4553 _id: Option<&GlobalElementId>,
4554 _inspector_id: Option<&InspectorElementId>,
4555 _bounds: Bounds<Pixels>,
4556 child: &mut Self::RequestLayoutState,
4557 _prepaint: &mut Self::PrepaintState,
4558 window: &mut Window,
4559 cx: &mut App,
4560 ) {
4561 child.paint(window, cx);
4562 }
4563}
4564
4565impl IntoElement for GeometryTrackedElement {
4566 type Element = Self;
4567
4568 fn into_element(self) -> Self::Element {
4569 self
4570 }
4571}
4572
4573impl Element for TranslatedElement {
4574 type RequestLayoutState = AnyElement;
4575 type PrepaintState = ();
4576
4577 fn id(&self) -> Option<ElementId> {
4578 None
4579 }
4580
4581 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
4582 None
4583 }
4584
4585 fn request_layout(
4586 &mut self,
4587 _id: Option<&GlobalElementId>,
4588 _inspector_id: Option<&InspectorElementId>,
4589 window: &mut Window,
4590 cx: &mut App,
4591 ) -> (LayoutId, Self::RequestLayoutState) {
4592 let mut child = self.child.take().expect("translated element renders once");
4593 let layout = child.request_layout(window, cx);
4594 (layout, child)
4595 }
4596
4597 fn prepaint(
4598 &mut self,
4599 _id: Option<&GlobalElementId>,
4600 _inspector_id: Option<&InspectorElementId>,
4601 _bounds: Bounds<Pixels>,
4602 child: &mut Self::RequestLayoutState,
4603 window: &mut Window,
4604 cx: &mut App,
4605 ) {
4606 window.with_element_offset(self.offset, |window| child.prepaint(window, cx));
4607 }
4608
4609 fn paint(
4610 &mut self,
4611 _id: Option<&GlobalElementId>,
4612 _inspector_id: Option<&InspectorElementId>,
4613 _bounds: Bounds<Pixels>,
4614 child: &mut Self::RequestLayoutState,
4615 _prepaint: &mut Self::PrepaintState,
4616 window: &mut Window,
4617 cx: &mut App,
4618 ) {
4619 child.paint(window, cx);
4620 }
4621}
4622
4623impl IntoElement for TranslatedElement {
4624 type Element = Self;
4625
4626 fn into_element(self) -> Self::Element {
4627 self
4628 }
4629}
4630
4631fn semantic_color(colors: &impl ColorResolver, token: &str, fallback: u32) -> Rgba8 {
4632 colors
4633 .resolve(&ColorValue::Token(token.to_owned()))
4634 .unwrap_or_else(|| Rgba8::from_rgb_hex(fallback))
4635}
4636
4637#[derive(Clone, Copy, Default)]
4638struct PseudoPaint {
4639 background: Option<Rgba8>,
4640 border: Option<Rgba8>,
4641 text: Option<Rgba8>,
4642 opacity: Option<f32>,
4643}
4644
4645fn pseudo_paint(properties: Option<&StyleProperties>, colors: &impl ColorResolver) -> PseudoPaint {
4646 let Some(properties) = properties else {
4647 return PseudoPaint::default();
4648 };
4649 PseudoPaint {
4650 background: properties
4651 .background
4652 .as_ref()
4653 .and_then(|color| colors.resolve(color)),
4654 border: properties
4655 .border_color
4656 .as_ref()
4657 .and_then(|color| colors.resolve(color)),
4658 text: properties
4659 .text_color
4660 .as_ref()
4661 .and_then(|color| colors.resolve(color)),
4662 opacity: properties
4663 .opacity
4664 .map(|opacity| f64_to_f32(opacity.clamp(0.0, 1.0))),
4665 }
4666}
4667
4668fn apply_pseudo_paint(
4669 mut style: gpui::StyleRefinement,
4670 paint: PseudoPaint,
4671) -> gpui::StyleRefinement {
4672 if let Some(color) = paint.background {
4673 style = style.bg(rgba(color.as_rgba_hex()));
4674 }
4675 if let Some(color) = paint.border {
4676 style.style().border_color = Some(rgba(color.as_rgba_hex()).into());
4677 }
4678 if let Some(color) = paint.text {
4679 style = style.text_color(rgba(color.as_rgba_hex()));
4680 }
4681 if let Some(opacity) = paint.opacity {
4682 style = style.opacity(opacity);
4683 }
4684 style
4685}
4686
4687fn apply_pseudo_backgrounds(
4688 mut element: Stateful<Div>,
4689 style: &Style,
4690 colors: &impl ColorResolver,
4691) -> Stateful<Div> {
4692 let hover = pseudo_paint(style.hover.as_ref(), colors);
4693 let active = pseudo_paint(style.active.as_ref(), colors);
4694 let mut focus = pseudo_paint(style.focus.as_ref(), colors);
4695 if focus.border.is_none() {
4696 focus.border = Some(semantic_color(colors, "focus_ring", 0x003b_82f6));
4697 }
4698 element = element.hover(move |style| apply_pseudo_paint(style, hover));
4699 element = element.active(move |style| apply_pseudo_paint(style, active));
4700 element = element.focus(move |style| apply_pseudo_paint(style, focus));
4701 element
4702}
4703
4704fn is_disabled(node: &UiNode) -> bool {
4705 node.attributes().get("disabled") == Some(&UiValue::Bool(true))
4706}
4707
4708fn apply_style(
4709 element: Div,
4710 style: &StyleProperties,
4711 colors: &impl ColorResolver,
4712 direction: TextDirection,
4713) -> Div {
4714 let mut style = style.clone();
4715 resolve_typography_role(&mut style, colors);
4716 resolve_style_lengths(&mut style, colors);
4717 let element = apply_layout(element, &style, direction);
4718 let element = apply_spacing(element, &style, direction);
4719 apply_paint_and_text(element, &style, colors, direction)
4720}
4721
4722fn resolve_typography_role(style: &mut StyleProperties, resolver: &impl ColorResolver) {
4723 let Some(role) = style.typography.as_deref() else {
4724 return;
4725 };
4726 let Some(typography) = resolver.resolve_typography(role) else {
4727 return;
4728 };
4729 if style.font_family.is_none() {
4730 style.font_family = typography.family;
4731 }
4732 if style.font_fallbacks.is_none() && !typography.fallbacks.is_empty() {
4733 style.font_fallbacks = Some(typography.fallbacks);
4734 }
4735 if style.font_size.is_none() {
4736 style.font_size = Some(typography.size);
4737 }
4738 if style.line_height.is_none() {
4739 style.line_height = Some(typography.line_height);
4740 }
4741 if style.font_weight.is_none() {
4742 style.font_weight = Some(typography.weight);
4743 }
4744}
4745
4746fn resolve_style_lengths(style: &mut StyleProperties, resolver: &impl ColorResolver) {
4747 let resolve_definite = |value: &mut Option<Length>| {
4748 *value = (*value).and_then(|value| resolver.resolve_length(value));
4749 };
4750 let resolve_layout = |value: &mut Option<LayoutLength>| {
4751 *value = (*value).and_then(|value| match value {
4752 LayoutLength::Definite(value) => {
4753 resolver.resolve_length(value).map(LayoutLength::Definite)
4754 }
4755 LayoutLength::Signed(_) | LayoutLength::Auto => Some(value),
4756 });
4757 };
4758 for value in [
4759 &mut style.width,
4760 &mut style.height,
4761 &mut style.min_width,
4762 &mut style.max_width,
4763 &mut style.min_height,
4764 &mut style.max_height,
4765 &mut style.flex_basis,
4766 ] {
4767 resolve_layout(value);
4768 }
4769 for value in [
4770 &mut style.margin.top,
4771 &mut style.margin.right,
4772 &mut style.margin.bottom,
4773 &mut style.margin.left,
4774 &mut style.margin.start,
4775 &mut style.margin.end,
4776 &mut style.top,
4777 &mut style.right,
4778 &mut style.bottom,
4779 &mut style.left,
4780 &mut style.inset_start,
4781 &mut style.inset_end,
4782 ] {
4783 resolve_layout(value);
4784 }
4785 for value in [
4786 &mut style.gap,
4787 &mut style.padding.top,
4788 &mut style.padding.right,
4789 &mut style.padding.bottom,
4790 &mut style.padding.left,
4791 &mut style.padding.start,
4792 &mut style.padding.end,
4793 &mut style.border_widths.top,
4794 &mut style.border_widths.right,
4795 &mut style.border_widths.bottom,
4796 &mut style.border_widths.left,
4797 &mut style.border_widths.start,
4798 &mut style.border_widths.end,
4799 &mut style.radii.top_left,
4800 &mut style.radii.top_right,
4801 &mut style.radii.bottom_right,
4802 &mut style.radii.bottom_left,
4803 &mut style.font_size,
4804 &mut style.line_height,
4805 ] {
4806 resolve_definite(value);
4807 }
4808}
4809
4810pub(crate) fn apply_style_override(
4811 element: Div,
4812 style: &Style,
4813 colors: &impl ColorResolver,
4814 direction: TextDirection,
4815) -> Div {
4816 apply_style(
4817 element,
4818 &style.resolve(&InteractionState::default()),
4819 colors,
4820 direction,
4821 )
4822}
4823
4824fn apply_layout(element: Div, style: &StyleProperties, text_direction: TextDirection) -> Div {
4825 let element = apply_display_and_position(element, style, text_direction);
4826 let element = apply_flex_alignment(element, style, text_direction);
4827 apply_layout_dimensions(element, style)
4828}
4829
4830fn apply_display_and_position(
4831 mut element: Div,
4832 style: &StyleProperties,
4833 direction: TextDirection,
4834) -> Div {
4835 if let Some(display) = style.display {
4836 element = match display {
4837 DisplayMode::Block => element.block(),
4838 DisplayMode::Flex => element.flex(),
4839 DisplayMode::Grid => element.grid(),
4840 DisplayMode::None => element.hidden(),
4841 };
4842 }
4843 if let Some(position) = style.position {
4844 element = match position {
4845 PositionMode::Relative => element.relative(),
4846 PositionMode::Absolute => element.absolute(),
4847 };
4848 }
4849 if let Some(value) = style.top {
4850 element = inset_top(element, value);
4851 }
4852 let (left, right) = logical_horizontal_edges(
4853 style.left,
4854 style.right,
4855 style.inset_start,
4856 style.inset_end,
4857 direction,
4858 );
4859 if let Some(value) = right {
4860 element = inset_right(element, value);
4861 }
4862 if let Some(value) = style.bottom {
4863 element = inset_bottom(element, value);
4864 }
4865 if let Some(value) = left {
4866 element = inset_left(element, value);
4867 }
4868 if matches!(style.overflow_x, Some(OverflowMode::Hidden))
4869 || matches!(style.overflow_y, Some(OverflowMode::Hidden))
4870 {
4871 element = element.overflow_hidden();
4872 }
4873 element
4874}
4875
4876fn apply_flex_alignment(
4877 mut element: Div,
4878 style: &StyleProperties,
4879 text_direction: TextDirection,
4880) -> Div {
4881 if let Some(direction) = style.direction {
4882 element = element.flex();
4883 element = match (direction, text_direction) {
4884 (FlexDirection::Row, TextDirection::LeftToRight) => element.flex_row(),
4885 (FlexDirection::Row, TextDirection::RightToLeft) => element.flex_row_reverse(),
4886 (FlexDirection::Column, _) => element.flex_col(),
4887 };
4888 }
4889 if let Some(wrap) = style.flex_wrap {
4890 element = match wrap {
4891 FlexWrapMode::NoWrap => element.flex_nowrap(),
4892 FlexWrapMode::Wrap => element.flex_wrap(),
4893 FlexWrapMode::WrapReverse => element.flex_wrap_reverse(),
4894 };
4895 }
4896 if let Some(align) = style.align_self {
4897 let align = if text_direction == TextDirection::RightToLeft {
4898 match align {
4899 Align::Start => Align::End,
4900 Align::End => Align::Start,
4901 other => other,
4902 }
4903 } else {
4904 align
4905 };
4906 element.style().align_self = Some(match align {
4907 Align::Start => GpuiAlignSelf::Start,
4908 Align::Center => GpuiAlignSelf::Center,
4909 Align::End => GpuiAlignSelf::End,
4910 Align::Stretch => GpuiAlignSelf::Stretch,
4911 });
4912 }
4913 if let Some(align) = style.align {
4914 let align = if style.direction == Some(FlexDirection::Column)
4915 && text_direction == TextDirection::RightToLeft
4916 {
4917 match align {
4918 Align::Start => Align::End,
4919 Align::End => Align::Start,
4920 other => other,
4921 }
4922 } else {
4923 align
4924 };
4925 element = match align {
4926 Align::Start => element.items_start(),
4927 Align::Center => element.items_center(),
4928 Align::End => element.items_end(),
4929 Align::Stretch => element,
4930 };
4931 }
4932 if let Some(justify) = style.justify {
4933 element = match justify {
4934 Justify::Start => element.justify_start(),
4935 Justify::Center => element.justify_center(),
4936 Justify::End => element.justify_end(),
4937 Justify::Between => element.justify_between(),
4938 Justify::Around => element.justify_around(),
4939 };
4940 }
4941 element
4942}
4943
4944fn apply_layout_dimensions(mut element: Div, style: &StyleProperties) -> Div {
4945 if let Some(value) = style.width {
4946 element = width(element, value);
4947 }
4948 if let Some(value) = style.height {
4949 element = height(element, value);
4950 }
4951 if let Some(value) = style.min_width {
4952 element = min_width(element, value);
4953 }
4954 if let Some(value) = style.max_width {
4955 element = max_width(element, value);
4956 }
4957 if let Some(value) = style.min_height {
4958 element = min_height(element, value);
4959 }
4960 if let Some(value) = style.max_height {
4961 element = max_height(element, value);
4962 }
4963 if let Some(value) = style.gap {
4964 element = gap(element, value);
4965 }
4966 if let Some(weight) = style.flex_grow_weight {
4967 element.style().flex_grow = Some(to_f32(weight));
4968 } else if style.flex_grow == Some(true) {
4969 element = element.flex_grow_1();
4970 }
4971 if let Some(shrink) = style.flex_shrink {
4972 element = if shrink {
4973 element.flex_shrink_1()
4974 } else {
4975 element.flex_shrink_0()
4976 };
4977 }
4978 if let Some(value) = style.flex_basis {
4979 element = flex_basis(element, value);
4980 }
4981 if let Some(columns) = style.grid_columns {
4982 element = element.grid_cols(columns);
4983 }
4984 if let Some(rows) = style.grid_rows {
4985 element = element.grid_rows(rows);
4986 }
4987 if let Some(span) = style.column_span {
4988 element = element.col_span(span);
4989 }
4990 if let Some(span) = style.row_span {
4991 element = element.row_span(span);
4992 }
4993 element
4994}
4995
4996fn apply_spacing(mut element: Div, style: &StyleProperties, direction: TextDirection) -> Div {
4997 if let Some(value) = style.padding.top {
4998 element = padding_top(element, value);
4999 }
5000 let (padding_left_value, padding_right_value) = logical_horizontal_edges(
5001 style.padding.left,
5002 style.padding.right,
5003 style.padding.start,
5004 style.padding.end,
5005 direction,
5006 );
5007 if let Some(value) = padding_right_value {
5008 element = padding_right(element, value);
5009 }
5010 if let Some(value) = style.padding.bottom {
5011 element = padding_bottom(element, value);
5012 }
5013 if let Some(value) = padding_left_value {
5014 element = padding_left(element, value);
5015 }
5016 if let Some(value) = style.margin.top {
5017 element = margin_top(element, value);
5018 }
5019 let (margin_left_value, margin_right_value) = logical_horizontal_edges(
5020 style.margin.left,
5021 style.margin.right,
5022 style.margin.start,
5023 style.margin.end,
5024 direction,
5025 );
5026 if let Some(value) = margin_right_value {
5027 element = margin_right(element, value);
5028 }
5029 if let Some(value) = style.margin.bottom {
5030 element = margin_bottom(element, value);
5031 }
5032 if let Some(value) = margin_left_value {
5033 element = margin_left(element, value);
5034 }
5035 element
5036}
5037
5038fn logical_horizontal_edges<T: Copy>(
5039 mut left: Option<T>,
5040 mut right: Option<T>,
5041 start: Option<T>,
5042 end: Option<T>,
5043 direction: TextDirection,
5044) -> (Option<T>, Option<T>) {
5045 match direction {
5046 TextDirection::LeftToRight => {
5047 if start.is_some() {
5048 left = start;
5049 }
5050 if end.is_some() {
5051 right = end;
5052 }
5053 }
5054 TextDirection::RightToLeft => {
5055 if start.is_some() {
5056 right = start;
5057 }
5058 if end.is_some() {
5059 left = end;
5060 }
5061 }
5062 }
5063 (left, right)
5064}
5065
5066fn logical_keyboard_key(key: &str, direction: TextDirection) -> &str {
5067 match (key, direction) {
5068 ("left", TextDirection::RightToLeft) => "right",
5069 ("right", TextDirection::RightToLeft) => "left",
5070 _ => key,
5071 }
5072}
5073
5074fn apply_paint_and_text(
5075 element: Div,
5076 style: &StyleProperties,
5077 colors: &impl ColorResolver,
5078 direction: TextDirection,
5079) -> Div {
5080 let element = apply_paint(element, style, colors, direction);
5081 let element = apply_typography(element, style, direction);
5082 apply_shadows(element, style, colors)
5083}
5084
5085fn apply_paint(
5086 mut element: Div,
5087 style: &StyleProperties,
5088 colors: &impl ColorResolver,
5089 direction: TextDirection,
5090) -> Div {
5091 if let Some(gradient) = &style.gradient
5092 && let (Some(from), Some(to)) =
5093 (colors.resolve(&gradient.from), colors.resolve(&gradient.to))
5094 {
5095 element = element.bg(linear_gradient(
5096 f64_to_f32(gradient.angle_degrees),
5097 linear_color_stop(rgba(from.as_rgba_hex()), 0.0),
5098 linear_color_stop(rgba(to.as_rgba_hex()), 1.0),
5099 ));
5100 } else if let Some(color) = style
5101 .background
5102 .as_ref()
5103 .and_then(|color| colors.resolve(color))
5104 {
5105 element = element.bg(rgba(color.as_rgba_hex()));
5106 }
5107 if let Some(color) = style
5108 .text_color
5109 .as_ref()
5110 .and_then(|color| colors.resolve(color))
5111 {
5112 element = element.text_color(rgba(color.as_rgba_hex()));
5113 }
5114 if let Some(color) = style
5115 .border_color
5116 .as_ref()
5117 .and_then(|color| colors.resolve(color))
5118 {
5119 element = element.border_color(rgba(color.as_rgba_hex()));
5120 }
5121 if let Some(border_style) = style.border_style {
5122 match border_style {
5123 crate::BorderLineStyle::Solid => {
5124 element.style().border_style = Some(gpui::BorderStyle::Solid);
5125 }
5126 crate::BorderLineStyle::Dashed => {
5127 element = element.border_dashed();
5128 }
5129 }
5130 }
5131 if let Some(value) = style.border_widths.top {
5132 element = border_top(element, value);
5133 }
5134 let (border_left_value, border_right_value) = logical_horizontal_edges(
5135 style.border_widths.left,
5136 style.border_widths.right,
5137 style.border_widths.start,
5138 style.border_widths.end,
5139 direction,
5140 );
5141 if let Some(value) = border_right_value {
5142 element = border_right(element, value);
5143 }
5144 if let Some(value) = style.border_widths.bottom {
5145 element = border_bottom(element, value);
5146 }
5147 if let Some(value) = border_left_value {
5148 element = border_left(element, value);
5149 }
5150 if let Some(value) = style.radii.top_left {
5151 element = radius_top_left(element, value);
5152 }
5153 if let Some(value) = style.radii.top_right {
5154 element = radius_top_right(element, value);
5155 }
5156 if let Some(value) = style.radii.bottom_right {
5157 element = radius_bottom_right(element, value);
5158 }
5159 if let Some(value) = style.radii.bottom_left {
5160 element = radius_bottom_left(element, value);
5161 }
5162 if let Some(value) = style.font_size {
5163 element = font_size(element, value);
5164 }
5165 if let Some(opacity) = style.opacity {
5166 element = element.opacity(f64_to_f32(opacity));
5167 }
5168 if let Some(visible) = style.visible {
5169 element = if visible {
5170 element.visible()
5171 } else {
5172 element.invisible()
5173 };
5174 }
5175 if let Some(cursor) = style.cursor {
5176 element = element.cursor(gpui_cursor(cursor));
5177 }
5178 element
5179}
5180
5181fn apply_typography(mut element: Div, style: &StyleProperties, direction: TextDirection) -> Div {
5182 if let Some(family) = &style.font_family {
5183 element = element.font_family(family.clone());
5184 }
5185 if let Some(fallbacks) = &style.font_fallbacks {
5186 element.text_style().font_fallbacks = Some(FontFallbacks::from_fonts(fallbacks.clone()));
5187 }
5188 if let Some(features) = &style.font_features {
5189 element.text_style().font_features = Some(FontFeatures(Arc::new(
5190 features
5191 .iter()
5192 .map(|(tag, value)| (tag.clone(), *value))
5193 .collect(),
5194 )));
5195 }
5196 if let Some(weight) = style.font_weight {
5197 element = element.font_weight(FontWeight(f32::from(weight)));
5198 }
5199 if let Some(slant) = style.font_slant {
5200 element = match slant {
5201 FontSlant::Normal => element.not_italic(),
5202 FontSlant::Italic => element.italic(),
5203 };
5204 }
5205 if let Some(value) = style.line_height {
5206 element = line_height(element, value);
5207 }
5208 if let Some(align) = style.text_align {
5209 let align = match (align, direction) {
5210 (TextAlignMode::Start, TextDirection::LeftToRight)
5211 | (TextAlignMode::End, TextDirection::RightToLeft) => TextAlign::Left,
5212 (TextAlignMode::Start, TextDirection::RightToLeft)
5213 | (TextAlignMode::End, TextDirection::LeftToRight) => TextAlign::Right,
5214 (TextAlignMode::Center, _) => TextAlign::Center,
5215 };
5216 element = element.text_align(align);
5217 }
5218 if let Some(white_space) = style.white_space {
5219 element = match white_space {
5220 WhiteSpaceMode::Normal => element.whitespace_normal(),
5221 WhiteSpaceMode::NoWrap => element.whitespace_nowrap(),
5222 };
5223 }
5224 if style.text_ellipsis == Some(true) {
5225 element = element.text_ellipsis();
5226 }
5227 if let Some(lines) = style.line_clamp {
5228 element = element.line_clamp(lines);
5229 }
5230 element
5231}
5232
5233fn apply_shadows(mut element: Div, style: &StyleProperties, colors: &impl ColorResolver) -> Div {
5234 if let Some(shadows) = &style.shadows {
5235 element = element.shadow(
5236 shadows
5237 .iter()
5238 .filter_map(|shadow| {
5239 colors.resolve(&shadow.color).map(|color| BoxShadow {
5240 color: rgba(color.as_rgba_hex()).into(),
5241 offset: point(px(f64_to_f32(shadow.x)), px(f64_to_f32(shadow.y))),
5242 blur_radius: px(f64_to_f32(shadow.blur)),
5243 spread_radius: px(f64_to_f32(shadow.spread)),
5244 inset: false,
5245 })
5246 })
5247 .collect(),
5248 );
5249 }
5250 element
5251}
5252
5253macro_rules! definite_length_fn {
5254 ($name:ident, $method:ident) => {
5255 fn $name(element: Div, value: Length) -> Div {
5256 match value {
5257 Length::Pixels(value) => element.$method(px(to_f32(value))),
5258 Length::Rems(value) => element.$method(rems(to_f32(value))),
5259 Length::Relative(value) => element.$method(relative(to_f32(value))),
5260 Length::ThemeSpacing(_) | Length::ThemeRadius(_) => element,
5261 }
5262 }
5263 };
5264}
5265
5266definite_length_fn!(gap, gap);
5267definite_length_fn!(padding_top, pt);
5268definite_length_fn!(padding_right, pr);
5269definite_length_fn!(padding_bottom, pb);
5270definite_length_fn!(padding_left, pl);
5271
5272macro_rules! layout_length_fn {
5273 ($name:ident, $method:ident) => {
5274 fn $name(element: Div, value: LayoutLength) -> Div {
5275 match value {
5276 LayoutLength::Definite(Length::Pixels(value)) => element.$method(px(to_f32(value))),
5277 LayoutLength::Signed(SignedLength::Pixels(value)) => {
5278 element.$method(px(f64_to_f32(value)))
5279 }
5280 LayoutLength::Definite(Length::Rems(value)) => element.$method(rems(to_f32(value))),
5281 LayoutLength::Signed(SignedLength::Rems(value)) => {
5282 element.$method(rems(f64_to_f32(value)))
5283 }
5284 LayoutLength::Definite(Length::Relative(value)) => {
5285 element.$method(relative(to_f32(value)))
5286 }
5287 LayoutLength::Signed(SignedLength::Relative(value)) => {
5288 element.$method(relative(f64_to_f32(value)))
5289 }
5290 LayoutLength::Auto => element.$method(auto()),
5291 LayoutLength::Definite(Length::ThemeSpacing(_) | Length::ThemeRadius(_)) => element,
5292 }
5293 }
5294 };
5295}
5296
5297layout_length_fn!(width, w);
5298layout_length_fn!(height, h);
5299layout_length_fn!(min_width, min_w);
5300layout_length_fn!(max_width, max_w);
5301layout_length_fn!(min_height, min_h);
5302layout_length_fn!(max_height, max_h);
5303layout_length_fn!(margin_top, mt);
5304layout_length_fn!(margin_right, mr);
5305layout_length_fn!(margin_bottom, mb);
5306layout_length_fn!(margin_left, ml);
5307layout_length_fn!(inset_top, top);
5308layout_length_fn!(inset_right, right);
5309layout_length_fn!(inset_bottom, bottom);
5310layout_length_fn!(inset_left, left);
5311layout_length_fn!(flex_basis, flex_basis);
5312
5313macro_rules! absolute_length_fn {
5314 ($name:ident, $method:ident) => {
5315 fn $name(element: Div, value: Length) -> Div {
5316 match value {
5317 Length::Pixels(value) => element.$method(px(to_f32(value))),
5318 Length::Rems(value) => element.$method(rems(to_f32(value))),
5319 Length::Relative(_) | Length::ThemeSpacing(_) | Length::ThemeRadius(_) => element,
5320 }
5321 }
5322 };
5323}
5324
5325absolute_length_fn!(border_top, border_t);
5326absolute_length_fn!(border_right, border_r);
5327absolute_length_fn!(border_bottom, border_b);
5328absolute_length_fn!(border_left, border_l);
5329absolute_length_fn!(radius_top_left, rounded_tl);
5330absolute_length_fn!(radius_top_right, rounded_tr);
5331absolute_length_fn!(radius_bottom_right, rounded_br);
5332absolute_length_fn!(radius_bottom_left, rounded_bl);
5333
5334fn font_size(element: Div, value: Length) -> Div {
5335 match value {
5336 Length::Pixels(value) => element.text_size(px(to_f32(value))),
5337 Length::Rems(value) => element.text_size(rems(to_f32(value))),
5338 Length::Relative(_) | Length::ThemeSpacing(_) | Length::ThemeRadius(_) => element,
5339 }
5340}
5341
5342fn line_height(element: Div, value: Length) -> Div {
5343 match value {
5344 Length::Pixels(value) => element.line_height(px(to_f32(value))),
5345 Length::Rems(value) => element.line_height(rems(to_f32(value))),
5346 Length::Relative(_) | Length::ThemeSpacing(_) | Length::ThemeRadius(_) => element,
5347 }
5348}
5349
5350const fn gpui_cursor(cursor: CursorKind) -> CursorStyle {
5351 match cursor {
5352 CursorKind::Default => CursorStyle::Arrow,
5353 CursorKind::Pointer => CursorStyle::PointingHand,
5354 CursorKind::Text => CursorStyle::IBeam,
5355 CursorKind::Move => CursorStyle::ClosedHand,
5356 CursorKind::Crosshair => CursorStyle::Crosshair,
5357 CursorKind::NotAllowed => CursorStyle::OperationNotAllowed,
5358 CursorKind::ResizeHorizontal => CursorStyle::ResizeLeftRight,
5359 CursorKind::ResizeVertical => CursorStyle::ResizeUpDown,
5360 }
5361}
5362
5363#[allow(clippy::cast_possible_truncation)]
5364fn to_f32(value: f64) -> f32 {
5365 debug_assert!(value.is_finite() && value >= 0.0 && value <= f64::from(f32::MAX));
5366 value as f32
5367}
5368
5369pub struct StaticUiView {
5371 tree: crate::RetainedUiTree,
5372 semantics: crate::CommittedSemanticFrame,
5373 primitives: PrimitiveRegistry,
5374}
5375
5376#[derive(Debug, thiserror::Error)]
5377pub enum StaticUiViewError {
5378 #[error(transparent)]
5379 Reconcile(#[from] crate::ReconcileError),
5380 #[error(transparent)]
5381 Accessibility(#[from] crate::AccessibilityError),
5382}
5383
5384impl StaticUiView {
5385 pub fn new(root: UiNode) -> Result<Self, StaticUiViewError> {
5391 Self::with_primitives(root, PrimitiveRegistry::new())
5392 }
5393
5394 pub fn with_primitives(
5400 root: UiNode,
5401 primitives: PrimitiveRegistry,
5402 ) -> Result<Self, StaticUiViewError> {
5403 let mut tree = crate::RetainedUiTree::new();
5404 tree.reconcile(root)?;
5405 let semantics = crate::CommittedSemanticFrame::from_retained(&tree)?;
5406 Ok(Self {
5407 tree,
5408 semantics,
5409 primitives,
5410 })
5411 }
5412
5413 pub fn set_root(
5419 &mut self,
5420 root: UiNode,
5421 cx: &mut Context<Self>,
5422 ) -> Result<crate::ReconcileReport, StaticUiViewError> {
5423 let mut tree = self.tree.clone();
5424 let report = tree.reconcile(root)?;
5425 let semantics = crate::CommittedSemanticFrame::from_retained(&tree)?;
5426 self.tree = tree;
5427 self.semantics = semantics;
5428 cx.notify();
5429 Ok(report)
5430 }
5431
5432 #[must_use]
5433 pub fn root(&self) -> Option<&UiNode> {
5434 self.tree.root()
5435 }
5436
5437 #[must_use]
5438 pub const fn retained(&self) -> &crate::RetainedUiTree {
5439 &self.tree
5440 }
5441}
5442
5443impl Render for StaticUiView {
5444 fn render(&mut self, window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
5445 if let Err(error) = self.primitives.retain_tree(&self.tree) {
5446 return div()
5447 .child(format!("Custom primitive lifecycle error: {error}"))
5448 .into_any_element();
5449 }
5450 self.root().map_or_else(
5451 || {
5452 div()
5453 .child("Static UI view has no accepted root")
5454 .into_any_element()
5455 },
5456 |_| {
5457 GpuiNodeRenderer::render_retained_with_committed_semantics(
5458 &self.tree,
5459 &self.semantics,
5460 window.is_a11y_active(),
5461 &LiteralColorResolver,
5462 &InteractionState::default(),
5463 &self.primitives,
5464 )
5465 },
5466 )
5467 }
5468}
5469
5470#[cfg(test)]
5471mod tests {
5472 use super::*;
5473 use crate::{ColorValue, Length, Rgba8, Style};
5474
5475 struct TypographyResolver;
5476
5477 impl ColorResolver for TypographyResolver {
5478 fn resolve(&self, color: &ColorValue) -> Option<Rgba8> {
5479 LiteralColorResolver.resolve(color)
5480 }
5481
5482 fn resolve_typography(&self, role: &str) -> Option<crate::ResolvedTypography> {
5483 (role == "body").then(|| crate::ResolvedTypography {
5484 family: Some("JetBrains Mono".to_owned()),
5485 fallbacks: vec!["PingFang SC".to_owned()],
5486 size: Length::Pixels(12.0),
5487 line_height: Length::Pixels(16.0),
5488 weight: 400,
5489 })
5490 }
5491 }
5492
5493 #[test]
5494 fn owned_color_snapshot_matches_the_native_primitive_theme_surface() {
5495 let engine = crate::RuntimeEngine::new();
5496 let mut theme = crate::load_theme_source(
5497 engine.engine(),
5498 "default_light.rhai",
5499 include_str!("../../../registry/themes/default_light.rhai"),
5500 )
5501 .unwrap();
5502 let custom = Rgba8::from_rgba_hex(0x55aa_ccff);
5503 theme.tokens.namespaces.insert(
5504 "brand".to_owned(),
5505 BTreeMap::from([("tint".to_owned(), crate::ThemeTokenValue::Color(custom))]),
5506 );
5507 let snapshot = OwnedColorResolver::capture(&theme);
5508 assert_eq!(
5509 snapshot.resolve(&ColorValue::Token("selection".to_owned())),
5510 theme.tokens.color("selection")
5511 );
5512 assert_eq!(
5513 snapshot.resolve(&ColorValue::Token("table.selection".to_owned())),
5514 theme.tokens.color("table.selection")
5515 );
5516 assert_eq!(
5517 snapshot.resolve_length(Length::ThemeSpacing(SpacingToken::Xxs)),
5518 theme.tokens.spacing.get("xxs").copied()
5519 );
5520 assert_eq!(
5521 snapshot.resolve(&ColorValue::Token("brand.tint".to_owned())),
5522 Some(custom)
5523 );
5524 }
5525
5526 #[test]
5527 fn declarative_nodes_and_typed_styles_convert_without_a_gpui_context() {
5528 let root = UiNode::column(vec![
5529 UiNode::text("one"),
5530 UiNode::text("two"),
5531 UiNode::svg("<svg viewBox='0 0 1 1'><path fill='currentColor' d='M0 0L1 1'/></svg>")
5532 .unwrap()
5533 .with_style(
5534 &Style::new().text_color(ColorValue::Literal(Rgba8::from_rgb_hex(0x00ff_ffff))),
5535 ),
5536 ])
5537 .with_style(
5538 &Style::new()
5539 .gap(Length::pixels(8.0).unwrap())
5540 .background(ColorValue::Literal(Rgba8::from_rgb_hex(0x0022_2222))),
5541 );
5542 let _element = GpuiNodeRenderer::render(&root);
5543 }
5544
5545 #[test]
5546 fn styled_inline_svg_fills_its_node_box_without_losing_intrinsic_default() {
5547 let bytes = b"<svg width='24' height='24' viewBox='0 0 24 24'></svg>".to_vec();
5548 let image = crate::asset::svg_image(&bytes, None).unwrap();
5549 let mut sized = inline_svg_image(Arc::clone(&image), true);
5550 assert_eq!(sized.style().size.width, Some(relative(1.0).into()));
5551 assert_eq!(sized.style().size.height, Some(relative(1.0).into()));
5552
5553 let mut intrinsic = inline_svg_image(image, false);
5554 assert_eq!(intrinsic.style().size.width, None);
5555 assert_eq!(intrinsic.style().size.height, None);
5556 }
5557
5558 #[test]
5559 fn ambient_text_color_inherits_and_a_local_value_wins() {
5560 let inherited = Rgba8::from_rgb_hex(0x0012_34ab);
5561 assert_eq!(
5562 resolve_ambient_text_color(&Style::new().base, &LiteralColorResolver, Some(inherited)),
5563 Some(inherited)
5564 );
5565
5566 let local = Rgba8::from_rgb_hex(0x00ab_cd12);
5567 assert_eq!(
5568 resolve_ambient_text_color(
5569 &Style::new().text_color(ColorValue::Literal(local)).base,
5570 &LiteralColorResolver,
5571 Some(inherited)
5572 ),
5573 Some(local)
5574 );
5575 }
5576
5577 #[test]
5578 fn asset_svg_uses_an_ancestor_text_color() {
5579 let assets = AssetRegistry::new();
5580 assets
5581 .register(
5582 "app",
5583 crate::InMemoryAssetProvider::new(BTreeMap::from([(
5584 "icon".to_owned(),
5585 crate::AssetData {
5586 mime_type: "image/svg+xml".to_owned(),
5587 bytes: br#"<svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"><rect width="1" height="1" fill="currentColor"/></svg>"#.to_vec(),
5588 },
5589 )])),
5590 )
5591 .unwrap();
5592 let handle = assets
5593 .load_image(&crate::AssetId::parse("app/icon").unwrap())
5594 .unwrap();
5595 let color = Rgba8::from_rgb_hex(0x0012_34ab);
5596 let root = UiNode::row(vec![UiNode::image(handle.opaque().clone())])
5597 .with_style(&Style::new().text_color(ColorValue::Literal(color)));
5598 let dispatcher = NodeEventDispatcher::new(|_, _, _, _, _| EventPropagation::Handled);
5599
5600 let _element = GpuiNodeRenderer::render_with_runtime(
5601 &root,
5602 &LiteralColorResolver,
5603 &InteractionState::default(),
5604 &PrimitiveRegistry::new(),
5605 &assets,
5606 &dispatcher,
5607 );
5608
5609 assert!(matches!(
5610 assets
5611 .image_source_tinted(handle.opaque(), Some(color))
5612 .unwrap(),
5613 gpui::ImageSource::Custom(_)
5614 ));
5615 }
5616
5617 #[test]
5618 fn symbolic_typography_resolves_and_explicit_fields_win() {
5619 let mut style = Style::new()
5620 .typography("body")
5621 .unwrap()
5622 .font_weight(650)
5623 .unwrap()
5624 .base;
5625 resolve_typography_role(&mut style, &TypographyResolver);
5626 assert_eq!(style.font_family.as_deref(), Some("JetBrains Mono"));
5627 assert_eq!(
5628 style.font_fallbacks.as_deref(),
5629 Some(["PingFang SC".to_owned()].as_slice())
5630 );
5631 assert_eq!(style.font_size, Some(Length::Pixels(12.0)));
5632 assert_eq!(style.line_height, Some(Length::Pixels(16.0)));
5633 assert_eq!(style.font_weight, Some(650));
5634 }
5635
5636 #[test]
5637 fn pseudo_paint_preserves_native_border_text_and_opacity_states() {
5638 let hover = Style::new()
5639 .background(ColorValue::Literal(Rgba8::from_rgb_hex(0x0011_2233)))
5640 .border_color(ColorValue::Literal(Rgba8::from_rgb_hex(0x0044_5566)))
5641 .text_color(ColorValue::Literal(Rgba8::from_rgb_hex(0x0077_8899)))
5642 .opacity(0.625)
5643 .unwrap();
5644 let paint = pseudo_paint(Some(&hover.base), &LiteralColorResolver);
5645 assert_eq!(paint.background, Some(Rgba8::from_rgb_hex(0x0011_2233)));
5646 assert_eq!(paint.border, Some(Rgba8::from_rgb_hex(0x0044_5566)));
5647 assert_eq!(paint.text, Some(Rgba8::from_rgb_hex(0x0077_8899)));
5648 assert_eq!(paint.opacity, Some(0.625));
5649 }
5650
5651 #[test]
5652 fn aligned_text_nodes_become_flex_content_boxes() {
5653 let node = UiNode::text("Centered").with_style(
5654 &Style::new()
5655 .items_center()
5656 .justify_center()
5657 .height(Length::pixels(32.0).unwrap()),
5658 );
5659 let mut resolved = node.style().resolve(&InteractionState::default());
5660
5661 normalize_text_content_layout(&node, &mut resolved);
5662
5663 assert_eq!(resolved.display, Some(DisplayMode::Flex));
5664 assert_eq!(resolved.align, Some(Align::Center));
5665 assert_eq!(resolved.justify, Some(Justify::Center));
5666 }
5667
5668 #[test]
5669 fn sampled_motion_values_override_dimensions_and_transform_without_rhai() {
5670 let values = BTreeMap::from([
5671 (
5672 MotionKey::for_node("root/card", MotionProperty::Width),
5673 180.0,
5674 ),
5675 (
5676 MotionKey::for_node("root/card", MotionProperty::ClipHeight),
5677 64.0,
5678 ),
5679 (
5680 MotionKey::for_node("root/card", MotionProperty::TranslateX),
5681 12.0,
5682 ),
5683 ]);
5684 let sampled = node_motion(&values, "root/card");
5685 let mut style = StyleProperties::default();
5686 apply_motion_dimensions(&mut style, sampled);
5687 assert_eq!(style.width, Some(Length::Pixels(180.0).into()));
5688 assert_eq!(style.height, Some(Length::Pixels(64.0).into()));
5689 assert_eq!(sampled.translate_x, Some(12.0));
5690 }
5691
5692 #[test]
5693 fn native_signal_values_override_approved_properties_without_rhai() {
5694 let component = crate::ComponentInstancePath::root("Meter", "primary");
5695 let id =
5696 crate::SignalId::new(component.clone(), "width", crate::SignalKind::Float).unwrap();
5697 let signal = crate::NativeSignal::new(id.clone());
5698 let node = UiNode::text("meter")
5699 .with_signal_binding(crate::SignalProperty::Width, signal.clone())
5700 .unwrap();
5701 let mut registry = crate::SignalRegistry::new();
5702 registry.reconcile(
5703 &component,
5704 BTreeMap::from([(
5705 id,
5706 crate::signal::SignalDescriptor::new(crate::SignalValue::Float(80.0)),
5707 )]),
5708 );
5709 registry
5710 .write(&signal, crate::SignalValue::Float(144.0))
5711 .unwrap();
5712 let values = node_signals(®istry, &node);
5713 let mut style = StyleProperties::default();
5714 apply_signal_style(&mut style, &values);
5715 assert_eq!(style.width, Some(Length::Pixels(144.0).into()));
5716 }
5717
5718 #[test]
5719 fn canvas_signal_transform_is_shared_by_paint_and_hit_testing() {
5720 let component = crate::ComponentInstancePath::root("PanZoom", "viewport");
5721 let scale_ids = (
5722 crate::SignalId::new(component.clone(), "scale-x", crate::SignalKind::Float).unwrap(),
5723 crate::SignalId::new(component.clone(), "scale-y", crate::SignalKind::Float).unwrap(),
5724 );
5725 let rotate_id =
5726 crate::SignalId::new(component.clone(), "rotate", crate::SignalKind::Float).unwrap();
5727 let scale_x = crate::NativeSignal::new(scale_ids.0.clone());
5728 let scale_y = crate::NativeSignal::new(scale_ids.1.clone());
5729 let rotate = crate::NativeSignal::new(rotate_id.clone());
5730 let scene = crate::CanvasScene::new(vec![crate::CanvasCommand::Rect {
5731 key: "target".to_owned(),
5732 x: 20.0,
5733 y: 25.0,
5734 width: 20.0,
5735 height: 20.0,
5736 fill: ColorValue::Token("accent".to_owned()),
5737 }])
5738 .unwrap();
5739 let node = UiNode::canvas(scene.clone())
5740 .with_signal_binding(crate::SignalProperty::ScaleX, scale_x)
5741 .unwrap()
5742 .with_signal_binding(crate::SignalProperty::ScaleY, scale_y)
5743 .unwrap()
5744 .with_signal_binding(crate::SignalProperty::Rotate, rotate)
5745 .unwrap();
5746 let mut registry = crate::SignalRegistry::new();
5747 registry.reconcile(
5748 &component,
5749 BTreeMap::from([
5750 (
5751 scale_ids.0,
5752 crate::signal::SignalDescriptor::new(crate::SignalValue::Float(1.6)),
5753 ),
5754 (
5755 scale_ids.1,
5756 crate::signal::SignalDescriptor::new(crate::SignalValue::Float(0.8)),
5757 ),
5758 (
5759 rotate_id,
5760 crate::signal::SignalDescriptor::new(crate::SignalValue::Float(19.0)),
5761 ),
5762 ]),
5763 );
5764 let motion = apply_canvas_signal_transform(
5765 NodeMotionValues::default(),
5766 &node_signals(®istry, &node),
5767 );
5768 let mut tree = RetainedUiTree::new();
5769 tree.reconcile(node).unwrap();
5770 let retained = tree.root_id().unwrap();
5771 let geometry = crate::GeometryRegistry::new();
5772 let bounds = crate::GeometryBounds::new(100.0, 50.0, 120.0, 90.0).unwrap();
5773 geometry.update(
5774 retained,
5775 crate::ElementGeometry {
5776 layout: bounds,
5777 visual: bounds,
5778 clip: None,
5779 },
5780 );
5781 geometry.update_canvas_transform(retained, motion.canvas_transform());
5782 let paint_bounds = Bounds::new(point(px(100.0), px(50.0)), gpui::size(px(120.0), px(90.0)));
5783 let painted = node_canvas_point(paint_bounds, 30.0, 35.0, motion);
5784 let context = PointerPayloadContext::retained(retained, geometry, Some(scene), Vec::new());
5785 let payload = context.enrich(pointer_payload(
5786 painted,
5787 Some(MouseButton::Left),
5788 vec![MouseButton::Left],
5789 Modifiers::default(),
5790 1,
5791 false,
5792 ));
5793 let UiValue::Map(payload) = payload else {
5794 unreachable!()
5795 };
5796 assert_eq!(payload["canvas_key"], UiValue::String("target".to_owned()));
5797 }
5798
5799 #[test]
5800 fn optional_width_override_activates_fixed_layout_only_when_present() {
5801 let mut base = StyleProperties {
5802 flex_basis: Some(Length::Relative(0.0).into()),
5803 flex_grow_weight: Some(2.0),
5804 ..StyleProperties::default()
5805 };
5806 apply_signal_style(&mut base, &NodeSignalValues::default());
5807 assert_eq!(base.flex_grow_weight, Some(2.0));
5808 let mut overridden = base;
5809 apply_signal_style(
5810 &mut overridden,
5811 &NodeSignalValues {
5812 width_override: Some(180.0),
5813 ..NodeSignalValues::default()
5814 },
5815 );
5816 assert_eq!(overridden.width, Some(Length::Pixels(180.0).into()));
5817 assert_eq!(overridden.flex_basis, None);
5818 assert_eq!(overridden.flex_grow, Some(false));
5819 assert_eq!(overridden.flex_grow_weight, None);
5820 assert_eq!(overridden.flex_shrink, Some(false));
5821 }
5822
5823 #[test]
5824 fn property_specific_layout_values_reach_gpui_without_loss() {
5825 let mut automatic = width(div(), LayoutLength::Auto);
5826 assert_eq!(automatic.style().size.width, Some(auto()));
5827
5828 let mut signed = margin_left(div(), LayoutLength::Signed(SignedLength::Pixels(-12.0)));
5829 assert_eq!(
5830 signed.style().margin.left,
5831 Some(gpui::Length::from(px(-12.0)))
5832 );
5833
5834 let mut weighted = apply_layout_dimensions(
5835 div(),
5836 &StyleProperties {
5837 flex_grow_weight: Some(2.5),
5838 flex_basis: Some(Length::Relative(0.0).into()),
5839 ..StyleProperties::default()
5840 },
5841 );
5842 assert_eq!(weighted.style().flex_grow, Some(2.5));
5843 assert_eq!(weighted.style().flex_basis, Some(relative(0.0).into()));
5844 }
5845
5846 #[test]
5847 fn committed_semantics_write_native_role_label_state_and_range() {
5848 let source = UiNode::text("ignored")
5849 .with_attribute("role", UiValue::String("slider".to_owned()))
5850 .with_attribute("label", UiValue::String("Volume".to_owned()))
5851 .with_attribute("value", UiValue::Float(25.0))
5852 .with_attribute("value_min", UiValue::Float(0.0))
5853 .with_attribute("value_max", UiValue::Float(100.0))
5854 .with_attribute("orientation", UiValue::String("horizontal".to_owned()));
5855 let mut retained = RetainedUiTree::new();
5856 retained.reconcile(source).unwrap();
5857 let frame = crate::CommittedSemanticFrame::from_retained(&retained).unwrap();
5858 let semantic = frame.node(retained.root_id().unwrap()).unwrap();
5859 let element = apply_native_semantics(div().id("volume"), Some(semantic));
5860 assert_eq!(element.a11y_role(), Some(gpui::Role::Slider));
5861 let mut node = gpui::accesskit::Node::new(gpui::Role::Slider);
5862 element.write_a11y_info(&mut node);
5863 assert_eq!(node.label(), Some("Volume"));
5864 assert_eq!(node.value(), Some("25"));
5865 assert_eq!(node.numeric_value(), Some(25.0));
5866 assert_eq!(node.min_numeric_value(), Some(0.0));
5867 assert_eq!(node.max_numeric_value(), Some(100.0));
5868 assert_eq!(node.orientation(), Some(gpui::Orientation::Horizontal));
5869 }
5870
5871 #[test]
5872 fn plain_text_semantics_use_a_valued_native_label() {
5873 let mut retained = RetainedUiTree::new();
5874 retained.reconcile(UiNode::text("Ready")).unwrap();
5875 let frame = crate::CommittedSemanticFrame::from_retained(&retained).unwrap();
5876 let semantic = frame.node(retained.root_id().unwrap()).unwrap();
5877 let element = apply_native_semantics(div().id("status-text"), Some(semantic));
5878
5879 assert_eq!(semantic.role, "text");
5880 assert_eq!(element.a11y_role(), Some(gpui::Role::Label));
5881 let mut node = gpui::accesskit::Node::new(gpui::Role::Label);
5882 element.write_a11y_info(&mut node);
5883 assert_eq!(node.label(), None);
5884 assert_eq!(node.value(), Some("Ready"));
5885 assert!(node.character_lengths().is_empty());
5886 }
5887
5888 #[test]
5889 fn logical_spacing_resolves_to_physical_edges_in_both_directions() {
5890 let start = Length::Pixels(12.0);
5891 let end = Length::Pixels(4.0);
5892 assert_eq!(
5893 logical_horizontal_edges(
5894 None,
5895 None,
5896 Some(start),
5897 Some(end),
5898 TextDirection::LeftToRight,
5899 ),
5900 (Some(start), Some(end))
5901 );
5902 assert_eq!(
5903 logical_horizontal_edges(
5904 None,
5905 None,
5906 Some(start),
5907 Some(end),
5908 TextDirection::RightToLeft,
5909 ),
5910 (Some(end), Some(start))
5911 );
5912 }
5913
5914 #[test]
5915 fn rtl_keyboard_navigation_maps_physical_arrows_to_logical_handlers() {
5916 assert_eq!(
5917 logical_keyboard_key("left", TextDirection::RightToLeft),
5918 "right"
5919 );
5920 assert_eq!(
5921 logical_keyboard_key("right", TextDirection::RightToLeft),
5922 "left"
5923 );
5924 assert_eq!(
5925 logical_keyboard_key("left", TextDirection::LeftToRight),
5926 "left"
5927 );
5928 assert_eq!(
5929 logical_keyboard_key("down", TextDirection::RightToLeft),
5930 "down"
5931 );
5932 }
5933
5934 #[test]
5935 fn logical_overlay_edges_resolve_against_text_direction() {
5936 let spec = |placement| crate::OverlayNodeSpec {
5937 id: crate::OverlayId::new("logical-sheet"),
5938 parent: None,
5939 kind: crate::OverlayKind::Sheet,
5940 placement,
5941 anchor: None,
5942 open: true,
5943 gap: 0.0,
5944 modal: true,
5945 dismiss: crate::OverlayDismissPolicy {
5946 escape: true,
5947 outside: true,
5948 },
5949 tooltip_delays: None,
5950 initial_focus: crate::OverlayInitialFocus::Panel,
5951 activate_on_trigger: false,
5952 width_policy: crate::OverlayWidthPolicy::Content,
5953 };
5954 assert_eq!(
5955 scoped_overlay_spec(
5956 &spec(crate::OverlayPlacement::Start),
5957 "view",
5958 TextDirection::RightToLeft,
5959 )
5960 .placement,
5961 crate::OverlayPlacement::Right
5962 );
5963 assert_eq!(
5964 scoped_overlay_spec(
5965 &spec(crate::OverlayPlacement::End),
5966 "view",
5967 TextDirection::RightToLeft,
5968 )
5969 .placement,
5970 crate::OverlayPlacement::Left
5971 );
5972 }
5973
5974 #[test]
5975 fn retained_tab_order_reads_explicit_group_indices() {
5976 let node = UiNode::text("tab")
5977 .with_attribute("tab_index", UiValue::Integer(7))
5978 .with_attribute("tab_stop", UiValue::Bool(false))
5979 .with_attribute("tab_group", UiValue::Bool(true));
5980 assert_eq!(node_tab_index(&node), 7);
5981 assert!(!node_tab_stop(&node));
5982 assert!(node_has_focus_declaration(&node));
5983 let _element = GpuiNodeRenderer::render(&node);
5984 }
5985
5986 #[test]
5987 fn pointer_payload_uses_committed_local_geometry_and_canvas_hit_key() {
5988 let scene = crate::CanvasScene::new(vec![crate::CanvasCommand::Rect {
5989 key: "clip".to_owned(),
5990 x: 0.0,
5991 y: 0.0,
5992 width: 40.0,
5993 height: 30.0,
5994 fill: ColorValue::Token("accent".to_owned()),
5995 }])
5996 .unwrap();
5997 let mut tree = crate::RetainedUiTree::new();
5998 tree.reconcile(UiNode::canvas(scene.clone()).with_key("canvas"))
5999 .unwrap();
6000 let node = tree.root_id().unwrap();
6001 let geometry = crate::GeometryRegistry::new();
6002 geometry.update(
6003 node,
6004 crate::ElementGeometry {
6005 layout: crate::GeometryBounds::new(100.0, 50.0, 200.0, 120.0).unwrap(),
6006 visual: crate::GeometryBounds::new(105.0, 54.0, 200.0, 120.0).unwrap(),
6007 clip: None,
6008 },
6009 );
6010 let outer_scroll = ScrollHandle::new();
6011 outer_scroll.set_offset(point(px(-10.0), px(-20.0)));
6012 let inner_scroll = ScrollHandle::new();
6013 inner_scroll.set_offset(point(px(-3.0), px(-4.0)));
6014 let context = PointerPayloadContext::retained(
6015 node,
6016 geometry,
6017 Some(scene),
6018 vec![outer_scroll, inner_scroll],
6019 );
6020 let payload = context.enrich(pointer_payload(
6021 point(px(112.0), px(68.0)),
6022 Some(MouseButton::Left),
6023 vec![MouseButton::Left],
6024 Modifiers::default(),
6025 1,
6026 false,
6027 ));
6028 let UiValue::Map(payload) = payload else {
6029 unreachable!()
6030 };
6031 assert_eq!(payload["canvas_key"], UiValue::String("clip".to_owned()));
6032 assert_eq!(
6033 payload["target"],
6034 crate::GeometryBounds::new(105.0, 54.0, 200.0, 120.0)
6035 .unwrap()
6036 .into_value()
6037 );
6038 assert!(value_point(&payload["local"]).is_some_and(|(x, y)| {
6039 (x - 7.0).abs() < f64::EPSILON && (y - 14.0).abs() < f64::EPSILON
6040 }));
6041 assert!(value_point(&payload["content"]).is_some_and(|(x, y)| {
6042 (x - 20.0).abs() < f64::EPSILON && (y - 38.0).abs() < f64::EPSILON
6043 }));
6044
6045 let scroll_style = Style::new().overflow_scroll();
6046 let nested_root = UiNode::box_node(vec![
6047 UiNode::box_node(vec![UiNode::text("target").with_key("target")])
6048 .with_key("inner")
6049 .with_style(&scroll_style),
6050 ])
6051 .with_key("outer")
6052 .with_style(&scroll_style);
6053 let mut nested = RetainedUiTree::new();
6054 nested.reconcile(nested_root).unwrap();
6055 let ids = nested
6056 .nodes()
6057 .filter_map(|node| node.key().map(|key| (key.to_owned(), node.id())))
6058 .collect::<BTreeMap<_, _>>();
6059 let handles = BTreeMap::from([
6060 (ids["outer"], ScrollHandle::new()),
6061 (ids["inner"], ScrollHandle::new()),
6062 ]);
6063 assert_eq!(
6064 scroll_handles_for_node(Some(&nested), Some(ids["target"]), &handles).len(),
6065 2
6066 );
6067 }
6068
6069 #[test]
6070 fn canvas_motion_uses_one_affine_transform_for_paint_and_hit_testing() {
6071 let scene = crate::CanvasScene::new(vec![crate::CanvasCommand::Rect {
6072 key: "target".to_owned(),
6073 x: 20.0,
6074 y: 25.0,
6075 width: 20.0,
6076 height: 20.0,
6077 fill: ColorValue::Token("accent".to_owned()),
6078 }])
6079 .unwrap();
6080 let mut tree = crate::RetainedUiTree::new();
6081 tree.reconcile(UiNode::canvas(scene.clone()).with_key("canvas"))
6082 .unwrap();
6083 let node = tree.root_id().unwrap();
6084 let geometry = crate::GeometryRegistry::new();
6085 let bounds = crate::GeometryBounds::new(100.0, 50.0, 120.0, 90.0).unwrap();
6086 geometry.update(
6087 node,
6088 crate::ElementGeometry {
6089 layout: bounds,
6090 visual: bounds,
6091 clip: None,
6092 },
6093 );
6094 let transform = crate::geometry::CanvasMotionTransform {
6095 rotate: 31.0,
6096 scale_x: 1.4,
6097 scale_y: 0.7,
6098 skew_x: 12.0,
6099 skew_y: -8.0,
6100 path_progress: None,
6101 };
6102 geometry.update_canvas_transform(node, transform);
6103 let motion = NodeMotionValues {
6104 rotate: Some(transform.rotate),
6105 scale_x: Some(transform.scale_x),
6106 scale_y: Some(transform.scale_y),
6107 skew_x: Some(transform.skew_x),
6108 skew_y: Some(transform.skew_y),
6109 ..NodeMotionValues::default()
6110 };
6111 let paint_bounds = Bounds::new(point(px(100.0), px(50.0)), gpui::size(px(120.0), px(90.0)));
6112 let painted = node_canvas_point(paint_bounds, 30.0, 35.0, motion);
6113 let context = PointerPayloadContext::retained(node, geometry, Some(scene), Vec::new());
6114 let payload = context.enrich(pointer_payload(
6115 painted,
6116 Some(MouseButton::Left),
6117 vec![MouseButton::Left],
6118 Modifiers::default(),
6119 1,
6120 false,
6121 ));
6122 let UiValue::Map(payload) = payload else {
6123 unreachable!()
6124 };
6125 assert_eq!(payload["canvas_key"], UiValue::String("target".to_owned()));
6126 }
6127}