the layout graph is maintained, and the shine draws for real
- the interface's layout graph is maintained as anchors change instead of rediscovered from scratch: 1.48 ms per mutated frame becomes 34 µs, and end to end a running cast now costs +0.14 ms where it cost +4.33 - the autocast shine draws its authored population — 1200 particles through the star's own projection, spinning as the model says - the shine is clipped to its own widget: the model frame installs its rect as the viewport, so nothing it draws leaves the button Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -85,17 +85,19 @@ pub(super) fn ensure_measured(lua: &Lua, rh: RegionHandle) {
|
||||
natural_w,
|
||||
key: req.key,
|
||||
};
|
||||
let mut moved = false;
|
||||
if let Some(d) = model.region_data.get_mut(&rh) {
|
||||
let moved = super::types::MeasuredText::layout_moved(d.measured, new);
|
||||
moved = super::types::MeasuredText::layout_moved(d.measured, new);
|
||||
// The KEY always lands — this IS the measure cache, and a stale key re-requests forever.
|
||||
d.measured = Some(new);
|
||||
if moved {
|
||||
// Measured extents are the auto-size axes' inputs — the layout gate's read set, the
|
||||
// same touch `set_measured_text` does. Guarded on a change the LAYOUT can see: a
|
||||
// re-measure that returns the same box (a same-width countdown tick) must not open
|
||||
// tier 1, or it costs a whole-roster hash to conclude nothing moved (decision 1385).
|
||||
model.touch_layout();
|
||||
}
|
||||
}
|
||||
if moved {
|
||||
// Measured extents are the auto-size axes' inputs — the layout gate's read set, the
|
||||
// same touch `set_measured_text` does. Guarded on a change the LAYOUT can see: a
|
||||
// re-measure that returns the same box (a same-width countdown tick) must not open
|
||||
// tier 1, or it costs a whole-roster hash to conclude nothing moved (decision 1385).
|
||||
// A new extent moves no edge and no roster membership, so it names its node (1388).
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -894,11 +894,11 @@ impl UiScript {
|
||||
/// number, which is why [`Self::set_measured_text_unwrapped`] exists for tests.
|
||||
pub fn set_measured_text(&mut self, measures: &[(u32, f32, f32, f32, u64)]) {
|
||||
let mut model = self.model_mut();
|
||||
let mut changed = false;
|
||||
for &(id, w, h, natural_w, key) in measures {
|
||||
let Some(&rh) = model.id_to_region.get(&id) else {
|
||||
continue;
|
||||
};
|
||||
let mut moved = false;
|
||||
if let Some(d) = model.region_data.get_mut(&rh) {
|
||||
let new = MeasuredText {
|
||||
w,
|
||||
@@ -908,13 +908,16 @@ impl UiScript {
|
||||
};
|
||||
// The KEY always lands (or the region re-requests its own measure forever); the
|
||||
// EPOCH moves only if the laid-out extent did — see `MeasuredText::layout_moved`.
|
||||
changed |= MeasuredText::layout_moved(d.measured, new);
|
||||
moved = MeasuredText::layout_moved(d.measured, new);
|
||||
d.measured = Some(new);
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
// Measured extents are the auto-size axes' inputs — the layout gate's read set.
|
||||
model.touch_layout();
|
||||
if moved {
|
||||
// Measured extents are the auto-size axes' inputs — the layout gate's read set.
|
||||
// Touched PER REGION rather than once for the batch (decision 1388): the batch
|
||||
// touch could only say "some extent moved", which is exactly the whole-roster
|
||||
// question the ledger exists to stop asking.
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1092,6 +1095,13 @@ impl UiScript {
|
||||
self.model_ref().layout_gate_walks
|
||||
}
|
||||
|
||||
/// How many times a resolve DERIVED the layout graph from scratch ([`Model::layout_derives`])
|
||||
/// — the whole-roster walk, and since decision 1388 the only expensive thing a resolve can do.
|
||||
/// A UI that merely animates should hold this flat.
|
||||
pub fn layout_derivations(&self) -> u64 {
|
||||
self.model_ref().layout_derives
|
||||
}
|
||||
|
||||
/// Total fixpoint ROUNDS across every solve ([`Model::layout_rounds`]) — a solve costs
|
||||
/// rounds × the whole graph, so the ratio against [`Self::layout_solves`] is the per-pass
|
||||
/// depth.
|
||||
|
||||
@@ -85,6 +85,32 @@ pub(crate) struct Model {
|
||||
/// change-gated — `tooltip::append_line`, and the gate
|
||||
/// `layout_gate::the_hover_re_enter_loop_neither_re_measures_nor_re_solves`.
|
||||
pub(crate) layout_epoch: u64,
|
||||
/// [`Self::layout_epoch`]'s **precise** form: the nodes a write actually NAMED since the last
|
||||
/// converged resolve — tier 1 promoted from "something moved" to "*these* moved" (decision
|
||||
/// 1388).
|
||||
///
|
||||
/// `Some(nodes)` carries a second, stronger claim than its contents: **every** write since the
|
||||
/// cached graph was built named its node *and* left the graph's SHAPE alone, so
|
||||
/// [`super::layout::LayoutScope`]'s roster, edges and per-node hashes still describe the live
|
||||
/// model and a resolve may seed the dirty closure straight from this list. `None` is the
|
||||
/// conservative state — a write that could not name its node, or one that moved the roster or
|
||||
/// an anchor's TARGET — and forces the next resolve to derive the graph from scratch, which is
|
||||
/// exactly what every resolve did before 1388.
|
||||
///
|
||||
/// That asymmetry is the safety argument. A site that keeps calling plain
|
||||
/// [`Model::touch_layout`] is *correct by construction* (it lands in `None`, i.e. today's
|
||||
/// behaviour); only a site that opts into [`Model::touch_layout_region`] /
|
||||
/// [`Model::touch_layout_frame`] can be wrong, and `WOW_LAYOUT_VERIFY` re-resolves every
|
||||
/// incremental pass in full scope and compares rects, so being wrong fails a test rather than
|
||||
/// shipping a stale one.
|
||||
/// The list holds **layout ids**, not handles: the touch sites below already have to prove the
|
||||
/// node is in the cached roster before they may name it, and that proof is a `node_of[id]`
|
||||
/// probe — which hands back the roster row, and with it the handle, for free at resolve time.
|
||||
pub(crate) layout_touched: Option<Vec<u32>>,
|
||||
/// `WOW_LAYOUT_VERIFY`'s flag that the resolve just taken was the incremental one and owes a
|
||||
/// full-derive re-run to prove it (see `layout::UiScript::resolve_layout`). Always `false` in
|
||||
/// production — nothing reads it unless the verify build set it.
|
||||
pub(crate) layout_verify_recheck: bool,
|
||||
/// Per-node input hashes + the dirty-closure scratch — what makes a resolve cost the nodes
|
||||
/// that MOVED rather than the whole graph (decision 1350; see `script::layout::LayoutScope`).
|
||||
/// Tiers 1 and 2 above decide *whether* to solve; this decides *what*.
|
||||
@@ -125,6 +151,20 @@ pub(crate) struct Model {
|
||||
/// consumes `benilla-ui` as a dependency, where `cfg!(test)` is false and the count is
|
||||
/// production's; inside this crate's own tests, assert on `layout_solves`.
|
||||
pub(crate) layout_gate_walks: u64,
|
||||
/// How many times a resolve **derived the layout graph** — the whole-roster walk that rebuilds
|
||||
/// the frame/region roster, the reverse edges and every per-node hash (decision 1388).
|
||||
///
|
||||
/// This is the cost counter [`Self::layout_gate_walks`] used to be. 1385 got a moving castbar
|
||||
/// spark down from three walks per frame to one; 1388 made that one walk *cheap* by seeding the
|
||||
/// dirty closure from a ledger of named nodes instead of rediscovering it, so "walks" no longer
|
||||
/// separates the 1.48 ms case from the 20 µs one and this does. A UI that animates anything
|
||||
/// should hold this at **zero** frame after frame — a non-zero steady-state reading means some
|
||||
/// write site is falling back to the conservative `touch_layout`, and that is the regression.
|
||||
///
|
||||
/// Verify-independent in the direction that matters: the `WOW_LAYOUT_VERIFY` re-run's
|
||||
/// derivation is counted, but `resolve_layout` restores the meter afterwards, so a test reads
|
||||
/// the same number in both modes.
|
||||
pub(crate) layout_derives: u64,
|
||||
/// The SCOPE of the last solve that ran: `(frames solved, regions swept)` — decision 1350's
|
||||
/// own meter, and the number its gate asserts on. `layout_solves` says how often the gate let
|
||||
/// a solve through and `layout_rounds` how deep each went; this says how WIDE, which is the
|
||||
@@ -1120,6 +1160,9 @@ impl Model {
|
||||
solver: LayoutSolver::new(),
|
||||
layout_fingerprint: None,
|
||||
layout_epoch: 0,
|
||||
layout_touched: None,
|
||||
layout_verify_recheck: false,
|
||||
layout_derives: 0,
|
||||
layout_scope: super::layout::LayoutScope::default(),
|
||||
layout_last_scope: (0, 0),
|
||||
layout_epoch_resolved: None,
|
||||
@@ -1424,6 +1467,97 @@ impl Model {
|
||||
/// exactly the failure `WOW_LAYOUT_VERIFY` (on for every benilla-ui test) exists to catch
|
||||
/// loudly, by proving the fingerprint still matches whenever tier 1 claims quiet.
|
||||
pub(crate) fn touch_layout(&mut self) {
|
||||
// The conservative half of the tier-1 ledger (decision 1388): this write did not name a
|
||||
// node, so the cached graph can no longer be trusted to describe the live model and the
|
||||
// next resolve must derive it in full. Every site that has NOT been migrated to a precise
|
||||
// touch lands here, which is why migration can be incremental and a missed one is slow
|
||||
// rather than wrong.
|
||||
self.layout_touched = None;
|
||||
self.bump_layout_epoch();
|
||||
}
|
||||
|
||||
/// A write that moved **one region's** layout inputs and changed nothing else about the graph:
|
||||
/// its anchor OFFSETS, its explicit size, its measured extent. Not its anchor targets, not its
|
||||
/// membership, not its liveness — those move edges or the roster, and belong on
|
||||
/// [`Self::touch_layout`].
|
||||
///
|
||||
/// Falls back to the conservative touch whenever it cannot prove the region is a node of the
|
||||
/// **cached** graph: no minted id, an id past the roster's arrays, or an id the last derive
|
||||
/// left unmapped (an anchor-less region, a region born since). That fallback is what makes the
|
||||
/// call safe to reach for — the worst a mistaken one can do is derive the graph again.
|
||||
pub(crate) fn touch_layout_region(&mut self, rh: RegionHandle) {
|
||||
match self.region_to_id.get(&rh) {
|
||||
Some(&id) => self.touch_layout_node(id),
|
||||
None => self.touch_layout(),
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::touch_layout_region`]'s frame twin — an anchor offset, a width/height, and nothing
|
||||
/// that moves an edge or the roster.
|
||||
pub(crate) fn touch_layout_frame(&mut self, h: FrameHandle) {
|
||||
match self.frame_to_id.get(&h) {
|
||||
Some(&id) => self.touch_layout_node(id),
|
||||
None => self.touch_layout(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Name a node **without opening tier 1** — for the resolve's own pre-pass
|
||||
/// (`tooltip::layout_tooltips`), which writes layout inputs derived from state that already
|
||||
/// arrived through touched paths.
|
||||
///
|
||||
/// It deliberately does not call [`Self::touch_layout`]: bumping the epoch from inside a
|
||||
/// resolve would pin the gate open forever (the pre-pass runs on every let-through resolve, so
|
||||
/// it would re-dirty the very resolve it is running in). But the ledger still has to hear about
|
||||
/// it, or an incremental pass would inherit a node whose hash the pre-pass moved and no write
|
||||
/// site named — the one shape that could ship a stale rect. Naming it is free: the hash it
|
||||
/// recomputes is unchanged on the frames the pre-pass writes idempotently, which is nearly all
|
||||
/// of them, so no dirty seed comes of it.
|
||||
pub(crate) fn note_layout_frame_write(&mut self, h: FrameHandle) {
|
||||
if self.layout_touched.is_some() {
|
||||
match self.frame_to_id.get(&h) {
|
||||
Some(&id) if self.layout_scope.has_node(id) => {
|
||||
self.layout_touched
|
||||
.as_mut()
|
||||
.expect("checked above")
|
||||
.push(id);
|
||||
}
|
||||
_ => self.layout_touched = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Self::note_layout_frame_write`]'s region twin.
|
||||
pub(crate) fn note_layout_region_write(&mut self, rh: RegionHandle) {
|
||||
if self.layout_touched.is_some() {
|
||||
match self.region_to_id.get(&rh) {
|
||||
Some(&id) if self.layout_scope.has_node(id) => {
|
||||
self.layout_touched
|
||||
.as_mut()
|
||||
.expect("checked above")
|
||||
.push(id);
|
||||
}
|
||||
_ => self.layout_touched = None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Name `id` as the only thing this write moved — if the cached graph has a node for it and no
|
||||
/// earlier write in this frame already gave up on naming.
|
||||
fn touch_layout_node(&mut self, id: u32) {
|
||||
let in_graph = self.layout_scope.has_node(id);
|
||||
match &mut self.layout_touched {
|
||||
// Already conservative: a precise touch cannot un-say an imprecise one.
|
||||
None => {}
|
||||
Some(_) if !in_graph => self.layout_touched = None,
|
||||
Some(list) => list.push(id),
|
||||
}
|
||||
self.bump_layout_epoch();
|
||||
}
|
||||
|
||||
/// Tier 1's counter, shared by every touch above. Bumping it is what re-opens the gate; which
|
||||
/// of [`Self::layout_touched`]'s two states the caller leaves behind is what decides how much
|
||||
/// the re-opened resolve has to derive.
|
||||
fn bump_layout_epoch(&mut self) {
|
||||
self.layout_epoch = self.layout_epoch.wrapping_add(1);
|
||||
// `WOW_LAYOUT_TOUCH_TRACE=<n>` — name the epoch's per-frame toucher: print a backtrace
|
||||
// for the first n touches (a mechanism probe, meaningful in a debuginfo build). Built
|
||||
|
||||
@@ -30,7 +30,7 @@ mod frame_state;
|
||||
mod layout_methods;
|
||||
pub(crate) mod movable;
|
||||
pub(crate) mod toplevel;
|
||||
pub(crate) use layout_methods::anchor_bits_eq;
|
||||
pub(crate) use layout_methods::{anchor_bits_eq, anchor_retarget_is_structural};
|
||||
pub(crate) use movable::{advance_move, advance_size, FrameMove, FrameSizing};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -137,7 +137,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let changed = input.width.to_bits() != w.to_bits();
|
||||
input.width = w;
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
// A size write moves no edge and no roster membership (decision 1388).
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -151,7 +152,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let changed = input.height.to_bits() != ht.to_bits();
|
||||
input.height = ht;
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -167,7 +168,7 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
input.width = w;
|
||||
input.height = ht;
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -423,13 +424,40 @@ fn set_point(lua: &Lua, this: &Table, point: &str, rest: [Value; 4]) -> mlua::Re
|
||||
.iter()
|
||||
.any(|a| a.point == point);
|
||||
if !same_at_tail {
|
||||
// Value-only unless the target moved (decision 1388) — the per-frame `SetPoint` idiom
|
||||
// (a dragged window, a moving spark) re-points the SAME anchor at the SAME target with new
|
||||
// offsets, so it names its node and the cached graph survives the frame.
|
||||
let structural = anchor_retarget_is_structural(&input.anchors, &new);
|
||||
input.anchors.retain(|a| a.point != point);
|
||||
input.anchors.push(new);
|
||||
model.touch_layout();
|
||||
if structural {
|
||||
model.touch_layout();
|
||||
} else {
|
||||
model.touch_layout_frame(h);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Would this `SetPoint` change the node's set of anchor TARGETS — i.e. is it structural?
|
||||
///
|
||||
/// The layout scope's reverse edges are built from `Anchor::relative_to` (decision 1350): "this
|
||||
/// node reads that node's rect". An anchor whose OFFSETS moved is a value change — the node
|
||||
/// re-solves and nothing else does, which is exactly what a precise touch claims. An anchor whose
|
||||
/// TARGET moved is not: an edge has to disappear and another to appear, and no per-node hash can
|
||||
/// say so, so the cached graph must be thrown away instead (decision 1388).
|
||||
///
|
||||
/// Mirrors the `retain(point) + push` the setters do. It is value-only in exactly one shape — one
|
||||
/// existing anchor carries this point and keeps the same target. Zero (an edge appears) and two or
|
||||
/// more (edges disappear) are both structural, and so is any change of target.
|
||||
pub(crate) fn anchor_retarget_is_structural(anchors: &[Anchor], new: &Anchor) -> bool {
|
||||
let mut same_point = anchors.iter().filter(|a| a.point == new.point);
|
||||
match (same_point.next(), same_point.next()) {
|
||||
(Some(old), None) => old.relative_to != new.relative_to,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bit-exact anchor equality — the same lens the layout gate's fingerprint reads anchors through
|
||||
/// (`InputFingerprint::anchors` feeds `f32::to_bits`), so the setters' no-op detection and the
|
||||
/// gate can never disagree about whether a write "changed" something.
|
||||
|
||||
@@ -451,7 +451,11 @@ pub(crate) fn advance_size(model: &mut Model, pos: (f32, f32)) {
|
||||
// anchored regions to conclude nothing had moved — the castbar's bug class in miniature, for
|
||||
// every frame of such a drag (decision 1385).
|
||||
if moved {
|
||||
model.touch_layout();
|
||||
// Offsets only — a resize grip never repoints an anchor, so the frame names itself and the
|
||||
// cached graph survives the drag (decision 1388). A drag is per-frame by nature: this is
|
||||
// the difference between a smooth window resize and one that re-derives 13,656 nodes on
|
||||
// every mouse-move.
|
||||
model.touch_layout_frame(sz.frame);
|
||||
}
|
||||
model.sizing = Some(FrameSizing { sample: pos, ..sz });
|
||||
}
|
||||
@@ -481,8 +485,9 @@ pub(crate) fn advance_move(model: &mut Model, pos: (f32, f32)) {
|
||||
}
|
||||
// The one mutation path every layout setter shares — the tier-1 epoch, never the
|
||||
// solver's arrays (see [`super::layout_methods`]'s mutate-only-on-change law; a zero
|
||||
// delta returned above, so this write always moved something).
|
||||
model.touch_layout();
|
||||
// delta returned above, so this write always moved something). Translating every
|
||||
// anchor moves no TARGET, so the frame names itself (decision 1388).
|
||||
model.touch_layout_frame(mv.frame);
|
||||
}
|
||||
}
|
||||
model.moving = Some(FrameMove { sample: pos, ..mv });
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
|
||||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use super::object::{anchor_bits_eq, as_f32, decode_id, id_to_lud, point_from_str};
|
||||
use super::object::{
|
||||
anchor_bits_eq, anchor_retarget_is_structural, as_f32, decode_id, id_to_lud, point_from_str,
|
||||
};
|
||||
use super::{
|
||||
Model, REG_FONTSTRING_META, REG_FONTSTRING_METHODS, REG_REGION_META, REG_REGION_METHODS,
|
||||
REG_TEXTURE_META, REG_TEXTURE_METHODS, REG_TITLE_META, REG_TITLE_METHODS, REG_WRAPPERS, SCREEN,
|
||||
@@ -698,9 +700,17 @@ pub(super) fn region_set_point(
|
||||
.iter()
|
||||
.any(|a| a.point == point);
|
||||
if !same_at_tail {
|
||||
// The frame twin's law (decision 1388): re-pointing the same anchor at the same target is
|
||||
// a VALUE change and names its node; anything that moves the target set is structural.
|
||||
// This is the castbar spark's and every combat-text string's per-frame write.
|
||||
let structural = anchor_retarget_is_structural(&data.anchors, &new);
|
||||
data.anchors.retain(|a| a.point != point);
|
||||
data.anchors.push(new);
|
||||
model.touch_layout();
|
||||
if structural {
|
||||
model.touch_layout();
|
||||
} else {
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -26,7 +26,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let changed = !size_bits_eq(d.size, new);
|
||||
d.size = new;
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
// A size write moves no edge and no roster membership (decision 1388).
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -42,7 +43,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let changed = !size_bits_eq(d.size, new);
|
||||
d.size = new;
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
// A size write moves no edge and no roster membership (decision 1388).
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
@@ -58,7 +60,8 @@ pub(super) fn install(lua: &Lua, m: &Table) -> mlua::Result<()> {
|
||||
let changed = !size_bits_eq(d.size, new);
|
||||
d.size = new;
|
||||
if changed {
|
||||
model.touch_layout();
|
||||
// A size write moves no edge and no roster membership (decision 1388).
|
||||
model.touch_layout_region(rh);
|
||||
}
|
||||
Ok(())
|
||||
})?,
|
||||
|
||||
@@ -414,6 +414,13 @@ fn free_block(model: &mut Model, rh: RegionHandle) {
|
||||
model.id_to_region.remove(&id);
|
||||
}
|
||||
model.arena.destroy_region(rh);
|
||||
// A death is the archetypal STRUCTURAL change — it takes a node out of the layout roster and
|
||||
// its reverse edges with it, which is exactly what a per-node ledger cannot describe
|
||||
// (decision 1388). `set_text`, this function's only caller, already ends in the same
|
||||
// conservative touch, so this is redundant today; it is here because the redundancy is the
|
||||
// point. A destruction site that does not invalidate the graph it destroys part of is a
|
||||
// stale rect waiting for a second caller.
|
||||
model.touch_layout();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -720,6 +720,10 @@ pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
// `pad_w` = SetPadding's extra width (ref ItemRefTooltip: room for the close button).
|
||||
input.width = maxw + 2.0 * TOOLTIP_PAD + pad_w;
|
||||
input.height = totalh + 2.0 * TOOLTIP_PAD;
|
||||
// This pre-pass runs INSIDE the resolve, so it must not open tier 1 (see the note method's
|
||||
// own doc) — but the per-node ledger decision 1388's incremental pass seeds from still has
|
||||
// to hear that these two inputs were written by something other than a Lua setter.
|
||||
model.note_layout_frame_write(h);
|
||||
// Right-flush each double line: right edge = left.right + (maxw − left.width)
|
||||
// = frame.left + pad + maxw = the text inset's right.
|
||||
for (i, (l, r)) in rows.iter().enumerate() {
|
||||
@@ -727,11 +731,16 @@ pub(super) fn layout_tooltips(model: &mut Model) {
|
||||
continue;
|
||||
}
|
||||
let lw = l.map_or(0.0, |(w, _)| w);
|
||||
let mut wrote = false;
|
||||
if let Some(d) = model.region_data.get_mut(&rights[i]) {
|
||||
if let Some(a) = d.anchors.first_mut() {
|
||||
a.x_off = maxw - lw;
|
||||
wrote = true;
|
||||
}
|
||||
}
|
||||
if wrote {
|
||||
model.note_layout_region_write(rights[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user