use super::*;
const SCROLLBAR_W: f32 = 4.0;
const SCROLLBAR_ACTIVE_W: f32 = 6.0;
const SCROLLBAR_HIT_W: f32 = 10.0;
const SCROLLBAR_INSET: f32 = 2.0;
const SCROLLBAR_MIN: f32 = 24.0;
const SCROLLBAR_HOLD: f64 = 1.0;
const SCROLLBAR_FADE: f64 = 0.25;
const FOCUS_RING_GAP: f32 = 2.0;
const FOCUS_RING_W: f32 = 2.0;
macro_rules! painter {
($core:expr) => {
Painter {
display: &mut $core.display,
text: &mut $core.text,
edit: &mut $core.edit,
cells: &mut $core.cells,
atlas: &mut $core.atlas,
session: &$core.session,
path_tex: &mut $core.path_textures,
frame_no: $core.frame_no,
}
};
}
impl Core {
fn thumb_color(&self, bar: &crate::spec::Scrollbar, active: bool) -> Color {
let t = self.theme();
if active {
bar.active_color.unwrap_or(t.scrollbar_active)
} else {
bar.color.unwrap_or(t.scrollbar)
}
}
#[inline(never)]
fn push_hit(
&mut self,
i: usize,
rect: Rect,
clip: Rect,
drop: Option<crate::input::DropOwner>,
hits: &mut Vec<HitRegion>,
) {
let spec = &self.tree.specs[i];
let parent = self.tree.parent[i];
let parent_rect = if parent == NIL {
Rect::new(0.0, 0.0, self.viewport.w, self.viewport.h)
} else {
let p = parent as usize;
Rect::from_pos_size(self.tree.pos[p], self.tree.size[p])
};
let live = !spec.disabled;
let context_menu = if self.tree.any_context_menu {
self.enclosing_menu(i).map(|j| crate::input::MenuOwner {
key: self.tree.keys[j],
origin: self.tree.origins[j],
tag: self.tree.specs[j]
.events()
.on_context_menu
.clone()
.expect("enclosing_menu returns a node that offers one"),
})
} else {
None
};
let slider = match spec.events().on_change.as_ref() {
Some(tag) if live && spec.access().role == Some(crate::access::Role::Slider) => {
crate::slider::SliderRange::of(spec.access()).map(|range| {
Box::new(crate::slider::SliderTrack::new(
rect,
spec.layout.padding,
spec.layout.dir == crate::spec::Dir::Column,
range,
tag.clone(),
))
})
}
_ => None,
};
let shape = match self.tree.content[i] {
NodeContent::Line(id) => {
let (run, points) = self.lines.run(id);
self.hit_shapes.segments(points, run.width)
}
NodeContent::Polygon(id) => {
let draw = self.fragments.get(id);
let mut pts = [Vec2::ZERO; crate::fragment::POLYGON_MAX_POINTS];
for (k, p) in pts.iter_mut().enumerate() {
*p = Vec2::new(draw.params[k * 2] * rect.w, draw.params[k * 2 + 1] * rect.h);
}
self.hit_shapes.polygon(&pts)
}
NodeContent::Path(id) => {
let (run, ops) = self.paths.run(id);
let mut outline = Vec::new();
let s = &spec.style;
let fill = run.stroke_w <= 0.0
|| s.bg.is_visible()
|| spec.interact.as_deref().is_some_and(|i| {
[i.hover_bg, i.pressed_bg, i.focus_bg]
.iter()
.any(|c| c.is_some_and(|c| c.is_visible()))
});
if fill && run.stroke_w <= 0.0 {
crate::path::flatten(ops, &mut outline);
} else {
crate::path::flatten_stroke(ops, &mut outline);
}
if let Some(angle) = run.angle.filter(|a| *a != 0.0) {
let c = Vec2::new(rect.w * 0.5, rect.h * 0.5);
for p in outline.iter_mut().filter(|p| !p.x.is_nan()) {
*p = crate::path::turned(*p, c, angle);
}
}
if fill {
self.hit_shapes.path(&outline, run.rule, run.stroke_w)
} else {
self.hit_shapes.segments(&outline, run.stroke_w)
}
}
_ if spec.style.radius != crate::display::SQUARE => {
crate::input::HitShape::Rounded(spec.style.radius)
}
_ => crate::input::HitShape::Rect,
};
hits.push(HitRegion {
key: self.tree.keys[i],
origin: self.tree.origins[i],
rect,
clip,
shape,
payload: spec.events().on_click.clone().filter(|_| live),
drag: spec.events().on_drag.clone().filter(|_| live),
parent_rect,
key_sink: spec.events().on_key.clone().filter(|_| live),
key_up: spec.events().key_up,
context_menu,
drop,
focusable: crate::access::focusable(&self.tree, i),
edit_origin: None,
select_scope: self.scope_of(i).filter(|_| live),
window: spec.window,
hover: spec.events().on_hover.clone(),
group: spec.interact().hover_group,
click_sound: spec.interact().click_sound.filter(|_| live),
hover_sound: spec.interact().hover_sound,
cursor: spec.cursor,
slider,
});
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn push_edit_hit(
&mut self,
i: usize,
key: Key,
rect: Rect,
clip: Rect,
scale: f32,
drop: Option<crate::input::DropOwner>,
hits: &mut Vec<HitRegion>,
) {
let spec = &self.tree.specs[i];
let pad = spec.layout.padding;
let content_origin = Vec2::new(rect.x + pad.l, rect.y + pad.t);
let inner_w = (rect.w - pad.x()).max(0.0);
let offset = {
let sess = &mut *self.session.state();
self.edit.line_offset(key, inner_w * scale, &mut sess.fonts)
};
hits.push(HitRegion {
key,
origin: self.tree.origins[i],
rect,
clip,
shape: if spec.style.radius != crate::display::SQUARE {
crate::input::HitShape::Rounded(spec.style.radius)
} else {
crate::input::HitShape::Rect
},
payload: None,
drag: None,
parent_rect: rect,
edit_origin: Some(Vec2::new(
content_origin.x - offset / scale,
content_origin.y,
)),
select_scope: None,
key_sink: None,
key_up: false,
context_menu: None,
drop,
focusable: !spec.disabled,
window: None,
hover: None,
group: None,
click_sound: None,
hover_sound: None,
cursor: spec.cursor,
slider: None,
});
}
#[inline(never)]
fn enclosing_scroll_region(&self, i: usize, regions: &[ScrollRegion]) -> u32 {
let mut a = self.tree.parent[i];
while a != NIL {
if let Some(r) = regions.iter().rposition(|r| r.node == a) {
return r as u32;
}
a = self.tree.parent[a as usize];
}
NIL
}
fn emit_node(
&mut self,
i: usize,
rect: Rect,
paint: Paint,
hits: &mut Vec<HitRegion>,
scroll_regions: &mut Vec<ScrollRegion>,
) {
let Paint { clip, scale, .. } = paint;
let interactive = self.interactive(i);
let spec = &self.tree.specs[i];
let style = spec.style;
let drop = if self.tree.any_drop && interactive {
self.enclosing_drop(i).map(|j| crate::input::DropOwner {
key: self.tree.keys[j],
origin: self.tree.origins[j],
tag: self.tree.specs[j]
.events()
.on_drop
.clone()
.expect("enclosing_drop returns a node that declares one"),
})
} else {
None
};
if spec.hover_tracked() && interactive {
self.push_hit(i, rect, clip.rect, drop.clone(), hits);
}
let spec = &self.tree.specs[i];
let handler = self.tree.any_scroll_handler && spec.events().on_scroll.is_some();
if spec.layout.scroll_x || spec.layout.scroll_y || handler {
let (takes_x, takes_y) = if handler {
let axes = spec.events().scroll_axes;
(axes.takes(true), axes.takes(false))
} else {
(spec.layout.scroll_x, spec.layout.scroll_y)
};
scroll_regions.push(ScrollRegion {
key: self.tree.keys[i],
node: i as u32,
rect,
clip: clip.rect,
inert: !interactive,
handler,
takes_x,
takes_y,
scrolls_x: spec.layout.scroll_x,
scrolls_y: spec.layout.scroll_y,
contain: spec.interact().overscroll == crate::spec::Overscroll::Contain,
mods: if handler {
spec.events().scroll_mods.bits()
} else {
0
},
parent: self.enclosing_scroll_region(i, scroll_regions),
});
}
if let NodeContent::Edit(key) = self.tree.content[i]
&& interactive
{
self.push_edit_hit(i, key, rect, clip.rect, scale, drop, hits);
}
let leaf = match self.tree.content[i] {
NodeContent::Text(tid) => {
let ancestry = self.text_ancestors(i);
self.text.place(
self.tree.keys[i],
&ancestry,
tid,
self.tree.pos[i],
self.scope_of(i),
true,
);
Leaf::Text {
tid,
sel: self.sel_range(i, tid),
}
}
NodeContent::Cells(cid) => Leaf::Cells {
cid,
at: self.cells_origin(i),
sel: self
.cell_selection
.as_ref()
.filter(|s| s.node == self.tree.keys[i] && !s.is_empty()),
tint: self.theme.selection,
},
NodeContent::Edit(key) => Leaf::Edit {
key,
focused: self.edit.focused() == Some(key),
pad: self.tree.specs[i].layout.padding,
},
NodeContent::Image(id, opts) => Leaf::Image(id, opts),
NodeContent::Fragment(id) => Leaf::Fragment(self.fragments.get(id)),
NodeContent::Polygon(id) => Leaf::Polygon(self.fragments.get(id)),
NodeContent::Line(id) => {
let (run, points) = self.lines.run(id);
Leaf::Line {
points,
width: run.width,
dash: run.dash,
}
}
NodeContent::Path(id) => {
let (run, ops) = self.paths.run(id);
Leaf::Path {
ops,
rule: run.rule,
stroke_w: run.stroke_w,
dash: run.dash,
hash: run.hash,
angle: run.angle,
animating: run.animating,
}
}
NodeContent::Container => Leaf::Container,
};
if self.tree.any_backdrop_blur {
let r = self.tree.specs[i].interact().backdrop_blur;
if r > 0.0 {
backdrop_quad(&mut self.display.quads, rect, &style, &paint, r);
}
}
let first_quad = self.display.quads.len();
painter!(self).paint_box(rect, &style, &paint, leaf);
if self.tree.any_gradient
&& let Some(g) = &self.tree.specs[i].interact().gradient
&& !matches!(
self.tree.content[i],
NodeContent::Line(_) | NodeContent::Polygon(_) | NodeContent::Path(_)
)
{
painter!(self).paint_gradient(g, rect, &style, &paint, first_quad);
}
if self.tree.any_table
&& let Some(c) = self.tree.specs[i].interact().rules
&& self.tree.specs[i].layout.is_table()
{
self.emit_rules(i, rect, c, &paint);
}
}
#[cold]
#[inline(never)]
fn emit_rules(&mut self, i: usize, rect: Rect, color: Color, paint: &Paint) {
let tree = &self.tree;
let at = |j: u32| Rect::from_pos_size(tree.pos[j as usize], tree.size[j as usize]);
let in_flow = |j: &u32| tree.specs[*j as usize].layout.float.is_none();
let children: Vec<RuledChild> = tree
.children(i as u32)
.filter(in_flow)
.map(|c| RuledChild {
rect: at(c),
cells: crate::layout::is_table_row(tree, c)
.then(|| tree.children(c).filter(in_flow).map(at).collect()),
})
.collect();
let spec = &tree.specs[i];
let lines = rule_lines(rect, spec.layout.padding, spec.interact().rule_w, &children);
let clip_id = if spec.layout.clips() {
let radius = if tree.any_rounded_clip {
spec.style.radius
} else {
crate::display::SQUARE
};
let clip = paint.clip.intersect(rect, radius);
self.display.intern_clip(clip.scaled(paint.scale))
} else {
paint.clip_id
};
push_rules(&mut self.display, &lines, color, paint, clip_id);
}
#[cold]
#[inline(never)]
fn emit_ghost_rules(
&mut self,
g: &Ghost,
i: usize,
rect: Rect,
offset: Vec2,
color: Color,
paint: &Paint,
) {
let at = |node: &crate::depart::GhostNode| {
Rect::new(
node.rect.x + offset.x,
node.rect.y + offset.y,
node.rect.w,
node.rect.h,
)
};
let mut children: Vec<RuledChild> = Vec::new();
let mut last = NIL;
for (j, node) in g.nodes.iter().enumerate().skip(i + 1) {
if node.parent == NIL || (node.parent as usize) < i {
break;
}
if node.spec.layout.float.is_some() {
continue;
}
if node.parent as usize == i {
let row = crate::layout::row_shaped(
&node.spec.layout,
matches!(node.content, GhostContent::Container),
);
children.push(RuledChild {
rect: at(node),
cells: row.then(Vec::new),
});
last = j as u32;
} else if node.parent == last
&& let Some(cells) = children.last_mut().and_then(|c| c.cells.as_mut())
{
cells.push(at(node));
}
}
let spec = &g.nodes[i].spec;
let lines = rule_lines(rect, spec.layout.padding, spec.interact().rule_w, &children);
let clip_id = if spec.layout.clips() {
let clip = paint.clip.intersect(rect, spec.style.radius);
self.display.intern_clip(clip.scaled(paint.scale))
} else {
paint.clip_id
};
push_rules(&mut self.display, &lines, color, paint, clip_id);
}
pub(crate) fn finish_frame(&mut self) {
self.layout_frame();
self.emit_frame();
self.building = false;
self.submenu_frame_end();
let mut out = std::mem::take(&mut self.pending);
self.report_focus("program", &mut out);
self.pending = out;
self.snapshot_nodes();
self.devtools_after_frame();
self.trace_finish_frame();
self.origin = crate::tree::OriginId::HOST;
}
fn layout_frame(&mut self) {
self.stack.truncate(1);
self.counters.truncate(1);
self.tree.host_area = self.dt_area;
if !self.pending_scroll_labels.is_empty() {
self.resolve_scroll_labels();
}
{
let sess = &mut *self.session.state();
let mut measure = Measure {
text: &mut self.text,
cells: &mut self.cells,
fonts: &mut sess.fonts,
edit: &mut self.edit,
resources: &sess.resources,
};
layout::compute(
&mut self.tree,
&mut measure,
&mut self.scroll,
self.viewport,
self.scale,
);
}
self.scroll_caret_into_view();
self.apply_pending_reveal();
if self.scroll.take_resliced() {
self.owe_frame("resliced");
}
if self.tree.any_slide {
self.ease_positions();
}
if self.tree.any_layout {
self.emit_layout_events();
}
self.diag
.check(&self.tree, &self.text, &self.edit, self.frame_no);
for seed in self.edit.take_unclaimed_seeds() {
self.diag.raise(match seed {
crate::edit::Unclaimed::Key(key) => crate::diag::edit_text_without_editor(key),
crate::edit::Unclaimed::Label(label) => {
crate::diag::edit_text_without_editor_label(&label)
}
});
}
self.sync_windows();
self.modal = if self.tree.any_modal {
self.modal_scope()
} else {
None
};
self.resolve_modal_focus();
self.resolve_regions();
if let Some(forward) = self.pending_focus_step.take() {
self.focus_next(forward);
}
}
fn resolve_selection(&mut self) {
self.sel_ords.clear();
self.sel_ends = None;
let Some(sel) = self.selection else { return };
self.sel_ords.resize(self.tree.len(), u32::MAX);
let (mut anchor, mut focus) = (None, None);
let mut built: Vec<(u32, Option<u64>, usize)> = Vec::new();
let mut ord = 0u32;
for i in 0..self.tree.len() {
if self.scopes.get(i).copied().flatten() != Some(sel.scope)
|| !matches!(self.tree.content[i], NodeContent::Text(_))
{
continue;
}
self.sel_ords[i] = ord;
let key = self.tree.keys[i];
let len = match self.tree.content[i] {
NodeContent::Text(tid) => self.text.content_len(tid),
_ => 0,
};
built.push((ord, self.rows.get(i).copied().flatten(), len));
if key == sel.anchor.node {
anchor = Some((ord, sel.anchor.byte));
}
if key == sel.focus.node {
focus = Some((ord, sel.focus.byte));
}
ord += 1;
}
let place = |end: &crate::select::Endpoint| -> Option<(u32, usize)> {
let row = end.row?;
let (start, last) = (built.first()?, built.last()?);
let mut in_row = built.iter().filter(|(_, r, _)| *r == Some(row));
if let Some(first_run) = in_row.next() {
return Some(if end.byte >= crate::select::ROW_END {
let last_run = in_row.next_back().unwrap_or(first_run);
(last_run.0, last_run.2)
} else {
(first_run.0, end.byte.min(first_run.2))
});
}
let hi = built.iter().rev().find_map(|(_, r, _)| *r)?;
if crate::select::unbuilt_row_is_after(row, Some(hi)) {
return Some((last.0, last.2));
}
Some((start.0, 0))
};
let anchor = anchor.or_else(|| place(&sel.anchor));
let focus = focus.or_else(|| place(&sel.focus));
if let (Some(a), Some(f)) = (anchor, focus) {
self.sel_ends = Some(crate::select::Ends::ordered(a, f));
}
}
fn sel_range(&self, i: usize, tid: crate::tree::TextId) -> Option<((usize, usize), Color)> {
let ends = self.sel_ends?;
let ord = self.sel_ords.get(i).copied()?;
if ord == u32::MAX {
return None;
}
let range = ends.range_in(ord, self.text.content_len(tid))?;
Some((range, self.theme().selection))
}
#[inline]
fn scope_of(&self, i: usize) -> Option<Key> {
self.scopes.get(i).copied().flatten()
}
fn text_ancestors(&mut self, i: usize) -> crate::text::Ancestry {
use crate::access::Role;
let mut ancestors = [Key::ROOT; crate::text::PLACE_ANCESTORS];
let mut depth = 0;
let mut none_at = None;
let mut p = self.tree.parent[i];
while p != NIL && depth < ancestors.len() {
let j = p as usize;
ancestors[depth] = self.tree.keys[j];
if none_at.is_none() && self.tree.specs[j].access().role == Some(Role::None) {
none_at = Some(depth);
}
depth += 1;
p = self.tree.parent[j];
}
if p != NIL && self.tree.any_line {
let mut q = p;
while q != NIL {
let j = q as usize;
if self.tree.specs[j].access().role == Some(Role::Line) {
self.diag.raise(crate::diag::text_beyond_line(
self.tree.keys[i],
self.tree.keys[j],
crate::text::PLACE_ANCESTORS,
));
break;
}
q = self.tree.parent[j];
}
}
crate::text::Ancestry {
keys: ancestors,
depth,
none_at,
}
}
fn emit_frame(&mut self) {
let scale = self.scale;
let mut hits: Vec<HitRegion> = self.interaction.take_hit_buffer();
self.hit_shapes = self.interaction.take_shape_buffer();
let mut scroll_regions: Vec<ScrollRegion> = Vec::new();
self.display.viewport = Size::new(self.viewport.w * scale, self.viewport.h * scale);
self.display.scale = scale;
self.display.time = self.anim.time().unwrap_or(0.0) as f32;
let any_clip = self.tree.any_clip;
let any_rounded_clip = self.tree.any_rounded_clip;
let any_opacity = self.tree.any_opacity;
let any_float = self.tree.any_float;
let any_selectable = self.tree.any_selectable;
let no_clip = self.display.intern_clip(Clip::NONE.scaled(scale));
self.clips.clear();
self.clip_ids.clear();
if any_clip {
self.clips.resize(self.tree.len(), Clip::NONE);
self.clip_ids.resize(self.tree.len(), no_clip);
}
self.opacity.clear();
if any_opacity {
self.opacity.resize(self.tree.len(), 1.0);
}
self.float_root.clear();
if any_float {
self.float_root.resize(self.tree.len(), NIL);
}
self.scopes.clear();
self.rows.clear();
if any_selectable {
self.scopes.resize(self.tree.len(), None);
self.rows.resize(self.tree.len(), None);
for &(node, index) in &self.tree.indexed {
if let Some(slot) = self.rows.get_mut(node as usize) {
*slot = Some(index);
}
}
for i in 0..self.tree.len() {
let parent = self.tree.parent[i];
let outer = if parent == NIL || self.tree.specs[i].layout.float.is_some() {
None
} else {
self.scopes[parent as usize]
};
let grid = matches!(self.tree.content[i], NodeContent::Cells(_));
self.scopes[i] = if self.tree.specs[i].interact().selectable {
Some(self.tree.keys[i])
} else if grid {
None
} else {
outer
};
if self.rows[i].is_none() && parent != NIL {
self.rows[i] = self.rows[parent as usize];
}
}
}
self.resolve_selection();
if !self.prev_tree.is_empty() || !self.depart.is_empty() {
self.collect_departures();
}
let mut replay = Replay::default();
let mut took_ghosts = false;
if !self.depart.is_empty()
&& let Some(now) = self.anim.time()
{
replay = self.depart.begin_replay(now);
took_ghosts = true;
}
let any_ghost = !replay.is_empty();
let mut roots: Vec<u32> = Vec::new();
for i in 0..self.tree.len() {
let parent = self.tree.parent[i];
let floats_here = any_float && self.tree.opens_layer(i);
if any_float {
self.float_root[i] = if floats_here {
roots.push(i as u32);
i as u32
} else if parent != NIL {
self.float_root[parent as usize]
} else {
NIL
};
}
let rect = Rect::from_pos_size(self.tree.pos[i], self.tree.size[i]);
let opacity = if !any_opacity {
1.0
} else {
let inherited = if parent == NIL {
1.0
} else {
self.opacity[parent as usize]
};
let o = inherited * self.tree.specs[i].style.opacity;
self.opacity[i] = o;
o
};
let drawn_in_parent = floats_here
&& parent != NIL
&& self.tree.specs[i]
.layout
.float
.is_some_and(|f| f.clipped_by_parent());
let (clip, clip_id) = if !any_clip {
(Clip::NONE, no_clip)
} else {
let (clip, id) = if parent == NIL || (floats_here && !drawn_in_parent) {
(Clip::NONE, no_clip)
} else {
let p = parent as usize;
if self.tree.specs[p].layout.clips() {
let box_rect = Rect::from_pos_size(self.tree.pos[p], self.tree.size[p]);
let box_radius = if any_rounded_clip {
self.tree.specs[p].style.radius
} else {
crate::display::SQUARE
};
let clip = self.clips[p].intersect(box_rect, box_radius);
(clip, self.display.intern_clip(clip.scaled(scale)))
} else {
(self.clips[p], self.clip_ids[p])
}
};
self.clips[i] = clip;
self.clip_ids[i] = id;
(clip, id)
};
if any_float && self.float_root[i] != NIL {
continue; }
if any_ghost && replay.may_precede(self.tree.keys[i]) {
let key = self.tree.keys[i];
replay.paint(At::UnderInFlow(key), |g, play| {
self.emit_ghost(g, play, scale)
});
}
let visible = rect.intersect(&clip.rect);
if visible.w <= 0.0 || visible.h <= 0.0 {
if any_selectable
&& let Some(scope) = self.scope_of(i)
&& let NodeContent::Text(tid) = self.tree.content[i]
{
let ancestry = self.text_ancestors(i);
self.text.place(
self.tree.keys[i],
&ancestry,
tid,
self.tree.pos[i],
Some(scope),
false,
);
}
continue;
}
let paint = Paint {
clip,
clip_id,
scale,
opacity,
};
self.emit_node(i, rect, paint, &mut hits, &mut scroll_regions);
}
if any_ghost {
replay.paint(At::InFlowEnd, |g, play| self.emit_ghost(g, play, scale));
}
let mut scrollbars: Vec<ScrollbarRegion> = Vec::new();
let mut chrome_from = 0usize;
self.emit_layer_chrome(
NIL,
&scroll_regions[chrome_from..],
&mut scrollbars,
hits.len(),
scale,
);
chrome_from = scroll_regions.len();
if any_float {
let order = self.stack_floats(&roots);
for &r in &order {
let root = r as usize;
let root_key = self.tree.keys[root];
if any_ghost && replay.may_precede(root_key) {
replay.paint(At::UnderLayer(root_key), |g, play| {
self.emit_ghost(g, play, scale)
});
}
let end = self.tree.subtree_end(root);
for i in root..end {
if self.float_root[i] != r {
continue; }
if any_ghost && replay.may_precede(self.tree.keys[i]) {
let key = self.tree.keys[i];
replay.paint(At::UnderInLayer(key), |g, play| {
self.emit_ghost(g, play, scale)
});
}
let rect = Rect::from_pos_size(self.tree.pos[i], self.tree.size[i]);
let clip = if any_clip { self.clips[i] } else { Clip::NONE };
let clip_id = if any_clip { self.clip_ids[i] } else { no_clip };
let opacity = if any_opacity { self.opacity[i] } else { 1.0 };
let visible = rect.intersect(&clip.rect);
if visible.w <= 0.0 || visible.h <= 0.0 {
continue;
}
let paint = Paint {
clip,
clip_id,
scale,
opacity,
};
self.emit_node(i, rect, paint, &mut hits, &mut scroll_regions);
}
if any_ghost {
replay.paint(At::LayerEnd(root_key), |g, play| {
self.emit_ghost(g, play, scale)
});
}
if scroll_regions.len() > chrome_from || self.focus_visible {
self.emit_layer_chrome(
r,
&scroll_regions[chrome_from..],
&mut scrollbars,
hits.len(),
scale,
);
chrome_from = scroll_regions.len();
}
}
self.check_layers_over_modal(&order);
} else if !self.float_stack.is_empty() {
self.float_stack.clear();
}
if any_ghost {
replay.paint(At::Top, |g, play| self.emit_ghost(g, play, scale));
}
if took_ghosts {
self.depart.end_replay(replay);
}
let joins = self.text.take_joins();
if !joins.is_empty() {
let sess = &mut *self.session.state();
let id = sess.resources.add_fragment(crate::fragment::JOIN);
if let Some(source) = sess.resources.fragment(id).cloned() {
crate::join::shape(&mut self.display, &joins, id, &source, scale);
}
}
let shapes = std::mem::take(&mut self.hit_shapes);
self.interaction.set_hits_shaped(hits, shapes);
let tree = &self.tree;
self.interaction
.drop_gone_buttons(|key| tree.index_of(key).is_some());
self.flush_sound_requests();
let window = self.env.window.id;
self.session.state().audio.reconcile(window);
self.interaction.scroll_regions = scroll_regions;
self.interaction.scrollbars = scrollbars;
self.ime_rect = self.focused_caret_rect();
self.note_sink_caret();
self.atlas.end_frame();
if self.atlas.short() {
self.owe_frame("atlas full");
}
if self.text.take_owed() {
self.owe_frame("long line rows");
}
}
fn collect_departures(&mut self) {
let Some(now) = self.anim.time() else {
self.depart.clear();
return;
};
if self.prev_tree.keys == self.tree.keys {
return;
}
let mut watch: FxHashSet<Key> = FxHashSet::default();
let mut mask = 0u64;
for k in self.depart.keys() {
watch.insert(k);
mask |= 1u64 << (k.0 & 63);
}
let mut candidates: Vec<usize> = Vec::new();
for i in 0..self.prev_tree.len() {
if crate::depart::can_depart(&self.prev_tree.specs[i]) {
candidates.push(i);
let p = self.prev_tree.parent[i];
let keys = [
Some(self.prev_tree.keys[i]),
(p != NIL).then(|| self.prev_tree.keys[p as usize]),
];
for k in keys.into_iter().flatten() {
watch.insert(k);
mask |= 1u64 << (k.0 & 63);
}
}
}
if watch.is_empty() {
return;
}
let mut live: FxHashSet<Key> = FxHashSet::default();
for &k in &self.tree.keys {
if mask & (1u64 << (k.0 & 63)) != 0 && watch.contains(&k) {
live.insert(k);
}
}
self.depart.retire_returned(&live);
let mut roots: Vec<usize> = Vec::new();
let mut wanted = 0usize;
let mut swallowed_until = 0usize;
for i in candidates {
if i < swallowed_until || live.contains(&self.prev_tree.keys[i]) {
continue;
}
let p = self.prev_tree.parent[i];
if p != NIL && !live.contains(&self.prev_tree.keys[p as usize]) {
continue;
}
swallowed_until = self.prev_tree.subtree_end(i);
wanted += swallowed_until - i;
roots.push(i);
}
if roots.is_empty() {
return;
}
if !self.depart.admit(wanted) {
self.diag.raise(Warning {
code: crate::diag::EXIT_BUDGET,
key: self.prev_tree.keys[roots[0]],
message: format!(
"this frame removed {wanted} nodes declaring `exit` and the exit store \
holds {}, so none of that removal animated: every departing node \
vanished at once, as a node with no `exit` does; `exit` is per node, \
and a list that drops many rows at once wants it on the list, not on \
every row",
crate::depart::MAX_NODES
),
});
return;
}
let mut order: Option<PaintOrder> = None;
for &i in &roots {
let place = order
.get_or_insert_with(|| PaintOrder::of(&self.prev_tree, &self.tree))
.place(&self.prev_tree, i, &self.float_stack);
let mut base = 1.0;
let mut a = self.prev_tree.parent[i];
while a != NIL {
base *= self.prev_tree.specs[a as usize].style.opacity;
a = self.prev_tree.parent[a as usize];
}
self.depart.depart(
&self.prev_tree,
i,
now,
base,
place,
&self.text,
&self.lines,
&self.fragments,
&self.paths,
);
}
self.trace_departures(&roots);
}
fn emit_ghost(&mut self, g: &Ghost, play: &Playback, scale: f32) {
let offset = play.offset.snapped(scale);
self.ghost_opacity.clear();
self.ghost_opacity.resize(g.nodes.len(), 1.0);
self.ghost_clip.clear();
self.ghost_clip.resize(g.nodes.len(), Clip::NONE);
self.ghost_clip_ids.clear();
self.ghost_clip_ids.resize(g.nodes.len(), NO_CLIP_ID);
self.ghost_rect.clear();
self.ghost_rect
.resize(g.nodes.len(), Rect::new(0.0, 0.0, 0.0, 0.0));
for (i, node) in g.nodes.iter().enumerate() {
let mut rect = Rect::new(
node.rect.x + offset.x,
node.rect.y + offset.y,
node.rect.w,
node.rect.h,
);
let mut style = node.spec.style;
let inherited = if node.parent == NIL {
if let Some(bg) = play.bg {
style.bg = bg;
}
if let Some(radius) = play.radius {
style.radius = radius;
}
if let Some((w, h)) = play.size {
rect.w = w.unwrap_or(rect.w);
rect.h = h.unwrap_or(rect.h);
}
style.opacity = play.opacity;
play.base_opacity
} else {
self.ghost_opacity[node.parent as usize]
};
let opacity = (inherited * style.opacity).clamp(0.0, 1.0);
self.ghost_opacity[i] = opacity;
let escapes = node
.spec
.layout
.float
.is_some_and(|f| !f.clipped_by_parent());
let (clip, clip_id) = if node.parent == NIL || escapes {
(Clip::NONE, NO_CLIP_ID)
} else {
let p = node.parent as usize;
let inherited = self.ghost_clip[p];
if g.nodes[p].spec.layout.clips() {
let clip =
inherited.intersect(self.ghost_rect[p], g.nodes[p].spec.style.radius);
(clip, self.display.intern_clip(clip.scaled(scale)))
} else {
(inherited, self.ghost_clip_ids[p])
}
};
self.ghost_clip[i] = clip;
self.ghost_clip_ids[i] = clip_id;
self.ghost_rect[i] = rect;
let visible = rect.intersect(&clip.rect);
if visible.w <= 0.0 || visible.h <= 0.0 {
continue;
}
let leaf = match node.content {
GhostContent::Container => Leaf::Container,
GhostContent::Text { cache_key, color } => {
match self.text.readd(cache_key, color) {
Some(tid) => Leaf::Text { tid, sel: None },
None => Leaf::Container,
}
}
GhostContent::Edit(key) => Leaf::Edit {
key,
focused: false,
pad: node.spec.layout.padding,
},
GhostContent::Image(id, opts) => Leaf::Image(id, opts),
GhostContent::Fragment(draw) => Leaf::Fragment(draw),
GhostContent::Polygon(draw) => Leaf::Polygon(draw),
GhostContent::Line {
first,
len,
width,
dash,
} => Leaf::Line {
points: &g.points[first as usize..(first + len) as usize],
width,
dash,
},
GhostContent::Path {
first,
len,
rule,
stroke_w,
dash,
hash,
angle,
} => Leaf::Path {
ops: &g.ops[first as usize..(first + len) as usize],
rule,
stroke_w,
dash,
hash,
angle,
animating: false,
},
};
let paint = Paint {
clip,
clip_id,
scale,
opacity,
};
let blur = node.spec.interact().backdrop_blur;
if blur > 0.0 {
backdrop_quad(&mut self.display.quads, rect, &style, &paint, blur);
}
let first_quad = self.display.quads.len();
painter!(self).paint_box(rect, &style, &paint, leaf);
if let Some(grad) = &node.spec.interact().gradient
&& !matches!(
node.content,
GhostContent::Line { .. }
| GhostContent::Polygon(_)
| GhostContent::Path { .. }
)
{
painter!(self).paint_gradient(grad, rect, &style, &paint, first_quad);
}
if let Some(c) = node.spec.interact().rules
&& node.spec.layout.is_table()
{
self.emit_ghost_rules(g, i, rect, offset, c, &paint);
}
}
}
fn ease_positions(&mut self) {
for i in 0..self.tree.len() {
let spec = &self.tree.specs[i];
let Some(t) = spec.transition else {
continue;
};
let enter = spec.anim().enter.filter(|e| e.offsets());
if !spec.slide && enter.is_none() {
continue;
}
let key = self.tree.keys[i];
let target = self.tree.pos[i];
let from = enter.map(|e| [target.x + e.dx, target.y + e.dy, 0.0, 0.0]);
let v = self.anim.drive(
key,
Slot::Pos,
from,
[target.x, target.y, 0.0, 0.0],
t,
spec.slide,
);
let d = Vec2::new(v[0] - target.x, v[1] - target.y).snapped(self.scale);
if d.x == 0.0 && d.y == 0.0 {
continue;
}
let end = self.tree.subtree_end(i);
for p in &mut self.tree.pos[i..end] {
p.x += d.x;
p.y += d.y;
}
}
}
fn emit_layout_events(&mut self) {
let frame_no = self.frame_no;
for i in 0..self.tree.len() {
let Some(tag) = &self.tree.specs[i].events().on_layout else {
continue;
};
let key = self.tree.keys[i];
let rect = Rect::from_pos_size(self.tree.pos[i], self.tree.size[i]);
let changed = match self.layouts.get(&key) {
Some((last, seen)) if *seen + 1 == frame_no => *last != rect,
_ => true,
};
self.layouts.insert(key, (rect, frame_no));
if !changed {
continue;
}
let parent = self.tree.parent[i];
let parent_rect = if parent == NIL {
Rect::new(0.0, 0.0, self.viewport.w, self.viewport.h)
} else {
let p = parent as usize;
Rect::from_pos_size(self.tree.pos[p], self.tree.size[p])
};
let rect_value = |r: Rect| {
Value::map([
("x", Value::Float(r.x as f64)),
("y", Value::Float(r.y as f64)),
("w", Value::Float(r.w as f64)),
("h", Value::Float(r.h as f64)),
])
};
let payload = Value::map([
("kind", Value::str("layout")),
("x", Value::Float(rect.x as f64)),
("y", Value::Float(rect.y as f64)),
("w", Value::Float(rect.w as f64)),
("h", Value::Float(rect.h as f64)),
("parent", rect_value(parent_rect)),
("scale", Value::Float(self.scale as f64)),
]);
self.pending
.push(UiEvent::on(self.tree.origins[i], key, payload).tagged(Some(tag)));
}
}
fn emit_layer_chrome(
&mut self,
layer: u32,
regions: &[ScrollRegion],
scrollbars: &mut Vec<ScrollbarRegion>,
above: usize,
scale: f32,
) {
let cursor = self.interaction.cursor();
let above = above as u32;
for r in regions {
let i = r.node as usize;
let style = self.tree.specs[i].interact().scrollbar;
if r.handler || style.mode == crate::spec::ScrollbarMode::Hidden {
continue;
}
let rest_w = style.width.unwrap_or(SCROLLBAR_W);
let active_w = rest_w + (SCROLLBAR_ACTIVE_W - SCROLLBAR_W);
let hit_w = SCROLLBAR_HIT_W.max(active_w + 2.0 * SCROLLBAR_INSET);
let max = self.tree.scroll_max[i];
let offset = self.scroll.drawn(r.key);
let clip_id = self.clip_ids.get(i).copied().unwrap_or(NO_CLIP_ID);
let mut opacity = self.opacity.get(i).copied().unwrap_or(1.0);
let track_y = Rect::new(
r.rect.x + r.rect.w - hit_w,
r.rect.y + SCROLLBAR_INSET,
hit_w,
r.rect.h - 2.0 * SCROLLBAR_INSET,
);
let track_x = Rect::new(
r.rect.x + SCROLLBAR_INSET,
r.rect.y + r.rect.h - hit_w,
r.rect.w - 2.0 * SCROLLBAR_INSET,
hit_w,
);
if style.mode == crate::spec::ScrollbarMode::Auto
&& let Some(now) = self.anim.time()
{
let held = self.interaction.is_scrollbar_dragging(r.key, ScrollAxis::Y)
|| self.interaction.is_scrollbar_dragging(r.key, ScrollAxis::X)
|| cursor.is_some_and(|p| {
(max.y > 0.0 && track_y.contains(p)) || (max.x > 0.0 && track_x.contains(p))
});
let idle = self.scroll.bar_idle(r.key, now, held);
let shown = if idle < SCROLLBAR_HOLD {
1.0
} else {
(1.0 - (idle - SCROLLBAR_HOLD) / SCROLLBAR_FADE).max(0.0)
};
if shown <= 0.0 {
continue; }
if !held {
self.owe_frame("scrollbar fade");
}
opacity *= shown as f32;
}
for (axis, track) in [(ScrollAxis::Y, track_y), (ScrollAxis::X, track_x)] {
let (max_a, offset_a) = match axis {
ScrollAxis::Y => (max.y, offset.y),
ScrollAxis::X => (max.x, offset.x),
};
if max_a <= 0.0 {
continue;
}
let active = self.interaction.is_scrollbar_dragging(r.key, axis)
|| cursor.is_some_and(|p| track.contains(p));
let w = if active { active_w } else { rest_w };
let (thumb, bar_len) = thumb_along(axis, r.rect, track, max_a, offset_a, w);
let mut bar =
scrollbar_quad(thumb, scale, clip_id, self.thumb_color(&style, active));
bar.color.a *= opacity;
self.display.quads.push(bar);
scrollbars.push(ScrollbarRegion {
key: r.key,
axis,
thumb,
track,
bar_len,
max: max_a,
inert: r.inert,
above,
});
}
}
self.emit_focus_ring(layer, scale);
}
#[inline]
fn layer_of(&self, i: usize) -> u32 {
self.float_root.get(i).copied().unwrap_or(NIL)
}
fn stack_floats(&mut self, roots: &[u32]) -> Vec<u32> {
let keys = &self.tree.keys;
let parent = &self.tree.parent;
let float_root = &self.float_root;
let outer_of = |root: u32| {
let p = parent[root as usize];
if p == NIL {
NIL
} else {
float_root[p as usize]
}
};
let nested_above = |order: &[u32]| {
order.iter().enumerate().all(|(pos, &r)| {
let outer = outer_of(r);
outer == NIL || order[..pos].contains(&outer)
})
};
let stack = &mut self.float_stack;
let steady = stack.len() == roots.len()
&& stack
.iter()
.all(|&(k, rank)| keys[roots[rank as usize] as usize] == k);
let order: Vec<u32> = if steady {
stack
.iter()
.map(|&(_, rank)| roots[rank as usize])
.collect()
} else {
Vec::new()
};
let order = if steady && nested_above(&order) {
order
} else {
let rank_of: FxHashMap<Key, u32> = roots
.iter()
.enumerate()
.map(|(rank, &r)| (keys[r as usize], rank as u32))
.collect();
let mut placed = vec![false; roots.len()];
let mut next: Vec<(Key, u32)> = Vec::with_capacity(roots.len());
for &(k, _) in stack.iter() {
if let Some(&rank) = rank_of.get(&k) {
placed[rank as usize] = true;
next.push((k, rank));
}
}
for (rank, &r) in roots.iter().enumerate() {
if !placed[rank] {
next.push((keys[r as usize], rank as u32));
}
}
let rank_of_outer = |root: u32| {
let outer = outer_of(root);
if outer == NIL {
None
} else {
rank_of.get(&keys[outer as usize]).copied()
}
};
let mut out: Vec<(Key, u32)> = Vec::with_capacity(next.len());
let mut out_has = vec![false; roots.len()];
let mut waiting: Vec<(Key, u32)> = Vec::new();
for e in next {
match rank_of_outer(roots[e.1 as usize]) {
Some(outer) if !out_has[outer as usize] => waiting.push(e),
_ => {
out_has[e.1 as usize] = true;
out.push(e);
while let Some(i) = waiting.iter().position(|w| {
rank_of_outer(roots[w.1 as usize])
.is_none_or(|outer| out_has[outer as usize])
}) {
let w = waiting.remove(i);
out_has[w.1 as usize] = true;
out.push(w);
}
}
}
}
debug_assert!(
waiting.is_empty(),
"a float inside a float this frame has no root"
);
*stack = out;
stack
.iter()
.map(|&(_, rank)| roots[rank as usize])
.collect()
};
debug_assert!(nested_above(&order));
order
}
fn check_layers_over_modal(&mut self, order: &[u32]) {
let Some((start, end, modal_key)) = self.modal else {
return;
};
let modal_layer = self.layer_of(start);
if modal_layer == NIL {
return; }
let Some(at) = order.iter().position(|&r| r == modal_layer) else {
return;
};
let over = order[at + 1..].iter().any(|&r| {
let root = r as usize;
!(start..end).contains(&root)
&& (root..self.tree.subtree_end(root)).any(|i| {
self.float_root[i] == r
&& (self.tree.specs[i].hover_tracked()
|| crate::access::focusable(&self.tree, i))
})
});
if over {
self.diag.raise(crate::diag::modal_under_layer(modal_key));
}
}
fn emit_focus_ring(&mut self, layer: u32, scale: f32) {
if !self.focus_visible {
return;
}
let Some(i) = self.focus_index() else {
return;
};
if self.layer_of(i) != layer {
return;
}
let spec = &self.tree.specs[i];
let editor = matches!(self.tree.content[i], NodeContent::Edit(_))
|| spec
.access()
.role
.is_some_and(crate::access::Role::is_editor);
if editor
|| spec.events().on_key.is_some()
|| spec.interact().focus_bg.is_some()
|| spec.disabled
{
return;
}
let node = Rect::from_pos_size(self.tree.pos[i], self.tree.size[i]);
let rect = Rect::new(
node.x - FOCUS_RING_GAP,
node.y - FOCUS_RING_GAP,
node.w + 2.0 * FOCUS_RING_GAP,
node.h + 2.0 * FOCUS_RING_GAP,
);
let clip = self.clips.get(i).copied().unwrap_or(Clip::NONE);
let clip_id = self.clip_ids.get(i).copied().unwrap_or(NO_CLIP_ID);
let visible = rect.intersect(&clip.rect);
if visible.w <= 0.0 || visible.h <= 0.0 {
return;
}
let mut ring = self.theme().focus_ring;
ring.a *= self.opacity.get(i).copied().unwrap_or(1.0);
self.display.quads.push(Quad {
rect: rect.scaled(scale),
color: Color::TRANSPARENT,
border_color: ring,
radius: spec.style.radius.map(|r| (r + FOCUS_RING_GAP) * scale),
border_w: FOCUS_RING_W * scale,
blur: 0.0,
kind: QuadKind::Solid,
clip: clip_id,
uv: [0; 4],
});
}
pub fn ime_rect(&self) -> Option<Rect> {
self.ime_rect
}
pub(crate) fn stock_caret_viewport_rect(&mut self, key: Key) -> Option<(usize, Rect)> {
let i = (0..self.tree.len()).find(|&i| self.tree.content[i] == NodeContent::Edit(key))?;
let caret = self.edit_with_fonts(|edit, fs| edit.caret_rect(key, fs))?;
let pad = self.tree.specs[i].layout.padding;
Some((
i,
Rect::new(
self.tree.pos[i].x + pad.l + caret.x / self.scale,
self.tree.pos[i].y + pad.t + caret.y / self.scale,
caret.w / self.scale,
caret.h / self.scale,
),
))
}
fn focused_caret_rect(&mut self) -> Option<Rect> {
if let Some(key) = self.edit.focused() {
return self.stock_caret_viewport_rect(key).map(|(_, r)| r);
}
let (l, caret, _) = self.sink_caret_line()?;
self.text.caret_at(self.tree.keys[l], caret as usize, false)
}
fn sink_caret_line(&self) -> Option<(usize, u32, bool)> {
if !self.tree.any_line {
return None;
}
let i = self.focus_index()?;
let i = if self.tree.specs[i].events().on_key.is_some() {
i
} else {
self.enclosing_sink(i)?
};
let l = crate::access::lines_under(&self.tree, i)
.into_iter()
.rev()
.find(|&l| self.tree.specs[l].access().caret.is_some())?;
let access = self.tree.specs[l].access();
Some((l, access.caret?, access.caret_solid))
}
fn note_sink_caret(&mut self) {
let (now, solid) = if self.edit.focused().is_some() {
(None, false)
} else {
match self.sink_caret_line() {
Some((l, offset, solid)) => (Some((self.tree.keys[l], offset)), solid),
None => (None, false),
}
};
if now != self.sink_caret {
self.sink_caret = now;
self.sink_caret_stamp += 1;
}
self.sink_caret_solid = solid;
}
pub fn has_caret(&self) -> bool {
self.edit.focused().is_some() || (self.sink_caret.is_some() && !self.sink_caret_solid)
}
pub fn caret_stamp(&self) -> u64 {
self.edit.caret_stamp().wrapping_add(self.sink_caret_stamp)
}
pub fn caret_visible(&self) -> bool {
self.edit.blink_visible()
}
pub fn set_caret_visible(&mut self, visible: bool) {
self.edit.set_blink_visible(visible);
}
}
struct Measure<'a> {
text: &'a mut TextSystem,
cells: &'a mut crate::cells::CellStore,
fonts: &'a mut cosmic_text::FontSystem,
edit: &'a mut EditStore,
resources: &'a Resources,
}
impl TextMeasure for Measure<'_> {
fn intrinsic(&mut self, id: crate::tree::TextId) -> Size {
self.text.intrinsic(id)
}
fn wrapped(&mut self, id: crate::tree::TextId, max_w: f32) -> Size {
self.text.wrapped(id, max_w, self.fonts)
}
fn min_content(&mut self, id: crate::tree::TextId) -> f32 {
self.text.min_content(id)
}
fn edit_intrinsic(&mut self, key: Key) -> Size {
self.edit.intrinsic(key, self.fonts)
}
fn edit_wrapped(&mut self, key: Key, max_w: f32) -> Size {
self.edit.wrapped(key, max_w, self.fonts)
}
fn baseline(&mut self, id: crate::tree::TextId) -> f32 {
self.text.baseline(id)
}
fn edit_baseline(&mut self, key: Key) -> f32 {
self.edit.baseline(key)
}
fn cells_size(&mut self, id: crate::cells::CellsId) -> Size {
self.cells.size(id, self.resources, self.fonts)
}
fn image_size(&mut self, id: crate::resources::ImageId) -> Size {
self.resources
.image(id)
.map_or(Size::ZERO, |e| Size::new(e.width as f32, e.height as f32))
}
}
#[derive(Clone, Copy)]
enum Leaf<'a> {
Container,
Text {
tid: crate::tree::TextId,
sel: Option<((usize, usize), Color)>,
},
Cells {
cid: crate::cells::CellsId,
at: Vec2,
sel: Option<&'a crate::select::CellSelection>,
tint: Color,
},
Edit {
key: Key,
focused: bool,
pad: crate::geom::Edges,
},
Image(crate::resources::ImageId, crate::resources::ImageOpts),
Fragment(crate::fragment::Draw),
Polygon(crate::fragment::Draw),
Line {
points: &'a [Vec2],
width: f32,
dash: Option<crate::line::Cut>,
},
Path {
ops: &'a [crate::path::PathOp],
rule: crate::path::FillRule,
stroke_w: f32,
dash: Option<crate::line::Cut>,
hash: u64,
angle: Option<f32>,
animating: bool,
},
}
struct Painter<'a> {
display: &'a mut DisplayList,
text: &'a mut TextSystem,
edit: &'a mut EditStore,
cells: &'a mut crate::cells::CellStore,
atlas: &'a mut GlyphAtlas,
session: &'a Session,
path_tex: &'a mut crate::path::PathTextures,
frame_no: u64,
}
impl Painter<'_> {
#[inline(always)]
fn paint_box(
&mut self,
rect: Rect,
style: &crate::spec::VisualStyle,
paint: &Paint,
leaf: Leaf<'_>,
) {
let Paint {
clip,
clip_id,
scale,
opacity,
} = *paint;
let clip_px = clip.scaled(scale);
let first_quad = self.display.quads.len();
if style.shadow.is_visible() {
self.display
.quads
.push(shadow_quad(style, rect, clip_id, scale));
}
let is_line = matches!(
leaf,
Leaf::Line { .. } | Leaf::Polygon(_) | Leaf::Path { .. }
);
if !is_line
&& (style.bg.is_visible() || (style.border_w > 0.0 && style.border_color.is_visible()))
{
let px = rect.scaled(scale);
self.display.quads.push(Quad {
rect: if style.pixel_snap { px.on_pixels() } else { px },
color: style.bg,
border_color: style.border_color,
radius: style.radius.map(|r| r * scale),
border_w: style.border_w * scale,
blur: 0.0,
kind: QuadKind::Solid,
clip: clip_id,
uv: [0; 4],
});
}
if !matches!(leaf, Leaf::Container) {
let covers = matches!(leaf, Leaf::Image(..) | Leaf::Fragment(..));
let solid = self.display.quads.len().checked_sub(1);
self.paint_leaf(rect, style, paint, clip_px, leaf);
if covers && style.border_w > 0.0 && style.border_color.is_visible() {
ring_over_content(&mut self.display.quads, solid);
}
}
if opacity < 1.0 {
fade(&mut self.display.quads[first_quad..], opacity);
}
}
#[cold]
#[inline(never)]
fn paint_gradient(
&mut self,
g: &crate::gradient::Gradient,
rect: Rect,
style: &crate::spec::VisualStyle,
paint: &Paint,
first_quad: usize,
) {
if !g.is_drawable() {
return;
}
let (w, h) = g.slot_size();
let Some(slot) = self
.atlas
.get_or_insert_gradient(g.key(), w, h, || g.rasterize())
else {
return;
};
let quads = &mut self.display.quads;
let px = rect.scaled(paint.scale);
let image = Quad {
rect: if style.pixel_snap { px.on_pixels() } else { px },
color: Color {
a: paint.opacity.min(1.0),
..Color::WHITE
},
border_color: Color::TRANSPARENT,
radius: style.radius.map(|r| r * paint.scale),
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Image,
clip: paint.clip_id,
uv: [slot.x + 1, slot.y + 1, slot.w - 2, slot.h - 2],
};
let at = first_quad + usize::from(style.shadow.is_visible());
let bordered = style.border_w > 0.0 && style.border_color.is_visible();
if !style.bg.is_visible() && !bordered {
quads.insert(at, image);
return;
}
let ring = (bordered && quads[at].border_w > 0.0).then(|| Quad {
color: Color::TRANSPARENT,
..quads[at]
});
if style.bg.is_visible() {
quads[at].border_w = 0.0;
quads[at].border_color = Color::TRANSPARENT;
quads.insert(at + 1, image);
} else {
quads[at] = image;
}
let over = at + 1 + usize::from(style.bg.is_visible());
if let Some(ring) = ring {
quads.insert(over, ring);
}
}
#[inline(never)]
fn paint_leaf(
&mut self,
rect: Rect,
style: &crate::spec::VisualStyle,
paint: &Paint,
clip_px: crate::display::Clip,
leaf: Leaf<'_>,
) {
let Paint { clip_id, scale, .. } = *paint;
match leaf {
Leaf::Container => {}
Leaf::Text { tid, sel } => {
let sess = &mut *self.session.state();
self.text.emit(
tid,
Vec2::new(rect.x, rect.y),
Size::new(rect.w, rect.h),
clip_px,
clip_id,
&mut self.display.clips,
&sess.resources,
&mut sess.fonts,
self.atlas,
&mut self.display.quads,
sel,
);
}
Leaf::Cells { cid, at, sel, tint } => {
let sess = &mut *self.session.state();
self.cells.emit(
cid,
at,
clip_px,
clip_id,
&sess.resources,
&mut sess.fonts,
self.text.raster_mut(),
self.atlas,
&mut self.display.quads,
sel.map(|s| (s, tint)),
);
}
Leaf::Edit { key, focused, pad } => {
let origin = Vec2::new(
crate::geom::snap_px((rect.x + pad.l) * scale),
crate::geom::snap_px((rect.y + pad.t) * scale),
);
let (edit_clip, edit_clip_id) = if self.edit.folds(key) {
(clip_px, clip_id)
} else {
let narrowed = clip_px.intersect(
Rect::new(
origin.x,
clip_px.rect.y,
(rect.w - pad.x()).max(0.0) * scale,
clip_px.rect.h,
),
crate::display::SQUARE,
);
(narrowed, self.display.intern_clip(narrowed))
};
let sess = &mut *self.session.state();
self.edit.emit(
key,
origin,
focused,
edit_clip,
edit_clip_id,
&mut sess.fonts,
self.text,
self.atlas,
&mut self.display.quads,
);
}
Leaf::Image(id, opts) => {
let sess = self.session.state();
if let Some(entry) = sess.resources.image(id) {
let slot =
match entry.backing {
crate::resources::ImageBacking::Atlas => self
.atlas
.get_or_insert_image(id, entry.width, entry.height, &entry.rgba),
crate::resources::ImageBacking::Texture => None,
};
let (kind, uv) = match slot {
Some(slot) => (QuadKind::Image, [slot.x, slot.y, slot.w, slot.h]),
None => (
QuadKind::Texture,
[self.display.textures.len() as u32, 0, 0, 0],
),
};
let mut uv = uv;
let (rect, tex_uv) = fit_image(
opts.fit,
rect,
Size::new(entry.width as f32, entry.height as f32),
[0, 0, entry.width, entry.height],
);
if kind == QuadKind::Image {
uv = [uv[0] + tex_uv[0], uv[1] + tex_uv[1], tex_uv[2], tex_uv[3]];
} else {
self.display
.textures
.push(crate::display::TextureDraw { id, uv: tex_uv });
self.display
.texture_pixels
.push(crate::display::TexturePixels {
width: entry.width,
height: entry.height,
rev: entry.rev,
rgba: entry.rgba.clone(),
});
}
self.display.quads.push(Quad {
rect: rect.scaled(scale),
color: Color::WHITE,
border_color: Color::TRANSPARENT,
radius: style.radius.map(|r| r * scale),
border_w: match opts.sampling {
crate::resources::Sampling::Linear => 0.0,
crate::resources::Sampling::Nearest => 1.0,
},
blur: 0.0,
kind,
clip: clip_id,
uv,
});
}
}
Leaf::Fragment(draw) => {
let px = rect.scaled(scale);
push_fragment(
self.display,
self.atlas,
&self.session.state().resources,
draw,
if style.pixel_snap { px.on_pixels() } else { px },
style.radius.map(|r| r * scale),
clip_id,
Color::WHITE,
);
}
Leaf::Polygon(draw) => {
if style.bg.is_visible() {
push_fragment(
self.display,
self.atlas,
&self.session.state().resources,
draw,
rect.scaled(scale),
crate::display::SQUARE,
clip_id,
style.bg,
);
}
}
Leaf::Line {
points,
width,
dash,
} => {
let origin = Vec2::new(rect.x, rect.y);
let quads = &mut self.display.quads;
let cut = dash.is_some_and(|d| {
push_marks(quads, origin, points, width, d, style.bg, clip_id, scale)
});
if !cut {
push_segments(quads, origin, points, width, style.bg, clip_id, scale);
}
}
Leaf::Path {
ops,
rule,
stroke_w,
dash,
hash,
angle,
animating,
} => {
let px = rect.scaled(scale);
let mask = match angle {
None => {
let bin_of = |f: f32| -> (f32, u8) {
let b = (f * 4.0).round() as u8;
if b >= 4 { (1.0, 0) } else { (0.0, b) }
};
let (fx, fy) = (px.x.floor(), px.y.floor());
let (cx, bx) = bin_of(px.x - fx);
let (cy, by) = bin_of(px.y - fy);
MaskAt {
at: Rect::new(
fx + cx,
fy + cy,
(px.w.ceil() + 1.0).max(1.0),
(px.h.ceil() + 1.0).max(1.0),
),
bin: (bx, by),
off: Vec2::new(f32::from(bx) * 0.25, f32::from(by) * 0.25),
angle: 0.0,
}
}
Some(angle) => {
let side = 2.0 * (px.w * 0.5).ceil() + 2.0;
let (cx, cy) = ((px.x + px.w * 0.5).round(), (px.y + px.h * 0.5).round());
MaskAt {
at: Rect::new(cx - side * 0.5, cy - side * 0.5, side, side),
bin: crate::path::TURNED_BIN,
off: Vec2::new((side - px.w) * 0.5, (side - px.h) * 0.5),
angle,
}
}
};
if style.bg.is_visible() {
self.paint_mask(
ops,
hash,
scale,
mask,
crate::path::MaskPaint::Fill(rule),
style.bg,
clip_id,
animating,
);
}
if stroke_w > 0.0 && style.border_color.is_visible() {
self.paint_mask(
ops,
hash,
scale,
mask,
match dash {
Some(cut) => crate::path::MaskPaint::Dashed(
stroke_w * scale,
crate::line::Cut {
lens: cut.lens.map(|l| l * scale),
offset: cut.offset * scale,
},
),
None => crate::path::MaskPaint::Stroke(stroke_w * scale),
},
style.border_color,
clip_id,
animating,
);
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn paint_mask(
&mut self,
ops: &[crate::path::PathOp],
hash: u64,
scale: f32,
mask: MaskAt,
paint: crate::path::MaskPaint,
color: Color,
clip_id: ClipId,
animating: bool,
) {
let MaskAt {
at,
bin,
off,
angle,
} = mask;
let (w, h) = (at.w as u32, at.h as u32);
let key = crate::path::mask_key(hash, scale, bin, paint);
let texels = u64::from(w) * u64::from(h);
let slot = if animating || texels >= crate::path::MAX_ATLAS_MASK_TEXELS {
None
} else {
self.atlas.get_or_insert_path(key, w, h, || {
crate::path::rasterize_at(ops, scale, off, w, h, paint)
})
};
let (kind, uv) = match slot {
Some(slot) => (QuadKind::GlyphMask, [slot.x, slot.y, slot.w, slot.h]),
None => {
let tex =
self.path_tex
.get_or_make(key, w, h, self.frame_no, self.session.id(), || {
crate::path::rasterize_at(ops, scale, off, w, h, paint)
});
let index = self.display.textures.len() as u32;
self.display.textures.push(crate::display::TextureDraw {
id: tex.id,
uv: [0, 0, w, h],
});
self.display
.texture_pixels
.push(crate::display::TexturePixels {
width: w,
height: h,
rev: 0,
rgba: tex.rgba.clone(),
});
(QuadKind::Texture, [index, 0, 0, 0])
}
};
self.display.quads.push(Quad {
rect: at,
color,
border_color: Color::TRANSPARENT,
radius: crate::display::SQUARE,
border_w: 0.0,
blur: angle,
kind,
clip: clip_id,
uv,
});
}
}
#[derive(Clone, Copy)]
struct MaskAt {
at: Rect,
bin: (u8, u8),
off: Vec2,
angle: f32,
}
#[derive(Clone, Copy)]
struct Paint {
clip: Clip,
clip_id: ClipId,
scale: f32,
opacity: f32,
}
struct PaintOrder {
float_root: Vec<u32>,
next_live: Vec<u32>,
live: FxHashSet<Key>,
roots: FxHashSet<Key>,
}
impl PaintOrder {
fn of(prev: &Tree, tree: &Tree) -> Self {
let n = prev.len();
let live: FxHashSet<Key> = tree.keys.iter().copied().collect();
let roots: FxHashSet<Key> = (0..tree.len())
.filter(|&i| tree.opens_layer(i))
.map(|i| tree.keys[i])
.collect();
let mut float_root = vec![NIL; n];
for i in 0..n {
let parent = prev.parent[i];
float_root[i] = if prev.opens_layer(i) {
i as u32
} else if parent != NIL {
float_root[parent as usize]
} else {
NIL
};
}
let mut next_live = vec![NIL; n + 1];
for j in (0..n).rev() {
next_live[j] = next_live[j + 1];
if float_root[j] == NIL && live.contains(&prev.keys[j]) {
next_live[j] = j as u32;
}
}
Self {
float_root,
next_live,
live,
roots,
}
}
fn place(&self, prev: &Tree, root: usize, stack: &[(Key, u32)]) -> Place {
let end = prev.subtree_end(root);
let layer = self.float_root[root];
if layer == NIL {
let after = self.next_live[end];
return Place::InFlow {
before: (after != NIL).then(|| prev.keys[after as usize]),
};
}
if layer as usize != root {
let layer_end = prev.subtree_end(layer as usize);
let before = (end..layer_end)
.find(|&j| self.float_root[j] == layer && self.live.contains(&prev.keys[j]))
.map(|j| prev.keys[j]);
return Place::InLayer {
layer: prev.keys[layer as usize],
before,
};
}
let key = prev.keys[root];
let at = stack.iter().position(|&(k, _)| k == key);
let before = at.and_then(|at| {
stack[at + 1..]
.iter()
.map(|&(k, _)| k)
.find(|k| self.roots.contains(k))
});
Place::Layer { before }
}
}
#[inline(never)]
fn push_segments(
quads: &mut Vec<Quad>,
origin: Vec2,
points: &[Vec2],
width: f32,
color: Color,
clip_id: ClipId,
scale: f32,
) {
let pad = crate::line::pad(width) * scale;
let w = width.max(0.0) * scale;
for pair in points.windows(2) {
let a = Vec2::new(
(origin.x + pair[0].x) * scale,
(origin.y + pair[0].y) * scale,
);
let b = Vec2::new(
(origin.x + pair[1].x) * scale,
(origin.y + pair[1].y) * scale,
);
let (x0, x1) = (a.x.min(b.x) - pad, a.x.max(b.x) + pad);
let (y0, y1) = (a.y.min(b.y) - pad, a.y.max(b.y) + pad);
quads.push(Quad {
rect: Rect::new(x0, y0, x1 - x0, y1 - y0),
color,
border_color: Color::TRANSPARENT,
radius: crate::display::SQUARE,
border_w: w,
blur: 0.0,
kind: QuadKind::Segment,
clip: clip_id,
uv: Quad::segment_uv([a.x, a.y, b.x, b.y]),
});
}
}
#[inline(never)]
#[allow(clippy::too_many_arguments)]
fn push_marks(
quads: &mut Vec<Quad>,
origin: Vec2,
points: &[Vec2],
width: f32,
cut: crate::line::Cut,
color: Color,
clip_id: ClipId,
scale: f32,
) -> bool {
let pad = crate::line::pad(width) * scale;
let w = width.max(0.0) * scale;
cut.marks(points, 1.0 / scale, |from, to| {
let a = Vec2::new((origin.x + from.x) * scale, (origin.y + from.y) * scale);
let b = Vec2::new((origin.x + to.x) * scale, (origin.y + to.y) * scale);
let (x0, x1) = (a.x.min(b.x) - pad, a.x.max(b.x) + pad);
let (y0, y1) = (a.y.min(b.y) - pad, a.y.max(b.y) + pad);
quads.push(Quad {
rect: Rect::new(x0, y0, x1 - x0, y1 - y0),
color,
border_color: Color::TRANSPARENT,
radius: crate::display::SQUARE,
border_w: w,
blur: 0.0,
kind: QuadKind::Segment,
clip: clip_id,
uv: Quad::segment_uv([a.x, a.y, b.x, b.y]),
});
})
}
#[inline(never)]
fn shadow_quad(style: &crate::spec::VisualStyle, rect: Rect, clip_id: ClipId, scale: f32) -> Quad {
let sh = style.shadow;
let blur = sh.blur.max(0.0);
let shape = Rect::new(
rect.x + sh.dx - sh.spread,
rect.y + sh.dy - sh.spread,
(rect.w + 2.0 * sh.spread).max(0.0),
(rect.h + 2.0 * sh.spread).max(0.0),
);
let rect = if style.pixel_snap {
let s = shape.scaled(scale).on_pixels();
let b = blur * scale;
Rect::new(s.x - b, s.y - b, s.w + 2.0 * b, s.h + 2.0 * b)
} else {
Rect::new(
(shape.x - blur) * scale,
(shape.y - blur) * scale,
(shape.w + 2.0 * blur) * scale,
(shape.h + 2.0 * blur) * scale,
)
};
Quad {
rect,
color: sh.color,
border_color: Color::TRANSPARENT,
radius: style.radius.map(|r| (r + sh.spread).max(0.0) * scale),
border_w: 0.0,
blur: blur * scale,
kind: QuadKind::Shadow,
clip: clip_id,
uv: [0; 4],
}
}
#[cold]
#[inline(never)]
fn backdrop_quad(
quads: &mut Vec<Quad>,
rect: Rect,
style: &crate::spec::VisualStyle,
paint: &Paint,
radius: f32,
) {
let px = rect.scaled(paint.scale);
if px.w <= 0.0 || px.h <= 0.0 || paint.opacity <= 0.0 {
return;
}
quads.push(Quad {
rect: if style.pixel_snap { px.on_pixels() } else { px },
color: Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: paint.opacity.min(1.0),
},
border_color: Color::TRANSPARENT,
radius: style.radius.map(|r| r * paint.scale),
border_w: 0.0,
blur: radius * paint.scale,
kind: QuadKind::Backdrop,
clip: paint.clip_id,
uv: [0; 4],
});
}
fn fade(quads: &mut [Quad], opacity: f32) {
for q in quads {
q.color.a *= opacity;
q.border_color.a *= opacity;
}
}
fn thumb_along(
axis: ScrollAxis,
rect: Rect,
track: Rect,
max: f32,
offset: f32,
w: f32,
) -> (Rect, f32) {
let t = (offset / max).clamp(0.0, 1.0);
match axis {
ScrollAxis::Y => {
let bar = (track.h * rect.h / (rect.h + max)).max(SCROLLBAR_MIN);
let thumb = Rect::new(
rect.x + rect.w - w - SCROLLBAR_INSET,
track.y + t * (track.h - bar),
w,
bar,
);
(thumb, bar)
}
ScrollAxis::X => {
let bar = (track.w * rect.w / (rect.w + max)).max(SCROLLBAR_MIN);
let thumb = Rect::new(
track.x + t * (track.w - bar),
rect.y + rect.h - w - SCROLLBAR_INSET,
bar,
w,
);
(thumb, bar)
}
}
}
struct RuledChild {
rect: Rect,
cells: Option<Vec<Rect>>,
}
fn rule_lines(
rect: Rect,
pad: crate::geom::Edges,
rule_w: f32,
children: &[RuledChild],
) -> Vec<Rect> {
let w = if rule_w > 0.0 { rule_w } else { 1.0 };
let Some(widest) = children
.iter()
.filter_map(|c| c.cells.as_ref())
.max_by_key(|cells| cells.len())
else {
return Vec::new();
};
let mut lines: Vec<Rect> = Vec::new();
for pair in children.windows(2) {
let (a, b) = (pair[0].rect, pair[1].rect);
let y = (a.y + a.h + b.y) / 2.0;
let x = rect.x + pad.l;
lines.push(Rect::new(x, y - w / 2.0, rect.w - pad.l - pad.r, w));
}
let mut run = 0usize;
while run < children.len() {
if children[run].cells.is_none() {
run += 1;
continue;
}
let mut end = run;
while end + 1 < children.len() && children[end + 1].cells.is_some() {
end += 1;
}
let (top, last) = (children[run].rect.y, children[end].rect);
let bottom = last.y + last.h;
for pair in widest.windows(2) {
let x = (pair[0].x + pair[0].w + pair[1].x) / 2.0;
lines.push(Rect::new(x - w / 2.0, top, w, bottom - top));
}
run = end + 1;
}
lines
}
fn push_rules(
display: &mut DisplayList,
lines: &[Rect],
color: Color,
paint: &Paint,
clip_id: ClipId,
) {
let color = Color {
a: color.a * paint.opacity,
..color
};
for line in lines {
display.quads.push(Quad {
rect: line.scaled(paint.scale).on_pixels(),
color,
border_color: Color::TRANSPARENT,
radius: [0.0; 4],
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Solid,
clip: clip_id,
uv: [0; 4],
});
}
}
fn scrollbar_quad(bar: Rect, scale: f32, clip_id: ClipId, color: Color) -> Quad {
Quad {
rect: bar.scaled(scale),
color,
border_color: Color::TRANSPARENT,
radius: [bar.w.min(bar.h) / 2.0 * scale; 4],
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Solid,
clip: clip_id,
uv: [0; 4],
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn fit_image(
fit: crate::resources::ImageFit,
rect: Rect,
image: Size,
texels: [u32; 4],
) -> (Rect, [u32; 4]) {
use crate::resources::ImageFit;
if image.w <= 0.0 || image.h <= 0.0 || rect.w <= 0.0 || rect.h <= 0.0 {
return (rect, texels);
}
let box_aspect = rect.w / rect.h;
let image_aspect = image.w / image.h;
match fit {
ImageFit::Fill => (rect, texels),
ImageFit::Contain => {
let (w, h) = if image_aspect > box_aspect {
(rect.w, rect.w / image_aspect)
} else {
(rect.h * image_aspect, rect.h)
};
(
Rect::new(
rect.x + (rect.w - w) * 0.5,
rect.y + (rect.h - h) * 0.5,
w,
h,
),
texels,
)
}
ImageFit::Cover => {
let (w, h) = if image_aspect > box_aspect {
((image.h * box_aspect).round().max(1.0), image.h)
} else {
(image.w, (image.w / box_aspect).round().max(1.0))
};
let x = ((image.w - w) * 0.5).floor();
let y = ((image.h - h) * 0.5).floor();
(
rect,
[
texels[0] + x as u32,
texels[1] + y as u32,
w as u32,
h as u32,
],
)
}
}
}
#[allow(clippy::too_many_arguments)]
fn push_fragment(
display: &mut DisplayList,
atlas: &mut GlyphAtlas,
resources: &crate::resources::Resources,
draw: crate::fragment::Draw,
rect: Rect,
radius: [f32; 4],
clip_id: ClipId,
color: Color,
) {
let Some(source) = resources.fragment(draw.id) else {
return;
};
let image = match draw.image {
None => crate::display::FragmentImage::None,
Some(id) => {
let Some(entry) = resources.image(id) else {
return;
};
let slot = match entry.backing {
crate::resources::ImageBacking::Atlas => {
atlas.get_or_insert_image(id, entry.width, entry.height, &entry.rgba)
}
crate::resources::ImageBacking::Texture => None,
};
match slot {
Some(slot) => {
crate::display::FragmentImage::Atlas([slot.x, slot.y, slot.w, slot.h])
}
None => {
let index = display.textures.len() as u32;
let uv = [0, 0, entry.width, entry.height];
display
.textures
.push(crate::display::TextureDraw { id, uv });
display.texture_pixels.push(crate::display::TexturePixels {
width: entry.width,
height: entry.height,
rev: entry.rev,
rgba: entry.rgba.clone(),
});
crate::display::FragmentImage::Texture { index, uv }
}
}
}
};
let index = display.fragments.len() as u32;
display.fragments.push(crate::display::FragmentDraw {
id: draw.id,
params: draw.params,
image,
});
display.fragment_sources.push(source.clone());
display.quads.push(Quad {
rect,
color,
border_color: Color::TRANSPARENT,
radius,
border_w: 0.0,
blur: 0.0,
kind: QuadKind::Fragment,
clip: clip_id,
uv: [index, 0, 0, 0],
});
}
#[cold]
#[inline(never)]
fn ring_over_content(quads: &mut Vec<Quad>, solid: Option<usize>) {
let Some(at) =
solid.filter(|&at| quads[at].kind == QuadKind::Solid && quads[at].border_w > 0.0)
else {
return;
};
let ring = Quad {
color: Color::TRANSPARENT,
..quads[at]
};
quads[at].border_w = 0.0;
quads[at].border_color = Color::TRANSPARENT;
quads.push(ring);
}