diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport_widget.rs b/crates/op-editor-ui/src/widgets/canvas_viewport_widget.rs index 213fb8959..e8cc89b3f 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport_widget.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport_widget.rs @@ -19,6 +19,7 @@ use crate::layout_scene::{SceneNode, SceneWidget}; use crate::widgets::PaintCx; use crate::{Color, Point2D, Rect, TextLayout}; +use std::borrow::Cow; /// Accent colour for "on" / filled portions (Tailwind blue-500). const ACCENT: Color = Color::rgb_u8(0x3b, 0x82, 0xf6); @@ -239,17 +240,7 @@ fn paint_text_field(cx: &mut PaintCx<'_>, node: &SceneNode, w: &SceneWidget, r: let stroke_w = node.stroke.map(|s| s.width).unwrap_or(1.0) * zoom; cx.backend.stroke_round_rect(r, radius, TRACK_OFF, stroke_w); - // number_input shows its numeric value when present. - let value = w.value_str.clone().or_else(|| { - (w.kind == "number_input") - .then(|| w.value_num.map(format_number)) - .flatten() - }); - let (text, color) = match value.filter(|s| !s.is_empty()) { - Some(v) => (Some(v), TEXT_VALUE), - None => (w.placeholder.clone().filter(|s| !s.is_empty()), TEXT_MUTED), - }; - if let Some(text) = text { + if let Some((text, color)) = text_field_display_text(w) { let fs = 14.0 * zoom; // text_area top-aligns; single-line inputs vertically centre. let ty = if w.kind == "text_area" { @@ -257,7 +248,7 @@ fn paint_text_field(cx: &mut PaintCx<'_>, node: &SceneNode, w: &SceneWidget, r: } else { y + (h - fs) / 2.0 }; - draw_label(cx, &text, color, x + 8.0 * zoom, ty, fs); + draw_label(cx, text.as_ref(), color, x + 8.0 * zoom, ty, fs); } } @@ -361,6 +352,26 @@ pub(crate) fn option_label<'a>(w: &'a SceneWidget, value: &str) -> Option<&'a st }) } +pub(crate) fn text_field_display_text(w: &SceneWidget) -> Option<(Cow<'_, str>, Color)> { + let value = match w.value_str.as_deref() { + Some(text) => (!text.is_empty()).then_some(Cow::Borrowed(text)), + None if w.kind == "number_input" => w + .value_num + .map(format_number) + .filter(|text| !text.is_empty()) + .map(Cow::Owned), + None => None, + }; + match value { + Some(text) => Some((text, TEXT_VALUE)), + None => w + .placeholder + .as_deref() + .filter(|text| !text.is_empty()) + .map(|text| (Cow::Borrowed(text), TEXT_MUTED)), + } +} + /// Format a slider / number value without a trailing `.0` for integers. fn format_number(v: f32) -> String { if v.fract().abs() < f32::EPSILON { diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport_widget_tests.rs b/crates/op-editor-ui/src/widgets/canvas_viewport_widget_tests.rs index d9a2ffba9..fc053b153 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport_widget_tests.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport_widget_tests.rs @@ -3,9 +3,12 @@ //! design surface (track + knob, box + check, bar, chevron, …). use crate::layout_scene::{NodeKind, SceneNode, SceneWidget, SceneWidgetOption}; -use crate::widgets::canvas_viewport_widget::{option_label, paint_widget_visual}; +use crate::widgets::canvas_viewport_widget::{ + option_label, paint_widget_visual, text_field_display_text, +}; use crate::widgets::PaintCx; use crate::{Color, ImageDrawMode, Point2D, Rect, RenderBackend, TextLayout}; +use std::borrow::Cow; /// Recording backend — captures round-rects (rect + fill colour), /// stroke lines (endpoints + colour), and text runs (content + origin). @@ -334,6 +337,36 @@ fn select_option_label_borrows_matching_option_text() { )); } +#[test] +fn text_field_display_text_borrows_value_and_placeholder() { + let with_value = SceneWidget { + value_str: Some("hello".into()), + placeholder: Some("Type here".into()), + ..Default::default() + }; + let (value, _) = text_field_display_text(&with_value).expect("value text"); + match value { + Cow::Borrowed(text) => assert!(std::ptr::eq( + text.as_ptr(), + with_value.value_str.as_deref().unwrap().as_ptr() + )), + Cow::Owned(_) => panic!("value_str should be borrowed during paint"), + } + + let with_placeholder = SceneWidget { + placeholder: Some("Type here".into()), + ..Default::default() + }; + let (placeholder, _) = text_field_display_text(&with_placeholder).expect("placeholder text"); + match placeholder { + Cow::Borrowed(text) => assert!(std::ptr::eq( + text.as_ptr(), + with_placeholder.placeholder.as_deref().unwrap().as_ptr() + )), + Cow::Owned(_) => panic!("placeholder should be borrowed during paint"), + } +} + #[test] fn radio_group_paints_circle_and_dot_for_selected() { let rect = Rect::xywh(0.0, 0.0, 120.0, 56.0);