1use std::cell::RefCell;
4use std::collections::BTreeMap;
5use std::rc::Rc;
6
7use gpui::{
8 AnyElement, App, Bounds, CursorStyle, DispatchPhase, Element, ElementId, GlobalElementId,
9 Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId, MouseButton, MouseDownEvent,
10 Pixels, Point, SharedString, Style, Window, fill, point, px, relative, rgba, size,
11};
12
13use crate::{
14 ComponentStateSchema, EventSchema, Length, ObjectField, PrimitiveContext, PrimitiveDescriptor,
15 PrimitiveHandler, PrimitiveId, PrimitiveInstance, PrimitiveInstanceId, PrimitiveProps,
16 PrimitiveTheme, PrimitiveValue, Rgba8, SignalId, SignalKind, SignalValue, TextDirection,
17 UiValue, ValueSchema,
18};
19
20const DEFAULT_MIN_WIDTH: f64 = 48.0;
21const MAX_COLUMN_WIDTH: f64 = 16_384.0;
22
23#[derive(Clone, Default)]
24pub(crate) struct ColumnMeasurementRegistry(Rc<RefCell<BTreeMap<SignalId, BTreeMap<u64, f64>>>>);
25
26impl ColumnMeasurementRegistry {
27 fn report(&self, group: &SignalId, instance: u64, width: f64) {
28 self.0
29 .borrow_mut()
30 .entry(group.clone())
31 .or_default()
32 .insert(instance, width);
33 }
34
35 fn remove(&self, group: &SignalId, instance: u64) {
36 let mut groups = self.0.borrow_mut();
37 let remove_group = groups.get_mut(group).is_some_and(|measurements| {
38 measurements.remove(&instance);
39 measurements.is_empty()
40 });
41 if remove_group {
42 groups.remove(group);
43 }
44 }
45
46 fn maximum(&self, group: &SignalId) -> Option<f64> {
47 self.0
48 .borrow()
49 .get(group)
50 .and_then(|measurements| measurements.values().copied().reduce(f64::max))
51 }
52}
53
54#[derive(Clone, Debug, PartialEq)]
55struct SourceWidth {
56 column_key: String,
57 kind: String,
58 value: f64,
59 min: f64,
60 max: f64,
61}
62
63#[derive(Clone)]
64struct ResizeConfig {
65 id: String,
66 column_key: String,
67 signal: crate::NativeSignal,
68 reference: crate::ElementRef,
69 source: SourceWidth,
70 min: f64,
71 max: f64,
72 direction: TextDirection,
73 idle_color: Rgba8,
74 active_color: Rgba8,
75}
76
77#[derive(Clone)]
78struct ResizeState(Rc<RefCell<ResizeStateInner>>);
79
80#[derive(Clone)]
81struct ResizeStateInner {
82 source: SourceWidth,
83 signal: crate::SignalId,
84}
85
86impl ResizeState {
87 fn new(config: &ResizeConfig) -> Self {
88 Self(Rc::new(RefCell::new(ResizeStateInner {
89 source: config.source.clone(),
90 signal: config.signal.id().clone(),
91 })))
92 }
93}
94
95struct ResizePrepaint {
96 hitbox: Hitbox,
97}
98
99struct ColumnResizeHandle {
100 config: ResizeConfig,
101 events: PrimitiveContext,
102 measurements: ColumnMeasurementRegistry,
103}
104
105impl IntoElement for ColumnResizeHandle {
106 type Element = Self;
107
108 fn into_element(self) -> Self::Element {
109 self
110 }
111}
112
113impl Element for ColumnResizeHandle {
114 type RequestLayoutState = ();
115 type PrepaintState = ResizePrepaint;
116
117 fn id(&self) -> Option<ElementId> {
118 Some(ElementId::Name(self.config.id.clone().into()))
119 }
120
121 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
122 None
123 }
124
125 fn request_layout(
126 &mut self,
127 _: Option<&GlobalElementId>,
128 _: Option<&InspectorElementId>,
129 window: &mut Window,
130 cx: &mut App,
131 ) -> (LayoutId, Self::RequestLayoutState) {
132 let style = Style {
133 size: size(relative(1.0).into(), relative(1.0).into()),
134 ..Style::default()
135 };
136 (window.request_layout(style, None, cx), ())
137 }
138
139 fn prepaint(
140 &mut self,
141 _: Option<&GlobalElementId>,
142 _: Option<&InspectorElementId>,
143 bounds: Bounds<Pixels>,
144 (): &mut Self::RequestLayoutState,
145 window: &mut Window,
146 cx: &mut App,
147 ) -> Self::PrepaintState {
148 let state = window
149 .use_state(cx, |_, _| ResizeState::new(&self.config))
150 .read(cx)
151 .clone();
152 let reset = {
153 let mut inner = state.0.borrow_mut();
154 let changed =
155 inner.source != self.config.source || inner.signal != *self.config.signal.id();
156 if changed {
157 inner.source = self.config.source.clone();
158 inner.signal = self.config.signal.id().clone();
159 }
160 changed
161 };
162 let owner = self.events.interaction_owner(&self.config.id);
163 if reset && !self.events.cancel_interaction(&owner, window, cx) {
164 let events = self.events.clone();
165 let signal = self.config.signal.clone();
166 window.defer(cx, move |_, cx| {
167 let _ = events.write_signal(&signal, SignalValue::OptionalFloat(None), cx);
168 });
169 }
170 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::BlockMouseExceptScroll);
171 ResizePrepaint { hitbox }
172 }
173
174 fn paint(
175 &mut self,
176 _: Option<&GlobalElementId>,
177 _: Option<&InspectorElementId>,
178 bounds: Bounds<Pixels>,
179 (): &mut Self::RequestLayoutState,
180 prepaint: &mut Self::PrepaintState,
181 window: &mut Window,
182 _: &mut App,
183 ) {
184 let owner = self.events.interaction_owner(&self.config.id);
185 self.events.present_interaction(owner.clone());
186 let dragged = self.events.interaction_is_active(&owner);
187 let hovered = prepaint.hitbox.is_hovered(window);
188 let color = if dragged || hovered {
189 self.config.active_color
190 } else {
191 self.config.idle_color
192 };
193 let line_width = if dragged { px(2.0) } else { px(1.0) };
194 let line = Bounds::new(
195 point(
196 bounds.origin.x + (bounds.size.width - line_width) / 2.0,
197 bounds.origin.y + px(4.0),
198 ),
199 size(line_width, (bounds.size.height - px(8.0)).max(px(1.0))),
200 );
201 window.paint_quad(fill(line, rgba(color.as_rgba_hex())));
202 window.set_cursor_style(CursorStyle::ResizeColumn, &prepaint.hitbox);
203 register_pointer_down(
204 prepaint,
205 self.config.clone(),
206 self.events.clone(),
207 self.measurements.clone(),
208 window,
209 );
210 }
211}
212
213fn register_pointer_down(
214 prepaint: &ResizePrepaint,
215 config: ResizeConfig,
216 events: PrimitiveContext,
217 measurements: ColumnMeasurementRegistry,
218 window: &mut Window,
219) {
220 let view = window.current_view();
221 let hitbox = prepaint.hitbox.clone();
222 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
223 if phase != DispatchPhase::Bubble
224 || event.button != MouseButton::Left
225 || !hitbox.is_hovered(window)
226 {
227 return;
228 }
229 let Some(bounds) = events.element_bounds(&config.reference, cx) else {
230 return;
231 };
232 if event.click_count >= 2 {
233 let owner = events.interaction_owner(&config.id);
234 events.cancel_interaction(&owner, window, cx);
235 let measured = measurements
236 .maximum(config.signal.id())
237 .unwrap_or(bounds.width);
238 let width = clamp_width(measured, config.min, config.max);
239 let _ =
240 events.write_signal(&config.signal, SignalValue::OptionalFloat(Some(width)), cx);
241 let payload = resize_payload(&config.column_key, width);
242 events.propose("resize", payload, window, cx);
243 if events.observes("resize") {
244 schedule_preview_clear(&events, &config.signal, window, cx);
245 }
246 cx.stop_propagation();
247 cx.notify(view);
248 return;
249 }
250 let width = clamp_width(bounds.width, config.min, config.max);
251 let previous_override = events.read_signal(&config.signal, cx).ok();
252 let update_config = config.clone();
253 let update_events = events.clone();
254 let update = move |gesture: crate::interaction::GestureUpdate,
255 _: &mut Window,
256 cx: &mut App| {
257 if gesture.moved() {
258 let delta =
259 horizontal_delta(gesture.start(), gesture.current(), update_config.direction);
260 let value = clamp_width(width + delta, update_config.min, update_config.max);
261 let _ = update_events.write_signal(
262 &update_config.signal,
263 SignalValue::OptionalFloat(Some(value)),
264 cx,
265 );
266 }
267 crate::interaction::InteractionFlow::Continue
268 };
269 let finish_config = config.clone();
270 let finish_events = events.clone();
271 let finish =
272 move |gesture: crate::interaction::GestureUpdate, window: &mut Window, cx: &mut App| {
273 if !gesture.moved() {
274 return;
275 }
276 let delta =
277 horizontal_delta(gesture.start(), gesture.current(), finish_config.direction);
278 let value = clamp_width(width + delta, finish_config.min, finish_config.max);
279 let _ = finish_events.write_signal(
280 &finish_config.signal,
281 SignalValue::OptionalFloat(Some(value)),
282 cx,
283 );
284 finish_events.propose(
285 "resize",
286 resize_payload(&finish_config.column_key, value),
287 window,
288 cx,
289 );
290 if finish_events.observes("resize") {
291 schedule_preview_clear(&finish_events, &finish_config.signal, window, cx);
292 }
293 };
294 let cancel_signal = config.signal.clone();
295 let cancel_events = events.clone();
296 let cancel = move |_: &mut Window, cx: &mut App| {
297 let value = previous_override
298 .clone()
299 .unwrap_or(SignalValue::OptionalFloat(None));
300 let _ = cancel_events.write_signal(&cancel_signal, value, cx);
301 };
302 let owner = events.interaction_owner(&config.id);
303 events.begin_interaction(
304 crate::interaction::NativeGesture::new(
305 owner,
306 event.position,
307 view,
308 update,
309 finish,
310 cancel,
311 ),
312 window,
313 cx,
314 );
315 cx.stop_propagation();
316 });
317}
318
319fn schedule_preview_clear(
320 events: &PrimitiveContext,
321 signal: &crate::NativeSignal,
322 window: &mut Window,
323 cx: &mut App,
324) {
325 let events = events.clone();
326 let signal = signal.clone();
327 window.defer(cx, move |_, cx| {
328 let _ = events.write_signal(&signal, SignalValue::OptionalFloat(None), cx);
329 });
330}
331
332fn horizontal_delta(start: Point<Pixels>, current: Point<Pixels>, direction: TextDirection) -> f64 {
333 let delta = f64::from(current.x - start.x);
334 match direction {
335 TextDirection::LeftToRight => delta,
336 TextDirection::RightToLeft => -delta,
337 }
338}
339
340fn clamp_width(width: f64, min: f64, max: f64) -> f64 {
341 width.clamp(min, max)
342}
343
344fn resize_payload(column_key: &str, width: f64) -> UiValue {
345 UiValue::Map(BTreeMap::from([
346 ("key".to_owned(), UiValue::String(column_key.to_owned())),
347 (
348 "width".to_owned(),
349 UiValue::Map(BTreeMap::from([
350 ("kind".to_owned(), UiValue::String("fixed".to_owned())),
351 ("value".to_owned(), UiValue::Float(width)),
352 ])),
353 ),
354 ]))
355}
356
357#[derive(Clone)]
358struct IntrinsicMeasureConfig {
359 text: String,
360 group: SignalId,
361 horizontal_padding: Length,
362 extra_width: f64,
363}
364
365struct IntrinsicTextMeasureElement {
366 instance: u64,
367 config: IntrinsicMeasureConfig,
368 measurements: ColumnMeasurementRegistry,
369}
370
371impl IntoElement for IntrinsicTextMeasureElement {
372 type Element = Self;
373
374 fn into_element(self) -> Self::Element {
375 self
376 }
377}
378
379impl Element for IntrinsicTextMeasureElement {
380 type RequestLayoutState = ();
381 type PrepaintState = ();
382
383 fn id(&self) -> Option<ElementId> {
384 None
385 }
386
387 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
388 None
389 }
390
391 fn request_layout(
392 &mut self,
393 _: Option<&GlobalElementId>,
394 _: Option<&InspectorElementId>,
395 window: &mut Window,
396 cx: &mut App,
397 ) -> (LayoutId, Self::RequestLayoutState) {
398 let style = Style {
399 size: size(px(0.0).into(), px(0.0).into()),
400 ..Style::default()
401 };
402 (window.request_layout(style, None, cx), ())
403 }
404
405 fn prepaint(
406 &mut self,
407 _: Option<&GlobalElementId>,
408 _: Option<&InspectorElementId>,
409 _: Bounds<Pixels>,
410 (): &mut Self::RequestLayoutState,
411 window: &mut Window,
412 _: &mut App,
413 ) -> Self::PrepaintState {
414 let text_style = window.text_style();
415 let font_size = text_style.font_size.to_pixels(window.rem_size());
416 let runs = [text_style.to_run(self.config.text.len())];
417 if let Ok(lines) = window.text_system().shape_text(
418 SharedString::from(self.config.text.clone()),
419 font_size,
420 &runs,
421 None,
422 Some(1),
423 ) {
424 let text_width = lines
425 .iter()
426 .map(|line| f64::from(line.width()))
427 .reduce(f64::max)
428 .unwrap_or(0.0);
429 let padding =
430 absolute_length_pixels(self.config.horizontal_padding, window).unwrap_or_default();
431 let width = (text_width + padding * 2.0 + self.config.extra_width).ceil();
432 if width.is_finite() {
433 self.measurements
434 .report(&self.config.group, self.instance, width);
435 }
436 }
437 }
438
439 fn paint(
440 &mut self,
441 _: Option<&GlobalElementId>,
442 _: Option<&InspectorElementId>,
443 _: Bounds<Pixels>,
444 (): &mut Self::RequestLayoutState,
445 (): &mut Self::PrepaintState,
446 _: &mut Window,
447 _: &mut App,
448 ) {
449 }
450}
451
452fn absolute_length_pixels(value: Length, window: &Window) -> Option<f64> {
453 match value {
454 Length::Pixels(value) => Some(value),
455 Length::Rems(value) => Some(value * f64::from(window.rem_size())),
456 Length::Relative(_) | Length::ThemeSpacing(_) | Length::ThemeRadius(_) => None,
457 }
458}
459
460#[derive(Default)]
461pub(crate) struct IntrinsicTextMeasurePrimitiveHandler {
462 measurements: ColumnMeasurementRegistry,
463 owners: BTreeMap<PrimitiveInstanceId, SignalId>,
464}
465
466impl IntrinsicTextMeasurePrimitiveHandler {
467 pub(crate) fn new(measurements: ColumnMeasurementRegistry) -> Self {
468 Self {
469 measurements,
470 owners: BTreeMap::new(),
471 }
472 }
473}
474
475impl PrimitiveHandler for IntrinsicTextMeasurePrimitiveHandler {
476 fn render(
477 &mut self,
478 instance: &PrimitiveInstance,
479 _: &PrimitiveContext,
480 theme: &PrimitiveTheme,
481 _: &mut Window,
482 _: &mut App,
483 ) -> Result<AnyElement, String> {
484 let id = instance
485 .id
486 .clone()
487 .ok_or_else(|| "intrinsic text measurement requires a keyed instance".to_owned())?;
488 let config = parse_measure_config(&instance.node.props, theme)?;
489 if let Some(previous) = self.owners.insert(id.clone(), config.group.clone())
490 && previous != config.group
491 {
492 self.measurements.remove(&previous, id.node().get());
493 }
494 Ok(IntrinsicTextMeasureElement {
495 instance: id.node().get(),
496 config,
497 measurements: self.measurements.clone(),
498 }
499 .into_any_element())
500 }
501
502 fn unmount(&mut self, instance: &PrimitiveInstanceId) {
503 if let Some(group) = self.owners.remove(instance) {
504 self.measurements.remove(&group, instance.node().get());
505 }
506 }
507}
508
509fn parse_measure_config(
510 props: &PrimitiveProps,
511 theme: &PrimitiveTheme,
512) -> Result<IntrinsicMeasureConfig, String> {
513 let text = props
514 .string("text")
515 .map(ToOwned::to_owned)
516 .ok_or_else(|| "intrinsic text measurement requires text".to_owned())?;
517 let group = props
518 .signal("group")
519 .ok_or_else(|| "intrinsic text measurement requires group".to_owned())?;
520 if group.id().kind() != SignalKind::OptionalFloat {
521 return Err("intrinsic text measurement group must be an optional-float signal".to_owned());
522 }
523 let horizontal_padding = match props.get("horizontal_padding") {
524 Some(PrimitiveValue::Length(value)) => theme
525 .resolve_length(*value)
526 .ok_or_else(|| "intrinsic text measurement padding did not resolve".to_owned())?,
527 None | Some(PrimitiveValue::Data(UiValue::Null)) => Length::Pixels(0.0),
528 Some(_) => return Err("intrinsic text measurement padding must be Length".to_owned()),
529 };
530 if !matches!(horizontal_padding, Length::Pixels(_) | Length::Rems(_)) {
531 return Err("intrinsic text measurement padding must resolve to pixels or rems".to_owned());
532 }
533 let extra_width = props.number("extra_width").unwrap_or(0.0);
534 if !extra_width.is_finite() || !(0.0..=MAX_COLUMN_WIDTH).contains(&extra_width) {
535 return Err(
536 "intrinsic text measurement extra width must be between 0 and 16384".to_owned(),
537 );
538 }
539 Ok(IntrinsicMeasureConfig {
540 text,
541 group: group.id().clone(),
542 horizontal_padding,
543 extra_width,
544 })
545}
546
547#[derive(Default)]
548pub struct ColumnResizePrimitiveHandler {
549 measurements: ColumnMeasurementRegistry,
550}
551
552impl ColumnResizePrimitiveHandler {
553 pub(crate) fn new(measurements: ColumnMeasurementRegistry) -> Self {
554 Self { measurements }
555 }
556}
557
558impl PrimitiveHandler for ColumnResizePrimitiveHandler {
559 fn render(
560 &mut self,
561 instance: &PrimitiveInstance,
562 events: &PrimitiveContext,
563 theme: &PrimitiveTheme,
564 _: &mut Window,
565 _: &mut App,
566 ) -> Result<AnyElement, String> {
567 let config = parse_config(&instance.node.props, theme)?;
568 Ok(ColumnResizeHandle {
569 config,
570 events: events.clone(),
571 measurements: self.measurements.clone(),
572 }
573 .into_any_element())
574 }
575}
576
577fn parse_config(props: &PrimitiveProps, theme: &PrimitiveTheme) -> Result<ResizeConfig, String> {
578 let column_key = props
579 .string("column_key")
580 .map(ToOwned::to_owned)
581 .ok_or_else(|| "column resize handle requires column_key".to_owned())?;
582 let source_kind = props
583 .string("source_kind")
584 .map(ToOwned::to_owned)
585 .ok_or_else(|| "column resize handle requires source_kind".to_owned())?;
586 let source_value = props
587 .number("source_value")
588 .ok_or_else(|| "column resize handle requires source_value".to_owned())?;
589 let min = props.number("min_width").unwrap_or(DEFAULT_MIN_WIDTH);
590 let max = props.number("max_width").unwrap_or(MAX_COLUMN_WIDTH);
591 if !source_value.is_finite()
592 || !min.is_finite()
593 || !max.is_finite()
594 || min <= 0.0
595 || max < min
596 || max > MAX_COLUMN_WIDTH
597 {
598 return Err("column resize widths must be finite and satisfy 0 < min <= max".to_owned());
599 }
600 let signal = props
601 .signal("signal")
602 .cloned()
603 .ok_or_else(|| "column resize handle requires signal".to_owned())?;
604 let reference = props
605 .element_ref("column_ref")
606 .cloned()
607 .ok_or_else(|| "column resize handle requires column_ref".to_owned())?;
608 Ok(ResizeConfig {
609 id: format!(
610 "gpui-rhai-column-resize:{}:{}",
611 signal.id().component(),
612 signal.id().key()
613 ),
614 column_key: column_key.clone(),
615 signal,
616 reference,
617 source: SourceWidth {
618 column_key,
619 kind: source_kind,
620 value: source_value,
621 min,
622 max,
623 },
624 min,
625 max,
626 direction: theme.direction(),
627 idle_color: theme
628 .color("border")
629 .unwrap_or(Rgba8::from_rgba_hex(0x5555_55ff)),
630 active_color: theme
631 .color("accent")
632 .unwrap_or(Rgba8::from_rgba_hex(0x3b82_f6ff)),
633 })
634}
635
636const fn resize_bound_schema() -> ValueSchema {
637 ValueSchema::Number {
638 min: None,
639 max: Some(MAX_COLUMN_WIDTH),
640 exclusive_min: Some(0.0),
641 exclusive_max: None,
642 }
643}
644
645#[must_use]
651pub fn column_resize_primitive_descriptor() -> PrimitiveDescriptor {
652 PrimitiveDescriptor {
653 id: PrimitiveId::parse("gpui_rhai.column_resize").expect("static primitive ID"),
654 export: "ColumnResizePrimitive".to_owned(),
655 props: BTreeMap::from([
656 (
657 "column_key".to_owned(),
658 ObjectField::required(ValueSchema::string()),
659 ),
660 (
661 "source_kind".to_owned(),
662 ObjectField::required(ValueSchema::String {
663 allowed: vec!["fixed".to_owned(), "percent".to_owned(), "flex".to_owned()],
664 }),
665 ),
666 (
667 "source_value".to_owned(),
668 ObjectField::required(ValueSchema::positive_number()),
669 ),
670 (
671 "min_width".to_owned(),
672 ObjectField::optional(resize_bound_schema()),
673 ),
674 (
675 "max_width".to_owned(),
676 ObjectField::optional(resize_bound_schema()),
677 ),
678 (
679 "signal".to_owned(),
680 ObjectField::required(ValueSchema::Signal),
681 ),
682 (
683 "column_ref".to_owned(),
684 ObjectField::required(ValueSchema::Ref),
685 ),
686 (
687 "on_resize".to_owned(),
688 ObjectField::optional(ValueSchema::optional(ValueSchema::Callback)),
689 ),
690 ]),
691 events: BTreeMap::from([(
692 "resize".to_owned(),
693 EventSchema {
694 payload: ValueSchema::object(BTreeMap::from([
695 (
696 "key".to_owned(),
697 ObjectField::required(ValueSchema::string()),
698 ),
699 (
700 "width".to_owned(),
701 ObjectField::required(ValueSchema::object(BTreeMap::from([
702 (
703 "kind".to_owned(),
704 ObjectField::required(ValueSchema::String {
705 allowed: vec!["fixed".to_owned()],
706 }),
707 ),
708 (
709 "value".to_owned(),
710 ObjectField::required(ValueSchema::positive_number()),
711 ),
712 ]))),
713 ),
714 ])),
715 },
716 )]),
717 state: ComponentStateSchema::default(),
718 lifecycle: false,
719 effect: None,
720 }
721}
722
723#[must_use]
730pub(crate) fn intrinsic_text_measure_primitive_descriptor() -> PrimitiveDescriptor {
731 PrimitiveDescriptor {
732 id: PrimitiveId::parse("gpui_rhai.intrinsic_text_measure").expect("static primitive ID"),
733 export: "IntrinsicTextMeasurePrimitive".to_owned(),
734 props: BTreeMap::from([
735 (
736 "text".to_owned(),
737 ObjectField::required(ValueSchema::string()),
738 ),
739 (
740 "group".to_owned(),
741 ObjectField::required(ValueSchema::Signal),
742 ),
743 (
744 "horizontal_padding".to_owned(),
745 ObjectField::optional(ValueSchema::Length),
746 ),
747 (
748 "extra_width".to_owned(),
749 ObjectField::optional(ValueSchema::Number {
750 min: Some(0.0),
751 max: Some(MAX_COLUMN_WIDTH),
752 exclusive_min: None,
753 exclusive_max: None,
754 }),
755 ),
756 ]),
757 events: BTreeMap::new(),
758 state: ComponentStateSchema::default(),
759 lifecycle: true,
760 effect: None,
761 }
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767
768 fn measurement_group() -> SignalId {
769 SignalId::new(
770 crate::ComponentInstancePath::root("Table", "users"),
771 "column-width-0",
772 SignalKind::OptionalFloat,
773 )
774 .unwrap()
775 }
776
777 #[test]
778 fn width_delta_and_clamping_follow_text_direction() {
779 let start = point(px(100.0), px(0.0));
780 let right = point(px(132.0), px(0.0));
781 assert!(
782 (horizontal_delta(start, right, TextDirection::LeftToRight) - 32.0).abs()
783 < f64::EPSILON
784 );
785 assert!(
786 (horizontal_delta(start, right, TextDirection::RightToLeft) + 32.0).abs()
787 < f64::EPSILON
788 );
789 assert!((clamp_width(10.0, 48.0, 200.0) - 48.0).abs() < f64::EPSILON);
790 assert!((clamp_width(240.0, 48.0, 200.0) - 200.0).abs() < f64::EPSILON);
791 }
792
793 #[test]
794 fn resize_event_uses_a_fixed_width_descriptor() {
795 assert_eq!(
796 resize_payload("name", 144.0),
797 UiValue::Map(BTreeMap::from([
798 ("key".to_owned(), UiValue::String("name".to_owned())),
799 (
800 "width".to_owned(),
801 UiValue::Map(BTreeMap::from([
802 ("kind".to_owned(), UiValue::String("fixed".to_owned())),
803 ("value".to_owned(), UiValue::Float(144.0)),
804 ])),
805 ),
806 ]))
807 );
808 }
809
810 #[test]
811 fn realized_measurements_replace_and_unmount_without_retaining_stale_maxima() {
812 let registry = ColumnMeasurementRegistry::default();
813 let group = measurement_group();
814 registry.report(&group, 1, 80.0);
815 registry.report(&group, 2, 144.0);
816 assert_eq!(registry.maximum(&group), Some(144.0));
817
818 registry.report(&group, 2, 96.0);
819 assert_eq!(registry.maximum(&group), Some(96.0));
820 registry.remove(&group, 2);
821 assert_eq!(registry.maximum(&group), Some(80.0));
822 registry.remove(&group, 1);
823 assert_eq!(registry.maximum(&group), None);
824 }
825
826 #[test]
827 fn intrinsic_measurement_is_a_keyed_lifecycle_primitive() {
828 let descriptor = intrinsic_text_measure_primitive_descriptor();
829 assert!(descriptor.lifecycle);
830 assert_eq!(descriptor.id.as_str(), "gpui_rhai.intrinsic_text_measure");
831 }
832}