use super::super::frame::*;
use super::super::paint_helpers::*;
use super::super::prelude::*;
use super::ElementHostWidget;
use super::{CachedPreparedTextByWidth, interactive_resize_text_width_cache_entries};
use fret_core::time::Instant;
fn push_attributed_span_background_quads(
scene: &mut fret_core::Scene,
services: &mut dyn fret_core::TextService,
blob: fret_core::TextBlobId,
spans: &[fret_core::TextSpan],
clip: Rect,
bounds_origin: Point,
vertical_offset: Px,
) {
let mut bg_runs: Vec<(usize, usize, Color)> = Vec::new();
let mut rects: Vec<fret_core::Rect> = Vec::new();
let mut offset = 0usize;
let mut active_bg: Option<(usize, Color)> = None;
for span in spans {
let end = offset.saturating_add(span.len);
match (active_bg, span.paint.bg) {
(Some((start, bg)), Some(next)) if bg == next => {}
(Some((start, bg)), Some(next)) => {
if start < offset {
bg_runs.push((start, offset, bg));
}
active_bg = Some((offset, next));
}
(Some((start, bg)), None) => {
if start < offset {
bg_runs.push((start, offset, bg));
}
active_bg = None;
}
(None, Some(next)) => {
active_bg = Some((offset, next));
}
(None, None) => {}
}
offset = end;
}
if let Some((start, bg)) = active_bg
&& start < offset
{
bg_runs.push((start, offset, bg));
}
for (start, end, bg) in bg_runs {
if start >= end {
continue;
}
rects.clear();
services.selection_rects_clipped(blob, (start, end), clip, &mut rects);
for r in rects.iter() {
let rect = fret_core::Rect::new(
fret_core::Point::new(
fret_core::Px(bounds_origin.x.0 + r.origin.x.0),
fret_core::Px(bounds_origin.y.0 + vertical_offset.0 + r.origin.y.0),
),
r.size,
);
scene.push(SceneOp::Quad {
order: DrawOrder(0),
rect,
background: Paint::Solid(bg).into(),
border: fret_core::Edges::all(fret_core::Px(0.0)),
border_paint: Paint::Solid(Color::TRANSPARENT).into(),
corner_radii: fret_core::Corners::all(fret_core::Px(0.0)),
});
}
}
}
impl ElementHostWidget {
pub(super) fn paint_impl<H: UiHost>(&mut self, cx: &mut PaintCx<'_, H>) {
let _element_id = self.element;
let Some(window) = cx.window else {
return;
};
let debug_enabled = cx.tree.debug_enabled();
let total_started = debug_enabled.then(Instant::now);
let (models_len, models_loop, globals_len, globals_loop) =
crate::elements::with_observed_deps_for_element(
cx.app,
window,
self.element,
|models, globals| {
let models_started = debug_enabled.then(Instant::now);
for &(model, invalidation) in models {
(cx.observe_model)(model, invalidation);
}
let models_loop = models_started.map(|started| started.elapsed());
let globals_started = debug_enabled.then(Instant::now);
for &(global, invalidation) in globals {
(cx.observe_global)(global, invalidation);
}
let globals_loop = globals_started.map(|started| started.elapsed());
(models.len(), models_loop, globals.len(), globals_loop)
},
);
if debug_enabled {
let total_elapsed = total_started.map(|started| started.elapsed());
let models_loop = models_loop.unwrap_or_default();
let globals_loop = globals_loop.unwrap_or_default();
let overhead = total_elapsed
.unwrap_or_default()
.saturating_sub(models_loop.saturating_add(globals_loop));
let overhead_models = overhead / 2;
let overhead_globals = overhead.saturating_sub(overhead_models);
cx.tree.debug_record_paint_host_widget_observed_models(
models_loop.saturating_add(overhead_models),
models_len,
);
cx.tree.debug_record_paint_host_widget_observed_globals(
globals_loop.saturating_add(overhead_globals),
globals_len,
);
}
let instance_started = cx.tree.debug_enabled().then(Instant::now);
let record = with_element_record_for_node(cx.app, window, cx.node, Clone::clone);
if let Some(instance_started) = instance_started {
cx.tree
.debug_record_paint_host_widget_instance_lookup(instance_started.elapsed());
}
let Some(record) = record else {
return;
};
let inherited_foreground = record.inherited_foreground;
let inherited_text_style = record.inherited_text_style;
let instance = record.instance;
with_scoped_foreground(cx, inherited_foreground, |cx| match instance {
ElementInstance::Container(props) => {
let bounds = if props.snap_to_device_pixels {
crate::pixel_snap::snap_rect_edges_round(cx.bounds, cx.scale_factor)
} else {
cx.bounds
};
let should_draw = props.shadow.is_some()
|| props.background.is_some()
|| props.background_paint.is_some()
|| props.border_color.is_some()
|| props.border_paint.is_some()
|| props.border != Edges::all(Px(0.0));
if should_draw {
if let Some(shadow) = props.shadow {
crate::paint::paint_shadow(cx.scene, DrawOrder(0), bounds, shadow);
}
let background = props.background_paint.unwrap_or_else(|| {
Paint::Solid(props.background.unwrap_or(Color::TRANSPARENT))
});
let border_paint = props.border_paint.unwrap_or_else(|| {
Paint::Solid(props.border_color.unwrap_or(Color::TRANSPARENT))
});
if let Some(dash) = props.border_dash
&& props.border != Edges::all(Px(0.0))
{
cx.scene.push(SceneOp::Quad {
order: DrawOrder(0),
rect: bounds,
background: background.into(),
border: Edges::all(Px(0.0)),
border_paint: Paint::Solid(Color::TRANSPARENT).into(),
corner_radii: props.corner_radii,
});
cx.scene.push(SceneOp::StrokeRRect {
order: DrawOrder(0),
rect: bounds,
corner_radii: props.corner_radii,
stroke: props.border,
stroke_paint: border_paint.into(),
style: fret_core::scene::StrokeStyleV1 { dash: Some(dash) },
});
} else {
cx.scene.push(SceneOp::Quad {
order: DrawOrder(0),
rect: bounds,
background: background.into(),
border: props.border,
border_paint: border_paint.into(),
corner_radii: props.corner_radii,
});
}
}
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
Some(props.corner_radii),
);
let focused = cx.focus.is_some_and(|focus| {
if props.focus_within {
cx.tree.is_descendant(cx.node, focus)
} else {
focus == cx.node
}
});
let focus_visible = crate::focus_visible::is_focus_visible(cx.app, cx.window);
let paint_focus_chrome = focused && focus_visible;
if paint_focus_chrome && let Some(border_color) = props.focus_border_color {
if let Some(dash) = props.border_dash
&& props.border != Edges::all(Px(0.0))
{
cx.scene.push(SceneOp::StrokeRRect {
order: DrawOrder(1),
rect: bounds,
corner_radii: props.corner_radii,
stroke: props.border,
stroke_paint: Paint::Solid(border_color).into(),
style: fret_core::scene::StrokeStyleV1 { dash: Some(dash) },
});
} else {
cx.scene.push(SceneOp::Quad {
order: DrawOrder(1),
rect: bounds,
background: Paint::Solid(Color::TRANSPARENT).into(),
border: props.border,
border_paint: Paint::Solid(border_color).into(),
corner_radii: props.corner_radii,
});
}
}
if let Some(ring) = props.focus_ring
&& (paint_focus_chrome || props.focus_ring_always_paint)
{
crate::paint::paint_focus_ring(cx.scene, DrawOrder(2), bounds, ring);
}
}
ElementInstance::Semantics(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::SemanticFlex(props) => {
paint_children_clipped_if(
cx,
matches!(props.flex.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::ViewCache(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
#[cfg(feature = "unstable-retained-bridge")]
ElementInstance::RetainedSubtree(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::FocusScope(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::LayoutQueryRegion(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::InteractivityGate(props) => {
if !props.present {
return;
}
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::HitTestGate(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::FocusTraversalGate(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::ForegroundScope(props) => {
with_scoped_foreground(cx, props.foreground, |cx| {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
});
}
ElementInstance::Opacity(props) => {
let opacity = props.opacity.clamp(0.0, 1.0);
if opacity <= 0.0 {
return;
}
if opacity < 1.0 {
cx.scene.push(SceneOp::PushOpacity { opacity });
}
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
if opacity < 1.0 {
cx.scene.push(SceneOp::PopOpacity);
}
}
ElementInstance::MaskLayer(props) => {
let mask = props.mask.sanitize();
if let Some(mask) = mask {
cx.scene.push(SceneOp::PushMask {
bounds: cx.bounds,
mask,
});
}
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
if mask.is_some() {
cx.scene.push(SceneOp::PopMask);
}
}
ElementInstance::CompositeGroup(props) => {
let desc =
fret_core::scene::CompositeGroupDesc::new(cx.bounds, props.mode, props.quality);
cx.scene.push(SceneOp::PushCompositeGroup { desc });
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
cx.scene.push(SceneOp::PopCompositeGroup);
}
ElementInstance::EffectLayer(props) => {
if !props.chain.is_empty() {
cx.scene.push(SceneOp::PushEffect {
bounds: cx.bounds,
mode: props.mode,
chain: props.chain,
quality: props.quality,
});
}
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
if !props.chain.is_empty() {
cx.scene.push(SceneOp::PopEffect);
}
}
ElementInstance::BackdropSourceGroup(props) => {
let pyramid = props.pyramid.map(|req| {
let max_levels = req.max_levels.max(1).min(7);
let max_radius_px = if req.max_radius_px.0.is_finite() {
fret_core::Px(req.max_radius_px.0.max(0.0))
} else {
fret_core::Px(0.0)
};
fret_core::scene::CustomEffectPyramidRequestV1 {
max_levels,
max_radius_px,
}
});
cx.scene.push(SceneOp::PushBackdropSourceGroupV1 {
bounds: cx.bounds,
pyramid,
quality: props.quality,
});
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
cx.scene.push(SceneOp::PopBackdropSourceGroup);
}
ElementInstance::VisualTransform(props) => {
let local = props.transform;
let is_finite = local.a.is_finite()
&& local.b.is_finite()
&& local.c.is_finite()
&& local.d.is_finite()
&& local.tx.is_finite()
&& local.ty.is_finite();
let needs_push = is_finite && local != Transform2D::IDENTITY;
if needs_push {
let origin = cx.bounds.origin;
let to_origin = Transform2D::translation(origin);
let from_origin =
Transform2D::translation(Point::new(Px(-origin.x.0), Px(-origin.y.0)));
let transform = to_origin * local * from_origin;
cx.scene.push(SceneOp::PushTransform { transform });
}
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
if needs_push {
cx.scene.push(SceneOp::PopTransform);
}
}
ElementInstance::RenderTransform(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::FractionalRenderTransform(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::Anchored(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::DismissibleLayer(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::Stack(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::Flex(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::RovingFlex(props) => {
paint_children_clipped_if(
cx,
matches!(props.flex.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::Grid(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
}
ElementInstance::Spacer(_props) => {}
ElementInstance::Pressable(props) => {
paint_children_clipped_if(
cx,
matches!(props.layout.overflow, Overflow::Clip),
None,
);
if props.enabled
&& cx.focus == Some(cx.node)
&& crate::focus_visible::is_focus_visible(cx.app, cx.window)
{
if let Some(ring) = props.focus_ring {
let bounds = props.focus_ring_bounds.map_or(cx.bounds, |b| {
Rect::new(
Point::new(
cx.bounds.origin.x + b.origin.x,
cx.bounds.origin.y + b.origin.y,
),
b.size,
)
});
crate::paint::paint_focus_ring(cx.scene, DrawOrder(0), bounds, ring);
}
} else if props.focus_ring_always_paint {
let bounds = props.focus_ring_bounds.map_or(cx.bounds, |b| {
Rect::new(
Point::new(
cx.bounds.origin.x + b.origin.x,
cx.bounds.origin.y + b.origin.y,
),
b.size,
)
});
if let Some(ring) = props.focus_ring {
crate::paint::paint_focus_ring(cx.scene, DrawOrder(0), bounds, ring);
}
}
}
ElementInstance::Text(props) => {
cx.observe_global::<fret_runtime::TextFontStackKey>(Invalidation::Layout);
let font_stack_key = cx
.app
.global::<fret_runtime::TextFontStackKey>()
.map(|k| k.0)
.unwrap_or(0);
let theme = cx.theme().snapshot();
let style =
props.resolved_text_style_with_inherited(theme, inherited_text_style.as_ref());
let input = props.build_text_input_with_style(style.clone());
let color = props
.color
.or(cx.paint_style.foreground)
.or_else(|| cx.theme().color_by_key("foreground"))
.unwrap_or(cx.theme().colors.text_primary);
let max_width =
crate::pixel_snap::snap_px_ceil(cx.bounds.size.width, cx.scale_factor);
let max_width = cx.tree.maybe_bucket_text_wrap_width(props.wrap, max_width);
let constraints = TextConstraints {
max_width: Some(max_width),
wrap: props.wrap,
overflow: props.overflow,
align: props.align,
scale_factor: cx.scale_factor,
};
cx.tree
.debug_record_text_constraints_prepared(cx.node, constraints);
let scale_bits = cx.scale_factor.to_bits();
let width_cache_entries = if cx.tree.interactive_resize_active()
&& !matches!(props.wrap, fret_core::TextWrap::None)
&& cx.tree.interactive_resize_is_small_step()
{
interactive_resize_text_width_cache_entries()
} else {
0
};
if width_cache_entries <= 1 {
self.text_cache.release_prepared_by_width(cx.services);
}
let blob_missing = self.text_cache.blob.is_none();
let scale_changed = self.text_cache.prepared_scale_factor_bits != Some(scale_bits);
let text_changed = self.text_cache.last_text.as_ref() != Some(&props.text);
let style_changed = self.text_cache.last_style.as_ref() != Some(&style);
let wrap_changed = self.text_cache.last_wrap != Some(props.wrap);
let overflow_changed = self.text_cache.last_overflow != Some(props.overflow);
let align_changed = self.text_cache.last_align != Some(props.align);
let ink_overflow_changed =
self.text_cache.last_ink_overflow != Some(props.ink_overflow);
let width_changed = self.text_cache.last_width != Some(max_width);
let font_stack_changed =
self.text_cache.last_font_stack_key != Some(font_stack_key);
let signature_changed = scale_changed
|| text_changed
|| style_changed
|| wrap_changed
|| overflow_changed
|| align_changed
|| ink_overflow_changed
|| font_stack_changed;
if signature_changed {
self.text_cache.release_prepared_by_width(cx.services);
}
let mut needs_prepare = blob_missing
|| scale_changed
|| text_changed
|| style_changed
|| wrap_changed
|| overflow_changed
|| align_changed
|| ink_overflow_changed
|| width_changed
|| font_stack_changed;
let reasons_mask = (blob_missing as u16)
| ((scale_changed as u16) << 1)
| ((text_changed as u16) << 2)
| ((false as u16) << 3)
| ((style_changed as u16) << 4)
| ((wrap_changed as u16) << 5)
| ((overflow_changed as u16) << 6)
| ((width_changed as u16) << 7)
| ((font_stack_changed as u16) << 8);
if needs_prepare
&& width_cache_entries > 1
&& width_changed
&& !signature_changed
&& !blob_missing
&& let Some(cached) = self.text_cache.take_prepared_for_width(max_width)
{
if let Some(prev_width) = self.text_cache.last_width
&& prev_width != max_width
&& let (Some(prev_blob), Some(prev_metrics)) =
(self.text_cache.blob.take(), self.text_cache.metrics.take())
{
self.text_cache.push_prepared_for_width(
cx.services,
width_cache_entries,
CachedPreparedTextByWidth {
width: prev_width,
blob: prev_blob,
metrics: prev_metrics,
ink_pad_top: self.text_cache.ink_pad_top,
ink_pad_bottom: self.text_cache.ink_pad_bottom,
},
);
}
self.text_cache.blob = Some(cached.blob);
self.text_cache.metrics = Some(cached.metrics);
self.text_cache.ink_pad_top = cached.ink_pad_top;
self.text_cache.ink_pad_bottom = cached.ink_pad_bottom;
self.text_cache.last_width = Some(max_width);
needs_prepare = false;
}
if needs_prepare {
if cx.tree.debug_enabled() {
cx.tree.debug_record_paint_text_prepare_reasons(
blob_missing,
scale_changed,
text_changed,
false,
style_changed,
wrap_changed,
overflow_changed,
width_changed,
font_stack_changed,
);
}
if let Some(blob) = self.text_cache.blob.take() {
if width_cache_entries > 1
&& width_changed
&& !signature_changed
&& let Some(prev_width) = self.text_cache.last_width
&& prev_width != max_width
&& let Some(prev_metrics) = self.text_cache.metrics.take()
{
self.text_cache.push_prepared_for_width(
cx.services,
width_cache_entries,
CachedPreparedTextByWidth {
width: prev_width,
blob,
metrics: prev_metrics,
ink_pad_top: self.text_cache.ink_pad_top,
ink_pad_bottom: self.text_cache.ink_pad_bottom,
},
);
} else {
cx.services.text().release(blob);
}
}
let prepare_started = cx.tree.debug_enabled().then(Instant::now);
let (blob, metrics) = cx.services.text().prepare(&input, constraints);
if let Some(prepare_started) = prepare_started {
let elapsed = prepare_started.elapsed();
cx.tree.debug_record_paint_text_prepare(elapsed);
cx.tree.debug_record_paint_text_prepare_hotspot(
cx.node,
Some(self.element),
"Text",
props.text.len().min(u32::MAX as usize) as u32,
constraints,
reasons_mask,
elapsed,
);
}
self.text_cache.blob = Some(blob);
self.text_cache.metrics = Some(metrics);
self.text_cache.prepared_scale_factor_bits = Some(scale_bits);
self.text_cache.last_text = Some(props.text.clone());
self.text_cache.last_style = Some(style.clone());
self.text_cache.last_wrap = Some(props.wrap);
self.text_cache.last_overflow = Some(props.overflow);
self.text_cache.last_align = Some(props.align);
self.text_cache.last_width = Some(max_width);
self.text_cache.last_font_stack_key = Some(font_stack_key);
self.text_cache.last_ink_overflow = Some(props.ink_overflow);
let (pad_top, pad_bottom) =
if props.ink_overflow == crate::element::TextInkOverflow::AutoPad {
crate::text::coords::compute_text_ink_overflow_padding(
cx.services.text(),
blob,
)
} else {
(Px(0.0), Px(0.0))
};
self.text_cache.ink_pad_top = pad_top;
self.text_cache.ink_pad_bottom = pad_bottom;
}
let Some(blob) = self.text_cache.blob else {
return;
};
let Some(metrics) = self.text_cache.metrics else {
return;
};
let (pad_top, pad_bottom) =
crate::text::coords::clamp_text_ink_overflow_padding_to_bounds(
metrics.size.height,
cx.bounds.size.height,
self.text_cache.ink_pad_top,
self.text_cache.ink_pad_bottom,
);
let bounds = fret_core::Rect::new(
fret_core::Point::new(cx.bounds.origin.x, Px(cx.bounds.origin.y.0 + pad_top.0)),
fret_core::Size::new(
cx.bounds.size.width,
Px((cx.bounds.size.height.0 - pad_top.0 - pad_bottom.0).max(0.0)),
),
);
let (mapping, _, baseline) =
crate::text::coords::compute_text_box_mapping_for_vertical_placement(
cx.services.text(),
blob,
bounds,
metrics,
style.vertical_placement,
);
let origin = mapping.baseline_origin(baseline);
cx.scene.push(SceneOp::Text {
order: DrawOrder(0),
origin,
text: blob,
paint: fret_core::scene::Paint::Solid(color).into(),
outline: None,
shadow: None,
});
}
ElementInstance::StyledText(props) => {
cx.observe_global::<fret_runtime::TextFontStackKey>(Invalidation::Layout);
let font_stack_key = cx
.app
.global::<fret_runtime::TextFontStackKey>()
.map(|k| k.0)
.unwrap_or(0);
let theme = cx.theme().snapshot();
let style =
props.resolved_text_style_with_inherited(theme, inherited_text_style.as_ref());
let input = props.build_text_input_with_style(style.clone());
let color = props
.color
.or(cx.paint_style.foreground)
.or_else(|| cx.theme().color_by_key("foreground"))
.unwrap_or(cx.theme().colors.text_primary);
let max_width =
crate::pixel_snap::snap_px_ceil(cx.bounds.size.width, cx.scale_factor);
let max_width = cx.tree.maybe_bucket_text_wrap_width(props.wrap, max_width);
let constraints = TextConstraints {
max_width: Some(max_width),
wrap: props.wrap,
overflow: props.overflow,
align: props.align,
scale_factor: cx.scale_factor,
};
cx.tree
.debug_record_text_constraints_prepared(cx.node, constraints);
let scale_bits = cx.scale_factor.to_bits();
let width_cache_entries = if cx.tree.interactive_resize_active()
&& !matches!(props.wrap, fret_core::TextWrap::None)
&& cx.tree.interactive_resize_is_small_step()
{
interactive_resize_text_width_cache_entries()
} else {
0
};
if width_cache_entries <= 1 {
self.text_cache.release_prepared_by_width(cx.services);
}
let blob_missing = self.text_cache.blob.is_none();
let scale_changed = self.text_cache.prepared_scale_factor_bits != Some(scale_bits);
let rich_changed = self.text_cache.last_rich.as_ref() != Some(&props.rich);
let style_changed = self.text_cache.last_style.as_ref() != Some(&style);
let wrap_changed = self.text_cache.last_wrap != Some(props.wrap);
let overflow_changed = self.text_cache.last_overflow != Some(props.overflow);
let align_changed = self.text_cache.last_align != Some(props.align);
let ink_overflow_changed =
self.text_cache.last_ink_overflow != Some(props.ink_overflow);
let width_changed = self.text_cache.last_width != Some(max_width);
let font_stack_changed =
self.text_cache.last_font_stack_key != Some(font_stack_key);
let signature_changed = scale_changed
|| rich_changed
|| style_changed
|| wrap_changed
|| overflow_changed
|| align_changed
|| ink_overflow_changed
|| font_stack_changed;
if signature_changed {
self.text_cache.release_prepared_by_width(cx.services);
}
let mut needs_prepare = blob_missing
|| scale_changed
|| rich_changed
|| style_changed
|| wrap_changed
|| overflow_changed
|| align_changed
|| ink_overflow_changed
|| width_changed
|| font_stack_changed;
let reasons_mask = (blob_missing as u16)
| ((scale_changed as u16) << 1)
| ((false as u16) << 2)
| ((rich_changed as u16) << 3)
| ((style_changed as u16) << 4)
| ((wrap_changed as u16) << 5)
| ((overflow_changed as u16) << 6)
| ((width_changed as u16) << 7)
| ((font_stack_changed as u16) << 8);
if needs_prepare
&& width_cache_entries > 1
&& width_changed
&& !signature_changed
&& !blob_missing
&& let Some(cached) = self.text_cache.take_prepared_for_width(max_width)
{
if let Some(prev_width) = self.text_cache.last_width
&& prev_width != max_width
&& let (Some(prev_blob), Some(prev_metrics)) =
(self.text_cache.blob.take(), self.text_cache.metrics.take())
{
self.text_cache.push_prepared_for_width(
cx.services,
width_cache_entries,
CachedPreparedTextByWidth {
width: prev_width,
blob: prev_blob,
metrics: prev_metrics,
ink_pad_top: self.text_cache.ink_pad_top,
ink_pad_bottom: self.text_cache.ink_pad_bottom,
},
);
}
self.text_cache.blob = Some(cached.blob);
self.text_cache.metrics = Some(cached.metrics);
self.text_cache.ink_pad_top = cached.ink_pad_top;
self.text_cache.ink_pad_bottom = cached.ink_pad_bottom;
self.text_cache.last_width = Some(max_width);
needs_prepare = false;
}
if needs_prepare {
if cx.tree.debug_enabled() {
cx.tree.debug_record_paint_text_prepare_reasons(
blob_missing,
scale_changed,
false,
rich_changed,
style_changed,
wrap_changed,
overflow_changed,
width_changed,
font_stack_changed,
);
}
if let Some(blob) = self.text_cache.blob.take() {
if width_cache_entries > 1
&& width_changed
&& !signature_changed
&& let Some(prev_width) = self.text_cache.last_width
&& prev_width != max_width
&& let Some(prev_metrics) = self.text_cache.metrics.take()
{
self.text_cache.push_prepared_for_width(
cx.services,
width_cache_entries,
CachedPreparedTextByWidth {
width: prev_width,
blob,
metrics: prev_metrics,
ink_pad_top: self.text_cache.ink_pad_top,
ink_pad_bottom: self.text_cache.ink_pad_bottom,
},
);
} else {
cx.services.text().release(blob);
}
}
let prepare_started = cx.tree.debug_enabled().then(Instant::now);
let (blob, metrics) = cx.services.text().prepare(&input, constraints);
if let Some(prepare_started) = prepare_started {
let elapsed = prepare_started.elapsed();
cx.tree.debug_record_paint_text_prepare(elapsed);
cx.tree.debug_record_paint_text_prepare_hotspot(
cx.node,
Some(self.element),
"StyledText",
props.rich.text.len().min(u32::MAX as usize) as u32,
constraints,
reasons_mask,
elapsed,
);
}
self.text_cache.blob = Some(blob);
self.text_cache.metrics = Some(metrics);
self.text_cache.prepared_scale_factor_bits = Some(scale_bits);
self.text_cache.last_text = None;
self.text_cache.last_rich = Some(props.rich.clone());
self.text_cache.last_style = Some(style.clone());
self.text_cache.last_wrap = Some(props.wrap);
self.text_cache.last_overflow = Some(props.overflow);
self.text_cache.last_align = Some(props.align);
self.text_cache.last_width = Some(max_width);
self.text_cache.last_font_stack_key = Some(font_stack_key);
self.text_cache.last_ink_overflow = Some(props.ink_overflow);
let (pad_top, pad_bottom) =
if props.ink_overflow == crate::element::TextInkOverflow::AutoPad {
crate::text::coords::compute_text_ink_overflow_padding(
cx.services.text(),
blob,
)
} else {
(Px(0.0), Px(0.0))
};
self.text_cache.ink_pad_top = pad_top;
self.text_cache.ink_pad_bottom = pad_bottom;
}
let Some(blob) = self.text_cache.blob else {
return;
};
let Some(metrics) = self.text_cache.metrics else {
return;
};
let (pad_top, pad_bottom) =
crate::text::coords::clamp_text_ink_overflow_padding_to_bounds(
metrics.size.height,
cx.bounds.size.height,
self.text_cache.ink_pad_top,
self.text_cache.ink_pad_bottom,
);
let bounds = fret_core::Rect::new(
fret_core::Point::new(cx.bounds.origin.x, Px(cx.bounds.origin.y.0 + pad_top.0)),
fret_core::Size::new(
cx.bounds.size.width,
Px((cx.bounds.size.height.0 - pad_top.0 - pad_bottom.0).max(0.0)),
),
);
let (mapping, vertical_offset, baseline) =
crate::text::coords::compute_text_box_mapping_for_vertical_placement(
cx.services.text(),
blob,
bounds,
metrics,
style.vertical_placement,
);
let clip = fret_core::Rect::new(
fret_core::Point::new(fret_core::Px(0.0), fret_core::Px(0.0)),
bounds.size,
);
push_attributed_span_background_quads(
cx.scene,
cx.services,
blob,
props.rich.spans.as_ref(),
clip,
bounds.origin,
vertical_offset,
);
let origin = mapping.baseline_origin(baseline);
cx.scene.push(SceneOp::Text {
order: DrawOrder(0),
origin,
text: blob,
paint: fret_core::scene::Paint::Solid(color).into(),
outline: None,
shadow: None,
});
}
ElementInstance::SelectableText(props) => {
cx.observe_global::<fret_runtime::TextFontStackKey>(Invalidation::Layout);
let font_stack_key = cx
.app
.global::<fret_runtime::TextFontStackKey>()
.map(|k| k.0)
.unwrap_or(0);
let theme = cx.theme().snapshot();
let style =
props.resolved_text_style_with_inherited(theme, inherited_text_style.as_ref());
let input = props.build_text_input_with_style(style.clone());
let color = props
.color
.or(cx.paint_style.foreground)
.or_else(|| cx.theme().color_by_key("foreground"))
.unwrap_or(cx.theme().colors.text_primary);
let max_width =
crate::pixel_snap::snap_px_ceil(cx.bounds.size.width, cx.scale_factor);
let max_width = cx.tree.maybe_bucket_text_wrap_width(props.wrap, max_width);
let constraints = TextConstraints {
max_width: Some(max_width),
wrap: props.wrap,
overflow: props.overflow,
align: props.align,
scale_factor: cx.scale_factor,
};
cx.tree
.debug_record_text_constraints_prepared(cx.node, constraints);
let scale_bits = cx.scale_factor.to_bits();
let width_cache_entries = if cx.tree.interactive_resize_active()
&& !matches!(props.wrap, fret_core::TextWrap::None)
&& cx.tree.interactive_resize_is_small_step()
{
interactive_resize_text_width_cache_entries()
} else {
0
};
if width_cache_entries <= 1 {
self.text_cache.release_prepared_by_width(cx.services);
}
let blob_missing = self.text_cache.blob.is_none();
let scale_changed = self.text_cache.prepared_scale_factor_bits != Some(scale_bits);
let rich_changed = self.text_cache.last_rich.as_ref() != Some(&props.rich);
let style_changed = self.text_cache.last_style.as_ref() != Some(&style);
let wrap_changed = self.text_cache.last_wrap != Some(props.wrap);
let overflow_changed = self.text_cache.last_overflow != Some(props.overflow);
let align_changed = self.text_cache.last_align != Some(props.align);
let ink_overflow_changed =
self.text_cache.last_ink_overflow != Some(props.ink_overflow);
let width_changed = self.text_cache.last_width != Some(max_width);
let font_stack_changed =
self.text_cache.last_font_stack_key != Some(font_stack_key);
let signature_changed = scale_changed
|| rich_changed
|| style_changed
|| wrap_changed
|| overflow_changed
|| align_changed
|| ink_overflow_changed
|| font_stack_changed;
if signature_changed {
self.text_cache.release_prepared_by_width(cx.services);
}
let mut needs_prepare = blob_missing
|| scale_changed
|| rich_changed
|| style_changed
|| wrap_changed
|| overflow_changed
|| align_changed
|| ink_overflow_changed
|| width_changed
|| font_stack_changed;
let reasons_mask = (blob_missing as u16)
| ((scale_changed as u16) << 1)
| ((false as u16) << 2)
| ((rich_changed as u16) << 3)
| ((style_changed as u16) << 4)
| ((wrap_changed as u16) << 5)
| ((overflow_changed as u16) << 6)
| ((width_changed as u16) << 7)
| ((font_stack_changed as u16) << 8);
if needs_prepare
&& width_cache_entries > 1
&& width_changed
&& !signature_changed
&& !blob_missing
&& let Some(cached) = self.text_cache.take_prepared_for_width(max_width)
{
if let Some(prev_width) = self.text_cache.last_width
&& prev_width != max_width
&& let (Some(prev_blob), Some(prev_metrics)) =
(self.text_cache.blob.take(), self.text_cache.metrics.take())
{
self.text_cache.push_prepared_for_width(
cx.services,
width_cache_entries,
CachedPreparedTextByWidth {
width: prev_width,
blob: prev_blob,
metrics: prev_metrics,
ink_pad_top: self.text_cache.ink_pad_top,
ink_pad_bottom: self.text_cache.ink_pad_bottom,
},
);
}
self.text_cache.blob = Some(cached.blob);
self.text_cache.metrics = Some(cached.metrics);
self.text_cache.ink_pad_top = cached.ink_pad_top;
self.text_cache.ink_pad_bottom = cached.ink_pad_bottom;
self.text_cache.last_width = Some(max_width);
needs_prepare = false;
}
if needs_prepare {
if cx.tree.debug_enabled() {
cx.tree.debug_record_paint_text_prepare_reasons(
blob_missing,
scale_changed,
false,
rich_changed,
style_changed,
wrap_changed,
overflow_changed,
width_changed,
font_stack_changed,
);
}
if let Some(blob) = self.text_cache.blob.take() {
if width_cache_entries > 1
&& width_changed
&& !signature_changed
&& let Some(prev_width) = self.text_cache.last_width
&& prev_width != max_width
&& let Some(prev_metrics) = self.text_cache.metrics.take()
{
self.text_cache.push_prepared_for_width(
cx.services,
width_cache_entries,
CachedPreparedTextByWidth {
width: prev_width,
blob,
metrics: prev_metrics,
ink_pad_top: self.text_cache.ink_pad_top,
ink_pad_bottom: self.text_cache.ink_pad_bottom,
},
);
} else {
cx.services.text().release(blob);
}
}
let prepare_started = cx.tree.debug_enabled().then(Instant::now);
let (blob, metrics) = cx.services.text().prepare(&input, constraints);
if let Some(prepare_started) = prepare_started {
let elapsed = prepare_started.elapsed();
cx.tree.debug_record_paint_text_prepare(elapsed);
cx.tree.debug_record_paint_text_prepare_hotspot(
cx.node,
Some(self.element),
"SelectableText",
props.rich.text.len().min(u32::MAX as usize) as u32,
constraints,
reasons_mask,
elapsed,
);
}
self.text_cache.blob = Some(blob);
self.text_cache.metrics = Some(metrics);
self.text_cache.prepared_scale_factor_bits = Some(scale_bits);
self.text_cache.last_text = None;
self.text_cache.last_rich = Some(props.rich.clone());
self.text_cache.last_style = Some(style.clone());
self.text_cache.last_wrap = Some(props.wrap);
self.text_cache.last_overflow = Some(props.overflow);
self.text_cache.last_align = Some(props.align);
self.text_cache.last_width = Some(max_width);
self.text_cache.last_font_stack_key = Some(font_stack_key);
self.text_cache.last_ink_overflow = Some(props.ink_overflow);
let (pad_top, pad_bottom) =
if props.ink_overflow == crate::element::TextInkOverflow::AutoPad {
crate::text::coords::compute_text_ink_overflow_padding(
cx.services.text(),
blob,
)
} else {
(Px(0.0), Px(0.0))
};
self.text_cache.ink_pad_top = pad_top;
self.text_cache.ink_pad_bottom = pad_bottom;
}
let Some(blob) = self.text_cache.blob else {
return;
};
let Some(metrics) = self.text_cache.metrics else {
return;
};
crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::SelectableTextState::default,
|state| {
crate::text_edit::utf8::clamp_selection_to_grapheme_boundaries(
&props.rich.text,
&mut state.selection_anchor,
&mut state.caret,
);
},
);
let (pad_top, pad_bottom) =
crate::text::coords::clamp_text_ink_overflow_padding_to_bounds(
metrics.size.height,
cx.bounds.size.height,
self.text_cache.ink_pad_top,
self.text_cache.ink_pad_bottom,
);
let bounds = fret_core::Rect::new(
fret_core::Point::new(cx.bounds.origin.x, Px(cx.bounds.origin.y.0 + pad_top.0)),
fret_core::Size::new(
cx.bounds.size.width,
Px((cx.bounds.size.height.0 - pad_top.0 - pad_bottom.0).max(0.0)),
),
);
let (mapping, vertical_offset, baseline) =
crate::text::coords::compute_text_box_mapping_for_vertical_placement(
cx.services.text(),
blob,
bounds,
metrics,
style.vertical_placement,
);
let focused = cx.focus == Some(cx.node);
let (dragging, last_pointer_pos) = crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::SelectableTextState::default,
|state| (state.dragging, state.last_pointer_pos),
);
if focused
&& dragging
&& let Some(pointer_pos) = last_pointer_pos
{
let local = mapping.window_to_text_local(pointer_pos);
let hit = cx.services.hit_test_point(blob, local);
crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::SelectableTextState::default,
|state| {
state.caret = crate::text_edit::utf8::clamp_to_grapheme_boundary(
&props.rich.text,
hit.index,
);
state.affinity = hit.affinity;
},
);
}
let clip = fret_core::Rect::new(
fret_core::Point::new(fret_core::Px(0.0), fret_core::Px(0.0)),
bounds.size,
);
let mut interactive_span_bounds: Vec<
crate::element::SelectableTextInteractiveSpanBounds,
> = Vec::new();
if !props.interactive_spans.is_empty() {
let mut rects: Vec<fret_core::Rect> = Vec::new();
for span in props.interactive_spans.iter() {
let start = span.range.start.min(props.rich.text.len());
let end = span.range.end.min(props.rich.text.len());
if start >= end {
continue;
}
if !props.rich.text.is_char_boundary(start)
|| !props.rich.text.is_char_boundary(end)
{
continue;
}
rects.clear();
cx.services
.selection_rects_clipped(blob, (start, end), clip, &mut rects);
if rects.is_empty() {
continue;
}
let mut x0 = f32::INFINITY;
let mut y0 = f32::INFINITY;
let mut x1 = f32::NEG_INFINITY;
let mut y1 = f32::NEG_INFINITY;
for r in rects.iter() {
x0 = x0.min(r.origin.x.0);
y0 = y0.min(r.origin.y.0 + vertical_offset.0 + pad_top.0);
x1 = x1.max(r.origin.x.0 + r.size.width.0);
y1 = y1.max(
r.origin.y.0 + r.size.height.0 + vertical_offset.0 + pad_top.0,
);
}
if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite()
{
continue;
}
if x1 <= x0 || y1 <= y0 {
continue;
}
interactive_span_bounds.push(
crate::element::SelectableTextInteractiveSpanBounds {
range: start..end,
tag: span.tag.clone(),
bounds_local: fret_core::Rect::new(
fret_core::Point::new(fret_core::Px(x0), fret_core::Px(y0)),
fret_core::Size::new(
fret_core::Px(x1 - x0),
fret_core::Px(y1 - y0),
),
),
},
);
}
}
crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::SelectableTextState::default,
|state| {
state.interactive_span_bounds = interactive_span_bounds;
},
);
push_attributed_span_background_quads(
cx.scene,
cx.services,
blob,
props.rich.spans.as_ref(),
clip,
bounds.origin,
vertical_offset,
);
let (anchor, caret) = crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::SelectableTextState::default,
|state| (state.selection_anchor, state.caret),
);
let start = anchor.min(caret);
let end = anchor.max(caret);
if start < end {
let window_focused = cx
.app
.global::<fret_core::WindowMetricsService>()
.and_then(|svc| svc.focused(window))
.unwrap_or(true);
let mut rects: Vec<fret_core::Rect> = Vec::new();
cx.services
.selection_rects_clipped(blob, (start, end), clip, &mut rects);
let sel_color = if focused {
cx.theme().color_token("selection.background")
} else if !window_focused {
cx.theme()
.color_token("selection.window_inactive.background")
} else {
cx.theme().color_token("selection.inactive.background")
};
for r in rects {
let rect = mapping.text_local_to_window_rect(r);
cx.scene.push(SceneOp::Quad {
order: DrawOrder(0),
rect,
background: Paint::Solid(sel_color).into(),
border: fret_core::Edges::all(fret_core::Px(0.0)),
border_paint: Paint::Solid(Color::TRANSPARENT).into(),
corner_radii: fret_core::Corners::all(fret_core::Px(0.0)),
});
}
}
let origin = mapping.baseline_origin(baseline);
cx.scene.push(SceneOp::Text {
order: DrawOrder(0),
origin,
text: blob,
paint: fret_core::scene::Paint::Solid(color).into(),
outline: None,
shadow: None,
});
if dragging
&& let Some(pointer_pos) = last_pointer_pos
&& let Some(window) = cx.window
{
const EDGE_MARGIN: Px = Px(24.0);
const MAX_STEP: Px = Px(16.0);
let mut node = cx.node;
while let Some(parent) = cx.tree.node_parent(node) {
node = parent;
let Some(record) = crate::declarative::frame::element_record_for_node(
cx.app, window, node,
) else {
continue;
};
let (handle, handle_key, scroll_x, scroll_y) = match record.instance {
ElementInstance::Scroll(props) => {
let handle = if let Some(handle) = props.scroll_handle.as_ref() {
handle.clone()
} else {
crate::elements::with_element_state(
&mut *cx.app,
window,
record.element,
crate::element::ScrollState::default,
|state| state.scroll_handle.clone(),
)
};
let key = props.scroll_handle.as_ref().map(|h| h.binding_key());
(handle, key, props.axis.scroll_x(), props.axis.scroll_y())
}
ElementInstance::VirtualList(props) => {
if props.axis == fret_core::Axis::Vertical {
(
props.scroll_handle.base_handle().clone(),
Some(props.scroll_handle.base_handle().binding_key()),
false,
true,
)
} else {
continue;
}
}
_ => continue,
};
if !scroll_x && !scroll_y {
continue;
}
let Some(scroll_bounds) = cx.tree.node_bounds(node) else {
break;
};
let left = scroll_bounds.origin.x;
let right =
fret_core::Px(scroll_bounds.origin.x.0 + scroll_bounds.size.width.0);
let top = scroll_bounds.origin.y;
let bottom =
fret_core::Px(scroll_bounds.origin.y.0 + scroll_bounds.size.height.0);
let mut step_x = Px(0.0);
if scroll_x {
if pointer_pos.x.0 < left.0 + EDGE_MARGIN.0 {
let t = ((left.0 + EDGE_MARGIN.0 - pointer_pos.x.0)
/ EDGE_MARGIN.0)
.clamp(0.0, 1.0);
step_x = Px(-MAX_STEP.0 * t);
} else if pointer_pos.x.0 > right.0 - EDGE_MARGIN.0 {
let t = ((pointer_pos.x.0 - (right.0 - EDGE_MARGIN.0))
/ EDGE_MARGIN.0)
.clamp(0.0, 1.0);
step_x = Px(MAX_STEP.0 * t);
}
}
let mut step_y = Px(0.0);
if scroll_y {
if pointer_pos.y.0 < top.0 + EDGE_MARGIN.0 {
let t = ((top.0 + EDGE_MARGIN.0 - pointer_pos.y.0) / EDGE_MARGIN.0)
.clamp(0.0, 1.0);
step_y = Px(-MAX_STEP.0 * t);
} else if pointer_pos.y.0 > bottom.0 - EDGE_MARGIN.0 {
let t = ((pointer_pos.y.0 - (bottom.0 - EDGE_MARGIN.0))
/ EDGE_MARGIN.0)
.clamp(0.0, 1.0);
step_y = Px(MAX_STEP.0 * t);
}
}
if step_x.0.abs() < 0.01 && step_y.0.abs() < 0.01 {
break;
}
let prev = handle.offset();
handle.set_offset(fret_core::Point::new(
Px(prev.x.0 + step_x.0),
Px(prev.y.0 + step_y.0),
));
let next = handle.offset();
let did_scroll = (next.y.0 - prev.y.0).abs() > 0.01
|| (next.x.0 - prev.x.0).abs() > 0.01;
if did_scroll {
if let Some(handle_key) = handle_key {
let bound = cx
.tree
.live_bound_scroll_handle_nodes(cx.app, window, handle_key);
for node in bound {
cx.tree.invalidate(node, Invalidation::Layout);
cx.tree.invalidate(node, Invalidation::Paint);
}
}
cx.tree.invalidate(node, Invalidation::HitTest);
cx.app.request_redraw(window);
cx.app.push_effect(Effect::RequestAnimationFrame(window));
}
break;
}
}
}
ElementInstance::TextInput(props) => {
let model = props.model.clone();
let model_id = model.id();
if self.text_input.is_none() {
self.text_input = Some(BoundTextInput::new(model.clone()));
}
let Some(input) = self.text_input.as_mut() else {
debug_assert!(false, "text input must be initialized");
return;
};
if input.model_id() != model_id {
input.set_model(model);
}
input.set_focus_ring_always_paint(props.focus_ring_always_paint);
input.set_chrome_style(props.chrome);
input.set_text_style(props.text_style);
input.set_placeholder(props.placeholder);
input.set_obscure_text(props.obscure_text);
input.set_submit_command(props.submit_command);
input.set_cancel_command(props.cancel_command);
input.paint(cx);
}
ElementInstance::TextArea(props) => {
let model = props.model.clone();
let model_id = model.id();
if self.text_area.is_none() {
self.text_area = Some(crate::text_area::BoundTextArea::new(model.clone()));
}
let Some(area) = self.text_area.as_mut() else {
debug_assert!(false, "text area must be initialized");
return;
};
if area.model_id() != model_id {
area.set_model(model);
}
area.set_focus_ring_always_paint(props.focus_ring_always_paint);
area.set_style(props.chrome);
area.set_text_style(props.text_style);
area.set_placeholder(props.placeholder);
area.set_min_height(props.min_height);
area.paint(cx);
}
ElementInstance::ResizablePanelGroup(props) => {
let model = props.model.clone();
let model_id = model.id();
if self.resizable_panel_group.is_none() {
self.resizable_panel_group =
Some(crate::resizable_panel_group::BoundResizablePanelGroup::new(
props.axis,
model.clone(),
));
}
let Some(group) = self.resizable_panel_group.as_mut() else {
debug_assert!(false, "resizable panel group must be initialized");
return;
};
if group.model_id() != model_id {
group.set_model(model);
}
group.set_axis(props.axis);
group.set_enabled(props.enabled);
group.set_min_px(props.min_px.clone());
group.set_style(props.chrome.clone());
group.paint(cx);
}
ElementInstance::VirtualList(props) => {
if cx.tree.view_cache_enabled()
&& !cx.tree.inspection_active()
&& let Some(window) = cx.window
{
let requested_refresh = crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::VirtualListState::default,
|state| {
let axis = props.axis;
state.metrics.ensure_with_mode(
props.measure_mode,
props.len,
props.estimate_row_height,
props.gap,
props.scroll_margin,
);
let viewport = match axis {
fret_core::Axis::Vertical => Px(state.viewport_h.0.max(0.0)),
fret_core::Axis::Horizontal => Px(state.viewport_w.0.max(0.0)),
};
if viewport.0 <= 0.0 || props.len == 0 {
return false;
}
let handle_offset = match axis {
fret_core::Axis::Vertical => props.scroll_handle.offset().y,
fret_core::Axis::Horizontal => props.scroll_handle.offset().x,
};
let offset = state.metrics.clamp_offset(handle_offset, viewport);
let Some(range) =
state
.metrics
.visible_range(offset, viewport, props.overscan)
else {
return false;
};
crate::virtual_list::virtual_list_needs_visible_range_refresh(
&props.visible_items,
range,
)
},
);
cx.tree
.debug_record_virtual_list_visible_range_check(requested_refresh);
}
cx.scene.push(SceneOp::PushClipRect { rect: cx.bounds });
let children_transform = cx.children_render_transform;
if let Some(transform) = children_transform {
cx.scene.push(SceneOp::PushTransform { transform });
}
let accumulated = children_transform
.map(|t| cx.accumulated_transform.compose(t))
.unwrap_or(cx.accumulated_transform);
for &child in cx.children {
let Some(child_bounds) = cx.child_bounds(child) else {
continue;
};
let clip_rect = child_bounds;
cx.scene.push(SceneOp::PushClipRect { rect: clip_rect });
cx.tree.paint_node(
cx.app,
cx.services,
child,
child_bounds,
cx.scene,
cx.scale_factor,
cx.paint_style,
accumulated,
);
cx.scene.push(SceneOp::PopClip);
}
if children_transform.is_some() {
cx.scene.push(SceneOp::PopTransform);
}
cx.scene.push(SceneOp::PopClip);
}
ElementInstance::Image(props) => {
let opacity = props.opacity.clamp(0.0, 1.0);
if let Some(uv) = props.uv {
cx.scene.push(SceneOp::ImageRegion {
order: DrawOrder(0),
rect: cx.bounds,
image: props.image,
uv,
sampling: props.sampling,
opacity,
});
} else {
cx.scene.push(SceneOp::Image {
order: DrawOrder(0),
rect: cx.bounds,
image: props.image,
fit: props.fit,
sampling: props.sampling,
opacity,
});
}
}
ElementInstance::Canvas(props) => {
let on_paint = crate::elements::with_element_state(
cx.app,
window,
self.element,
crate::canvas::CanvasPaintHooks::default,
|hooks| hooks.on_paint.clone(),
);
self.canvas_cache
.begin_paint(cx.app.frame_id().0, props.cache_policy);
if let Some(on_paint) = on_paint {
{
let mut host = crate::canvas::UiCanvasHostAdapter::new(cx);
let mut painter =
crate::canvas::CanvasPainter::new(&mut host, &mut self.canvas_cache);
(on_paint)(&mut painter);
}
}
self.canvas_cache.end_paint(cx.services);
}
ElementInstance::ViewportSurface(props) => {
let opacity = props.opacity.clamp(0.0, 1.0);
if opacity <= 0.0 {
return;
}
let mapping = fret_core::ViewportMapping {
content_rect: cx.bounds,
target_px_size: props.target_px_size,
fit: props.fit,
};
cx.scene.push(SceneOp::ViewportSurface {
order: DrawOrder(0),
rect: mapping.map().draw_rect,
target: props.target,
opacity,
});
}
ElementInstance::SvgIcon(props) => {
let opacity = props.opacity.clamp(0.0, 1.0);
let color = if props.inherit_color {
cx.paint_style.foreground.unwrap_or(props.color)
} else {
props.color
};
if opacity <= 0.0 || color.a <= 0.0 {
return;
}
let svg = self.resolve_svg_for_icon(cx.services, &props.svg);
cx.scene.push(SceneOp::SvgMaskIcon {
order: DrawOrder(0),
rect: cx.bounds,
svg,
fit: props.fit,
color,
opacity,
});
}
ElementInstance::Spinner(props) => {
let inherited_fg = cx.paint_style.foreground;
let theme = cx.theme();
let base = props
.color
.or(inherited_fg)
.or_else(|| theme.color_by_key("muted-foreground"))
.unwrap_or_else(|| theme.color(crate::ThemeColorKey::MutedForeground));
let n = props.dot_count.clamp(1, 32) as usize;
let w = cx.bounds.size.width.0.max(0.0);
let h = cx.bounds.size.height.0.max(0.0);
let min_dim = w.min(h);
if min_dim <= 0.0 {
return;
}
let dot = (min_dim * 0.18).clamp(2.0, (min_dim * 0.25).max(2.0));
let radius = (min_dim * 0.5 - dot * 0.5).max(0.0);
let cx0 = cx.bounds.origin.x.0 + w * 0.5;
let cy0 = cx.bounds.origin.y.0 + h * 0.5;
let speed = props.speed.max(0.0);
if speed > 0.0 {
cx.app.push_effect(Effect::RequestAnimationFrame(window));
}
let phase = cx.app.frame_id().0 as f32 * speed;
let active = (phase.floor() as i32).rem_euclid(n as i32) as usize;
let tail_len = (n.min(5)).saturating_sub(1);
for i in 0..n {
let dist = ((i + n) - active) % n;
let t = if tail_len == 0 {
if dist == 0 { 1.0 } else { 0.25 }
} else if dist == 0 {
1.0
} else if dist <= tail_len {
1.0 - dist as f32 / (tail_len as f32 + 1.0)
} else {
0.25
};
let angle = (i as f32 / n as f32) * std::f32::consts::TAU;
let x = cx0 + radius * angle.cos() - dot * 0.5;
let y = cy0 + radius * angle.sin() - dot * 0.5;
let mut color = base;
color.a = (color.a * t).clamp(0.0, 1.0);
let rect = Rect::new(
fret_core::Point::new(Px(x), Px(y)),
Size::new(Px(dot), Px(dot)),
);
let r = Px(dot * 0.5);
cx.scene.push(SceneOp::Quad {
order: DrawOrder(0),
rect,
background: Paint::Solid(color).into(),
border: Edges::all(Px(0.0)),
border_paint: Paint::Solid(Color::TRANSPARENT).into(),
corner_radii: fret_core::Corners::all(r),
});
}
}
ElementInstance::HoverRegion(props) => {
let clip = matches!(props.layout.overflow, Overflow::Clip);
if clip {
cx.scene.push(SceneOp::PushClipRect { rect: cx.bounds });
}
for &child in cx.children {
let bounds = cx.child_bounds(child).unwrap_or(cx.bounds);
cx.paint(child, bounds);
}
if clip {
cx.scene.push(SceneOp::PopClip);
}
}
ElementInstance::PointerRegion(props) => {
let clip = matches!(props.layout.overflow, Overflow::Clip);
if clip {
cx.scene.push(SceneOp::PushClipRect { rect: cx.bounds });
}
for &child in cx.children {
let bounds = cx.child_bounds(child).unwrap_or(cx.bounds);
cx.paint(child, bounds);
}
if clip {
cx.scene.push(SceneOp::PopClip);
}
}
ElementInstance::TextInputRegion(props) => {
let clip = matches!(props.layout.overflow, Overflow::Clip);
if clip {
cx.scene.push(SceneOp::PushClipRect { rect: cx.bounds });
}
for &child in cx.children {
let bounds = cx.child_bounds(child).unwrap_or(cx.bounds);
cx.paint(child, bounds);
}
if clip {
cx.scene.push(SceneOp::PopClip);
}
}
ElementInstance::InternalDragRegion(props) => {
let clip = matches!(props.layout.overflow, Overflow::Clip);
if clip {
cx.scene.push(SceneOp::PushClipRect { rect: cx.bounds });
}
for &child in cx.children {
let bounds = cx.child_bounds(child).unwrap_or(cx.bounds);
cx.paint(child, bounds);
}
if clip {
cx.scene.push(SceneOp::PopClip);
}
}
ElementInstance::ExternalDragRegion(props) => {
let clip = matches!(props.layout.overflow, Overflow::Clip);
if clip {
cx.scene.push(SceneOp::PushClipRect { rect: cx.bounds });
}
for &child in cx.children {
let bounds = cx.child_bounds(child).unwrap_or(cx.bounds);
cx.paint(child, bounds);
}
if clip {
cx.scene.push(SceneOp::PopClip);
}
}
ElementInstance::WheelRegion(props) => {
let clip = matches!(props.layout.overflow, Overflow::Clip);
if clip {
cx.scene.push(SceneOp::PushClipRect { rect: cx.bounds });
}
for &child in cx.children {
let bounds = cx.child_bounds(child).unwrap_or(cx.bounds);
cx.paint(child, bounds);
}
if clip {
cx.scene.push(SceneOp::PopClip);
}
}
ElementInstance::Scroll(props) => {
let clip = matches!(props.layout.overflow, Overflow::Clip);
if clip {
cx.scene.push(SceneOp::PushClipRect { rect: cx.bounds });
}
for &child in cx.children {
let bounds = cx.child_bounds(child).unwrap_or(cx.bounds);
cx.paint(child, bounds);
}
if clip {
cx.scene.push(SceneOp::PopClip);
}
}
ElementInstance::Scrollbar(props) => {
let handle = props.scroll_handle.clone();
let (hovered, dragging) = crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::ScrollbarState::default,
|state| (state.hovered, state.dragging_thumb),
);
let is_horizontal = matches!(props.axis, crate::element::ScrollbarAxis::Horizontal);
let offset = handle.offset();
let viewport = handle.viewport_size();
let content = handle.content_size();
let has_overflow = if is_horizontal {
(content.width.0 - viewport.width.0).max(0.0) > 0.0
} else {
(content.height.0 - viewport.height.0).max(0.0) > 0.0
};
if !has_overflow {
if cx.tree.debug_enabled() {
cx.tree.debug_record_scrollbar_telemetry(
crate::tree::UiDebugScrollbarTelemetry {
node: cx.node,
element: Some(self.element),
axis: if is_horizontal {
crate::tree::UiDebugScrollAxis::X
} else {
crate::tree::UiDebugScrollAxis::Y
},
scroll_target: props.scroll_target,
offset,
viewport,
content,
track: cx.bounds,
thumb: None,
hovered,
dragging,
},
);
}
return;
}
let thumb = if is_horizontal {
scrollbar_thumb_rect_horizontal(
cx.bounds,
viewport.width,
content.width,
offset.x,
props.style.track_padding,
)
} else {
scrollbar_thumb_rect(
cx.bounds,
viewport.height,
content.height,
offset.y,
props.style.track_padding,
)
};
if cx.tree.debug_enabled() {
cx.tree.debug_record_scrollbar_telemetry(
crate::tree::UiDebugScrollbarTelemetry {
node: cx.node,
element: Some(self.element),
axis: if is_horizontal {
crate::tree::UiDebugScrollAxis::X
} else {
crate::tree::UiDebugScrollAxis::Y
},
scroll_target: props.scroll_target,
offset,
viewport,
content,
track: cx.bounds,
thumb,
hovered,
dragging,
},
);
}
let Some(thumb) = thumb else {
return;
};
let mut bg = if hovered || dragging {
props.style.thumb_hover
} else {
props.style.thumb
};
if !(hovered || dragging) {
bg.a *= props.style.thumb_idle_alpha.clamp(0.0, 1.0);
}
let rect = if is_horizontal {
let inset = 1.0f32.min(thumb.size.height.0 * 0.25);
Rect::new(
fret_core::Point::new(thumb.origin.x, Px(thumb.origin.y.0 + inset)),
Size::new(
thumb.size.width,
Px((thumb.size.height.0 - inset * 2.0).max(0.0)),
),
)
} else {
let inset = 1.0f32.min(thumb.size.width.0 * 0.25);
Rect::new(
fret_core::Point::new(Px(thumb.origin.x.0 + inset), thumb.origin.y),
Size::new(
Px((thumb.size.width.0 - inset * 2.0).max(0.0)),
thumb.size.height,
),
)
};
cx.scene.push(SceneOp::Quad {
order: DrawOrder(20_000),
rect,
background: Paint::Solid(bg).into(),
border: Edges::all(Px(0.0)),
border_paint: Paint::Solid(Color::TRANSPARENT).into(),
corner_radii: fret_core::Corners::all(Px(999.0)),
});
}
});
}
}
#[cfg(test)]
mod tests {
use crate::text::coords::compute_text_vertical_offset_and_baseline;
use fret_core::{
FontId, FontWeight, Px, Size, TextBlobId, TextLineMetrics, TextMetrics, TextStyle,
TextVerticalPlacement,
};
#[test]
fn text_vertical_offset_centers_metrics_in_bounds() {
let style = TextStyle {
font: FontId::default(),
size: Px(12.0),
weight: FontWeight::NORMAL,
slant: Default::default(),
line_height: Some(Px(16.0)),
line_height_em: None,
line_height_policy: Default::default(),
letter_spacing_em: None,
features: Vec::new(),
axes: Vec::new(),
vertical_placement: TextVerticalPlacement::CenterMetricsBox,
leading_distribution: Default::default(),
strut_style: None,
};
let _ = style;
let offset = crate::text::coords::compute_text_vertical_offset(Px(16.0), Px(12.0));
assert_eq!(offset, Px(2.0));
}
#[test]
fn text_vertical_offset_clamps_negative_half_leading_to_zero() {
let style = TextStyle {
font: FontId::default(),
size: Px(14.0),
weight: FontWeight::NORMAL,
slant: Default::default(),
line_height: Some(Px(12.0)),
line_height_em: None,
line_height_policy: Default::default(),
letter_spacing_em: None,
features: Vec::new(),
axes: Vec::new(),
vertical_placement: TextVerticalPlacement::CenterMetricsBox,
leading_distribution: Default::default(),
strut_style: None,
};
let _ = style;
let offset = crate::text::coords::compute_text_vertical_offset(Px(12.0), Px(14.0));
assert_eq!(offset, Px(0.0));
}
struct FakeTextService {
line: Option<TextLineMetrics>,
}
impl fret_core::TextService for FakeTextService {
fn prepare(
&mut self,
_input: &fret_core::TextInput,
_constraints: fret_core::TextConstraints,
) -> (TextBlobId, TextMetrics) {
unimplemented!("not needed for this test")
}
fn release(&mut self, _blob: TextBlobId) {}
fn first_line_metrics(&mut self, _blob: TextBlobId) -> Option<TextLineMetrics> {
self.line
}
}
#[test]
fn bounds_as_line_box_baseline_uses_half_leading_in_bounds() {
let mut blobs: slotmap::SlotMap<TextBlobId, ()> = slotmap::SlotMap::with_key();
let blob = blobs.insert(());
let mut services = FakeTextService {
line: Some(TextLineMetrics {
ascent: Px(10.0),
descent: Px(4.0),
line_height: Px(16.0),
}),
};
let metrics = TextMetrics {
size: Size::new(Px(32.0), Px(16.0)),
baseline: Px(12.0),
};
let (offset, baseline) = compute_text_vertical_offset_and_baseline(
&mut services,
blob,
Px(20.0),
metrics,
TextVerticalPlacement::BoundsAsLineBox,
);
assert_eq!(offset, Px(0.0));
assert_eq!(baseline, Px(13.0));
}
}