Files
ARCADIA-Studio/harness/experiments/text-diagnostics-20260913/reviews/prepared-widget-tests.rs
T
sorokin.oleg.edu 9fd76aef3a Add current-draft diagnostic underlines and passive hover
Preserve raw include diagnostic provenance and derive bands from the existing shaped editor. Retain deferred tooltip paragraphs through rendering. Include source, regression, failure and native-trial evidence; corrected native acceptance remains pending.
2026-09-13 01:43:22 -07:00

539 lines
25 KiB
Rust

//! PRIVATE prepared tests. Parent may copy to source_annotations/widget/tests.rs
//! and register `#[cfg(all(test, debug_assertions))] mod tests;` in widget.rs.
//! No build or execution was performed while preparing this file.
//!
//! Actual Decorations + TextEditor + existing raw-input observer, one retained
//! Tree, graphics Editor/Paragraph, ID-based focus Operation and emitted editor
//! actions. The recorder observes actual fill_editor clusters, renderer layers,
//! and shaped tooltip paragraphs. No private editor-state indexing or fake
//! character-width metrics. Double/triple click events are batched through the
//! actual widget, then their emitted actions are applied in order to Content.
//!
//! Limits: CPU layout/advance coverage, not raster ink/GPU/native/IME, real App
//! publication admission, menu controller, or the production sensor/view tree.
//! Snapshot and hover_enabled are explicit widget-boundary inputs. Word/Line
//! insertion compares a hovered path with an actual direct-typing control.
//! Footer expectations target the parent's measured-height/remaining-count fix.
#![cfg(debug_assertions)]
use super::{Decorations, Snapshot};
use crate::views::source_tests::FontLayout;
use iced::advanced::{
Layout, Shell, Widget, graphics, layout, renderer,
text::{self as shaped, Paragraph as _},
widget as core_widget,
};
use iced::widget::text_editor::{self, Action, Content};
use iced::{
Background, Color, Element, Event, Font, Padding, Pixels, Point, Rectangle, Size, Theme,
Transformation, keyboard, mouse, window,
};
use oa_core::{
Id,
language::{Diagnostic, Span},
source_project::SourceDiagnostic,
validation::Severity,
};
use std::{
collections::BTreeSet,
ops::Range,
sync::{Arc, atomic::{AtomicU64, Ordering}},
};
const ORIGIN: Point = Point::new(23., 31.);
const PAD: f32 = 16.;
const EDITOR_ID: &str = "annotation-wrapper-editor";
const ROW: &str = "alpha beta gamma\n";
#[derive(Debug, Clone)]
struct Glyph {
line: usize,
bytes: Range<usize>,
bounds: Rectangle,
}
#[derive(Debug, Clone)]
struct ParagraphPaint {
content: String,
glyphs: Vec<Glyph>,
clip: Rectangle,
}
#[derive(Debug)]
struct EditorPaint {
glyphs: Vec<Glyph>,
clip: Rectangle,
}
#[derive(Debug)]
struct QuadPaint {
bounds: Rectangle,
clip: Rectangle,
layer_depth: usize,
background: Background,
}
#[derive(Debug, Default)]
struct Frame {
editors: Vec<EditorPaint>,
paragraphs: Vec<ParagraphPaint>,
quads: Vec<QuadPaint>,
}
impl Frame {
fn editor(&self) -> &EditorPaint {
assert_eq!(self.editors.len(), 1, "one actual fill_editor per frame");
&self.editors[0]
}
fn point(&self, line: usize, byte: usize) -> Point {
let editor = self.editor();
let glyph = editor.glyphs.iter().find(|g| {
g.line == line && g.bytes.contains(&byte) && inside(g.bounds, editor.clip)
}).expect("requested fixture cluster must be genuinely visible");
Point::new(
glyph.bounds.x + glyph.bounds.width / 2.,
glyph.bounds.y + glyph.bounds.height / 2.,
)
}
fn tooltip(&self) -> String {
self.paragraphs.iter().map(|p| p.content.as_str()).collect::<Vec<_>>().join("\n")
}
fn decoration_quads(&self) -> impl Iterator<Item = &QuadPaint> {
self.quads.iter().filter(|q| q.layer_depth > 0)
}
}
fn inside(bounds: Rectangle, clip: Rectangle) -> bool {
const EPS: f32 = 0.05;
bounds.x >= clip.x - EPS && bounds.y >= clip.y - EPS
&& bounds.x + bounds.width <= clip.x + clip.width + EPS
&& bounds.y + bounds.height <= clip.y + clip.height + EPS
}
struct Recorder {
frame: Frame,
transforms: Vec<Transformation>,
clips: Vec<Rectangle>,
}
impl Recorder {
fn new(viewport: Rectangle) -> Self {
Self { frame: Frame::default(), transforms: vec![Transformation::IDENTITY], clips: vec![viewport] }
}
fn transform(&self) -> Transformation { *self.transforms.last().unwrap() }
fn clip(&self, bounds: Rectangle) -> Rectangle {
self.clips.last().unwrap().intersection(&(bounds * self.transform()))
.unwrap_or(Rectangle::with_size(Size::ZERO))
}
fn glyphs(&self, buffer: &graphics::text::cosmic_text::Buffer, origin: Point) -> Vec<Glyph> {
buffer.layout_runs().flat_map(|run| {
run.glyphs.iter().map(move |glyph| Glyph {
line: run.line_i,
bytes: glyph.start..glyph.end,
// Actual shaped advances and line bands, not estimated ink.
bounds: Rectangle::new(
Point::new(origin.x + glyph.x, origin.y + run.line_top),
Size::new(glyph.w, run.line_height),
) * self.transform(),
})
}).collect()
}
fn paragraph(&mut self, paragraph: &graphics::text::Paragraph, origin: Point, clip: Rectangle) {
let buffer = paragraph.buffer();
self.frame.paragraphs.push(ParagraphPaint {
content: buffer.lines.iter().map(|line| line.text()).collect::<Vec<_>>().join("\n"),
glyphs: self.glyphs(buffer, origin),
clip: self.clip(clip),
});
}
}
impl renderer::Renderer for Recorder {
fn start_layer(&mut self, bounds: Rectangle) { self.clips.push(self.clip(bounds)); }
fn end_layer(&mut self) { assert!(self.clips.len() > 1); self.clips.pop(); }
fn start_transformation(&mut self, value: Transformation) {
self.transforms.push(self.transform() * value);
}
fn end_transformation(&mut self) { assert!(self.transforms.len() > 1); self.transforms.pop(); }
fn reset(&mut self, bounds: Rectangle) { *self = Self::new(bounds); }
fn fill_quad(&mut self, quad: renderer::Quad, background: impl Into<Background>) {
self.frame.quads.push(QuadPaint {
bounds: quad.bounds * self.transform(),
clip: *self.clips.last().unwrap(),
layer_depth: self.clips.len() - 1,
background: background.into(),
});
}
fn allocate_image(
&mut self, handle: &iced::advanced::image::Handle,
callback: impl FnOnce(Result<iced::advanced::image::Allocation, iced::advanced::image::Error>) + Send + 'static,
) { renderer::Renderer::allocate_image(&mut FontLayout, handle, callback); }
}
impl shaped::Renderer for Recorder {
type Font = Font;
type Paragraph = graphics::text::Paragraph;
type Editor = graphics::text::Editor;
const ICON_FONT: Font = <FontLayout as shaped::Renderer>::ICON_FONT;
const CHECKMARK_ICON: char = <FontLayout as shaped::Renderer>::CHECKMARK_ICON;
const ARROW_DOWN_ICON: char = <FontLayout as shaped::Renderer>::ARROW_DOWN_ICON;
const SCROLL_UP_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_UP_ICON;
const SCROLL_DOWN_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_DOWN_ICON;
const SCROLL_LEFT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_LEFT_ICON;
const SCROLL_RIGHT_ICON: char = <FontLayout as shaped::Renderer>::SCROLL_RIGHT_ICON;
const ICED_LOGO: char = <FontLayout as shaped::Renderer>::ICED_LOGO;
fn default_font(&self) -> Font { shaped::Renderer::default_font(&FontLayout) }
fn default_size(&self) -> Pixels { shaped::Renderer::default_size(&FontLayout) }
fn fill_paragraph(&mut self, paragraph: &Self::Paragraph, origin: Point, _: Color, clip: Rectangle) {
self.paragraph(paragraph, origin, clip);
}
fn fill_editor(&mut self, editor: &Self::Editor, origin: Point, _: Color, clip: Rectangle) {
self.frame.editors.push(EditorPaint { glyphs: self.glyphs(editor.buffer(), origin), clip: self.clip(clip) });
}
fn fill_text(&mut self, value: shaped::Text, origin: Point, _: Color, clip: Rectangle) {
let paragraph = graphics::text::Paragraph::with_text(value.as_ref());
self.paragraph(&paragraph, origin, clip);
}
}
fn diagnostic(path: &str, bytes: Range<usize>, code: &str, severity: Severity) -> SourceDiagnostic {
SourceDiagnostic {
path: path.into(),
diagnostic: Diagnostic {
severity, code: code.into(), message: format!("Detail {code} 雨"),
object_id: None, field: None, line: 1, column: 1,
span: Span { start: bytes.start, end: bytes.end },
},
related: Vec::new(),
}
}
struct Mounted {
content: Content<Recorder>,
source: Arc<str>,
path: String,
diagnostics: Vec<SourceDiagnostic>,
publication: Option<Id>,
hover_enabled: bool,
tree: core_widget::Tree,
clock: Arc<AtomicU64>,
width: f32,
height: f32,
viewport: Option<Rectangle>,
}
impl Mounted {
fn new(source: &str) -> Self {
Self {
content: Content::with_text(source), source: Arc::from(source), path: "a".into(),
diagnostics: vec![diagnostic("a", 0..5, "A", Severity::Warning), diagnostic("a", 6..10, "B", Severity::Error)],
publication: Some(Id::new_v4()), hover_enabled: true, tree: core_widget::Tree::empty(),
clock: Arc::new(AtomicU64::new(0)), width: 552., height: 392., viewport: None,
}
}
fn with_widget<T>(&mut self, visit: impl FnOnce(
&mut dyn Widget<Action, Theme, Recorder>, &mut core_widget::Tree,
Layout<'_>, &mut Recorder, &Rectangle,
) -> T) -> T {
let snapshot = self.publication.map(|publication| Snapshot {
publication, source: self.source.clone(), path: &self.path, diagnostics: &self.diagnostics,
});
let editor = iced::widget::text_editor(&self.content)
.id(EDITOR_ID).on_action(|action| action).font(Font::MONOSPACE).size(14)
.padding(PAD).width(self.width).height(self.height)
.highlight_with::<crate::highlight::ArcadiaHighlighter>(self.source.clone(), crate::highlight::format);
let decorated = Decorations::new(editor, &self.content, snapshot, Padding::new(PAD), self.hover_enabled);
let mut view: Element<'_, Action, Theme, Recorder> =
crate::source_navigation::interaction::observe(decorated.into(), self.clock.clone());
self.tree.diff(view.as_widget());
let bounds = Rectangle::new(ORIGIN, Size::new(self.width, self.height));
let viewport = self.viewport.unwrap_or(bounds);
let mut recorder = Recorder::new(viewport);
let node = view.as_widget_mut().layout(&mut self.tree, &recorder,
&layout::Limits::new(Size::ZERO, bounds.size())).move_to(ORIGIN);
assert_eq!(Layout::new(&node).bounds(), bounds);
visit(view.as_widget_mut(), &mut self.tree, Layout::new(&node), &mut recorder, &viewport)
}
fn draw(&mut self, cursor: mouse::Cursor) -> Frame {
self.with_widget(|widget, tree, layout, recorder, viewport| {
widget.draw(tree, recorder, &Theme::Light, &renderer::Style::default(), layout, cursor, viewport);
assert_eq!(recorder.clips.len(), 1, "balanced renderer layers");
assert_eq!(recorder.transforms.len(), 1, "balanced renderer transformations");
std::mem::take(&mut recorder.frame)
})
}
fn event(&mut self, event: &Event, cursor: mouse::Cursor) -> (Vec<Action>, bool, window::RedrawRequest) {
self.with_widget(|widget, tree, layout, renderer, viewport| {
let mut messages = Vec::new();
let (captured, redraw) = {
let mut shell = Shell::new(&mut messages);
widget.update(tree, event, layout, cursor, renderer, &mut iced::advanced::clipboard::Null, &mut shell, viewport);
(shell.is_event_captured(), shell.redraw_request())
};
(messages, captured, redraw)
})
}
fn inspect_focus(&mut self, set: Option<bool>) -> bool {
struct Focus { set: Option<bool>, found: Option<bool> }
impl core_widget::Operation for Focus {
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn core_widget::Operation)) { visit(self); }
fn focusable(&mut self, id: Option<&core_widget::Id>, _: Rectangle, state: &mut dyn core_widget::operation::Focusable) {
if id == Some(&core_widget::Id::new(EDITOR_ID)) {
assert!(self.found.is_none(), "duplicate editor ID");
if let Some(focus) = self.set {
if focus { state.focus(); } else { state.unfocus(); }
}
self.found = Some(state.is_focused());
}
}
}
let mut operation = Focus { set, found: None };
self.with_widget(|widget, tree, layout, renderer, _| widget.operate(tree, layout, renderer, &mut operation));
operation.found.expect("actual editor is reachable by ID through all wrappers")
}
fn state(&mut self) -> ContentState {
let focused = self.inspect_focus(None);
ContentState {
text: self.content.text(), selected: self.content.selection(), cursor: self.content.cursor(),
scroll: self.content.with_editor(|e| (e.buffer().scroll().line, e.buffer().scroll().vertical)),
focused, clock: self.clock.load(Ordering::SeqCst),
}
}
fn gesture(&mut self, point: Point, clicks: usize) {
let messages = self.with_widget(|widget, tree, layout, renderer, viewport| {
let mut messages = Vec::new();
for _ in 0..clicks {
for event in [Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))] {
let mut shell = Shell::new(&mut messages);
widget.update(tree, &event, layout, mouse::Cursor::Available(point), renderer,
&mut iced::advanced::clipboard::Null, &mut shell, viewport);
}
}
messages
});
assert_eq!(messages.len(), clicks);
assert!(matches!(messages[0], Action::Click(_)));
assert!(matches!(messages[1], Action::SelectWord));
if clicks == 3 { assert!(matches!(messages[2], Action::SelectLine)); }
for message in messages { self.content.perform(message); }
assert!(self.inspect_focus(None));
}
fn type_x(&mut self) {
let event = Event::Keyboard(keyboard::Event::KeyPressed {
key: keyboard::Key::Character("x".into()), modified_key: keyboard::Key::Character("x".into()),
physical_key: keyboard::key::Physical::Code(keyboard::key::Code::KeyX),
location: keyboard::Location::Standard, modifiers: keyboard::Modifiers::empty(),
text: Some("X".into()), repeat: false,
});
let (messages, captured, _) = self.event(&event, mouse::Cursor::Unavailable);
assert!(captured);
assert_eq!(messages.len(), 1);
assert!(matches!(messages[0], Action::Edit(text_editor::Edit::Insert('X'))));
for action in messages { self.content.perform(action); }
}
}
#[derive(Debug, PartialEq)]
struct ContentState {
text: String,
selected: Option<String>,
cursor: text_editor::Cursor,
scroll: (usize, f32),
focused: bool,
clock: u64,
}
fn passive(mounted: &mut Mounted, point: Option<Point>) -> Frame {
let before = mounted.state();
let (event, cursor) = match point {
Some(point) => (Event::Mouse(mouse::Event::CursorMoved { position: point }), mouse::Cursor::Available(point)),
None => (Event::Mouse(mouse::Event::CursorLeft), mouse::Cursor::Unavailable),
};
let (messages, captured, redraw) = mounted.event(&event, cursor);
assert!(messages.is_empty(), "hover must not synthesize editor actions");
assert!(!captured, "hover must not consume ordinary pointer movement");
assert_eq!(redraw, window::RedrawRequest::NextFrame);
let frame = mounted.draw(cursor);
assert_eq!(mounted.state(), before, "hover must preserve exact content, hidden selection, focus, scroll and raw clock");
frame
}
#[test]
fn pointer_motion_between_bands_blank_and_leave_is_passive_and_always_redraws() {
let mut mounted = Mounted::new(&ROW.repeat(24));
mounted.diagnostics = (0..24).flat_map(|line| {
let start = line * ROW.len();
[diagnostic("a", start..start + 5, "A", Severity::Warning), diagnostic("a", start + 6..start + 10, "B", Severity::Error)]
}).collect();
assert!(mounted.inspect_focus(Some(true)));
mounted.draw(mouse::Cursor::Unavailable);
mounted.content.perform(Action::Scroll { lines: 2 });
let frame = mounted.draw(mouse::Cursor::Unavailable);
let line = frame.editor().glyphs.iter().find(|g| g.bytes.contains(&2) && inside(g.bounds, frame.editor().clip)).unwrap().line;
assert!(mounted.state().scroll != (0, 0.));
let a = frame.point(line, 2);
let b = frame.point(line, 7);
let clip = frame.editor().clip;
let blank = Point::new(clip.x + clip.width - 3., a.y);
let frame_a = passive(&mut mounted, Some(a));
assert!(frame_a.tooltip().contains("Detail A 雨"));
assert!(!frame_a.tooltip().contains("Detail B 雨"));
let frame_b = passive(&mut mounted, Some(b));
assert!(frame_b.tooltip().contains("Detail B 雨"));
assert!(!frame_b.tooltip().contains("Detail A 雨"));
assert!(passive(&mut mounted, Some(blank)).paragraphs.is_empty());
assert!(passive(&mut mounted, None).paragraphs.is_empty());
// Unfocused motion also requests repaint, without depending on caret blink.
assert!(!mounted.inspect_focus(Some(false)));
assert!(passive(&mut mounted, Some(a)).tooltip().contains("Detail A 雨"));
}
#[test]
fn actual_word_and_line_selection_then_typing_survive_hover_and_publication_changes() {
for clicks in [2, 3] {
let run = |hover: bool| {
let mut mounted = Mounted::new("alpha beta gamma\r\nsecond line 雨\r\n");
let frame = mounted.draw(mouse::Cursor::Unavailable);
let point = frame.point(0, 2);
mounted.gesture(point, clicks);
let selected = if clicks == 2 { "alpha" } else { "alpha beta gamma" };
assert_eq!(mounted.content.selection().as_deref(), Some(selected));
mounted.draw(mouse::Cursor::Unavailable);
let before = mounted.state();
if hover {
passive(&mut mounted, Some(point));
mounted.publication = None;
assert!(mounted.draw(mouse::Cursor::Available(point)).paragraphs.is_empty());
mounted.publication = Some(Id::new_v4());
assert!(mounted.draw(mouse::Cursor::Available(point)).tooltip().contains("Detail A"));
passive(&mut mounted, None);
assert_eq!(mounted.state(), before);
}
mounted.type_x();
mounted.content.text()
};
let control = run(false);
let hovered = run(true);
let expected = if clicks == 2 { "X beta gamma\r\nsecond line 雨\r\n" } else { "X\r\nsecond line 雨\r\n" };
assert_eq!(control, expected, "actual direct-typing control");
assert_eq!(hovered, expected, "hover must preserve the effective selected bytes, not only raw cursor endpoints");
}
}
#[test]
fn stationary_pointer_replacement_absence_and_path_switch_use_only_current_index() {
let mut mounted = Mounted::new(ROW);
mounted.diagnostics = vec![diagnostic("a", 0..5, "A", Severity::Warning), diagnostic("b", 0..5, "OTHER_PATH", Severity::Error)];
assert!(mounted.inspect_focus(Some(true)));
let frame = mounted.draw(mouse::Cursor::Unavailable);
let alpha = frame.point(0, 2);
let beta = frame.point(0, 7);
let before = mounted.state();
assert!(mounted.draw(mouse::Cursor::Available(alpha)).tooltip().contains("Detail A"));
// Same publication and same source Arc; only the active logical path changes.
mounted.path = "b".into();
let other = mounted.draw(mouse::Cursor::Available(alpha));
assert!(other.tooltip().contains("OTHER_PATH"));
assert!(!other.tooltip().contains("Detail A"));
mounted.path = "a".into();
assert!(mounted.draw(mouse::Cursor::Available(alpha)).tooltip().contains("Detail A"));
// Same path/source Arc but a different publication moves the diagnostic.
mounted.publication = Some(Id::new_v4());
mounted.diagnostics = vec![diagnostic("a", 6..10, "NEW_BETA", Severity::Error)];
assert!(mounted.draw(mouse::Cursor::Available(alpha)).paragraphs.is_empty());
assert!(mounted.draw(mouse::Cursor::Available(beta)).tooltip().contains("NEW_BETA"));
mounted.publication = None;
let empty = mounted.draw(mouse::Cursor::Available(beta));
assert!(empty.paragraphs.is_empty());
assert_eq!(empty.decoration_quads().count(), 0);
mounted.publication = Some(Id::new_v4());
assert!(mounted.draw(mouse::Cursor::Available(beta)).tooltip().contains("NEW_BETA"));
assert_eq!(mounted.state(), before);
}
#[test]
fn disabled_hover_and_unavailable_cursor_hide_details_without_removing_current_marks() {
let mut mounted = Mounted::new(ROW);
let frame = mounted.draw(mouse::Cursor::Unavailable);
let point = frame.point(0, 2);
assert!(mounted.draw(mouse::Cursor::Available(point)).tooltip().contains("Detail A"));
let before = mounted.state();
mounted.hover_enabled = false;
let disabled = mounted.draw(mouse::Cursor::Available(point));
assert!(disabled.paragraphs.is_empty());
assert!(disabled.decoration_quads().count() > 0);
mounted.hover_enabled = true;
let unavailable = mounted.draw(mouse::Cursor::Unavailable);
assert!(unavailable.paragraphs.is_empty());
assert!(unavailable.decoration_quads().count() > 0);
assert_eq!(mounted.state(), before);
}
fn paragraph_complete(paint: &ParagraphPaint, clip: Rectangle) -> bool {
let lines: Vec<_> = paint.content.split('\n').collect();
let wanted: BTreeSet<_> = lines.iter().enumerate().flat_map(|(line, text)| {
text.char_indices().filter(|(_, c)| !c.is_whitespace()).map(move |(byte, _)| (line, byte))
}).collect();
let mut covered = BTreeSet::new();
for glyph in &paint.glyphs {
let Some(text) = lines.get(glyph.line) else { return false };
let Some(cluster) = text.get(glyph.bytes.clone()) else { return false };
if cluster.chars().all(char::is_whitespace) { continue; }
if !inside(glyph.bounds, clip) { return false; }
for (byte, character) in text.char_indices() {
if !character.is_whitespace() && glyph.bytes.contains(&byte) { covered.insert((glyph.line, byte)); }
}
}
!wanted.is_empty() && covered == wanted
}
#[test]
fn measured_footer_and_omitted_count_fit_actual_shaped_widths_and_ancestor_clip() {
for width in [100., 180., 520.] {
let mut mounted = Mounted::new(ROW);
mounted.width = width + PAD * 2.;
mounted.height = 432.;
mounted.diagnostics = (0..6).map(|n| {
let mut value = diagnostic("a", 0..5, &format!("OVERLAP_{n}"), if n == 0 { Severity::Warning } else { Severity::Error });
value.diagnostic.message = format!("Long 雨 message {}", "bounded details ".repeat(90));
value
}).collect();
let first = mounted.draw(mouse::Cursor::Unavailable);
let point = first.point(0, 2);
let frame = mounted.draw(mouse::Cursor::Available(point));
let footer = frame.paragraphs.iter().find(|p| p.content.contains("4 of 6 shown")).expect("overlap footer must exist at admitted test viewport");
assert!(footer.content.contains("2 more"));
assert!(footer.content.contains("Actions → Source problems"));
assert!(paragraph_complete(footer, footer.clip), "all shaped footer clusters must fit at width {width}: {footer:?}");
let body = frame.paragraphs.iter().find(|p| p.content.contains("OVERLAP_")).unwrap();
assert!(body.content.starts_with("Error [OVERLAP_1]"), "errors precede warning even when warning was published first");
assert!(!body.content.contains("OVERLAP_0"));
for quad in frame.decoration_quads() { assert!(inside(quad.bounds, quad.clip), "decoration outside ancestor layer: {quad:?}"); }
let warning = Background::Color(Theme::Light.palette().warning);
let error = Background::Color(Theme::Light.palette().danger);
let colors: Vec<_> = frame.decoration_quads().filter(|q| q.background == warning || q.background == error).map(|q| q.background).collect();
assert_eq!(colors.len(), 6);
assert_eq!(colors[0], warning, "warnings painted below overlapping errors");
assert!(colors[1..].iter().all(|color| *color == error));
if width == 100. {
// In-process rejected old budget: same actual shaped footer clipped
// to the old 26px allowance must fail our completeness evaluator.
assert!(footer.clip.height > 26.);
assert!(!paragraph_complete(footer, Rectangle { height: 26., ..footer.clip }));
}
// A genuinely narrower ancestor viewport, not a nominal child width.
let inset = if width > 100. { 9. } else { 0. };
mounted.viewport = Some(Rectangle::new(
Point::new(ORIGIN.x + PAD + inset, ORIGIN.y + PAD), Size::new(width - inset, 220.),
));
let clipped = mounted.draw(mouse::Cursor::Available(point));
for quad in clipped.decoration_quads() { assert!(inside(quad.bounds, quad.clip)); }
let footer = clipped.paragraphs.iter().find(|p| p.content.contains("4 of 6 shown")).expect("footer must remain usable at 220px effective height");
assert!(paragraph_complete(footer, footer.clip));
mounted.viewport = Some(Rectangle::new(
Point::new(ORIGIN.x + PAD, ORIGIN.y + PAD), Size::new(width, 60.),
));
assert!(mounted.draw(mouse::Cursor::Available(point)).paragraphs.is_empty(), "insufficient tooltip height is suppressed safely");
}
}