perf(canvas): avoid text field paint allocations

This commit is contained in:
Kayshen-X 2026-06-15 03:44:50 +08:00
parent 5788344cb0
commit c3660e9ba2
2 changed files with 57 additions and 13 deletions

View file

@ -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 {

View file

@ -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);