Compose compact Text workspace with guarded root navigation

This commit is contained in:
2026-09-13 04:10:27 -07:00
parent feab2020d8
commit 74542ae2c1
20 changed files with 5482 additions and 738 deletions
@@ -0,0 +1,248 @@
//! Shapes and draws the production summary, checking actual paragraph advances.
//! Native validation remains necessary for platform raster ink/fallback fonts.
use super::*;
use crate::source_navigation::query;
use crate::views::source_tests::FontLayout;
use iced::advanced::{Layout, layout, renderer, text as shaped, widget::Tree};
use iced::{Background, Color, Pixels, Point, Rectangle, Size, Transformation};
use std::sync::Arc;
struct Cluster {
paragraph: String,
bytes: std::ops::Range<usize>,
bounds: Rectangle,
clip: Rectangle,
}
struct Recorder {
clips: Vec<Rectangle>,
transforms: Vec<Transformation>,
clusters: Vec<Cluster>,
}
impl Recorder {
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))
}
}
impl renderer::Renderer for Recorder {
fn start_layer(&mut self, bounds: Rectangle) {
self.clips.push(self.clip(bounds));
}
fn end_layer(&mut self) {
self.clips.pop().unwrap();
}
fn start_transformation(&mut self, transform: Transformation) {
self.transforms.push(self.transform() * transform);
}
fn end_transformation(&mut self) {
self.transforms.pop().unwrap();
}
fn reset(&mut self, bounds: Rectangle) {
self.clips = vec![bounds];
self.transforms = vec![Transformation::IDENTITY];
self.clusters.clear();
}
fn fill_quad(&mut self, _: renderer::Quad, _: impl Into<Background>) {}
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 = <FontLayout as shaped::Renderer>::Paragraph;
type Editor = <FontLayout as shaped::Renderer>::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,
position: Point,
_: Color,
clip: Rectangle,
) {
let buffer = paragraph.buffer();
let content = buffer
.lines
.iter()
.map(|line| line.text())
.collect::<Vec<_>>()
.join("\n");
let clip = self.clip(clip);
for run in buffer.layout_runs() {
for glyph in run.glyphs {
self.clusters.push(Cluster {
paragraph: content.clone(),
bytes: glyph.start..glyph.end,
bounds: Rectangle {
x: position.x + glyph.x + glyph.x_offset * glyph.font_size,
y: position.y + run.line_top,
width: glyph.w,
height: run.line_height,
} * self.transform(),
clip,
});
}
}
}
fn fill_editor(&mut self, _: &Self::Editor, _: Point, _: Color, _: Rectangle) {}
fn fill_text(&mut self, _: shaped::Text, _: Point, _: Color, _: Rectangle) {}
}
const FLAGS: [&str; 5] = ["Stale", "Partial", "Collision", "Detached", "Unverified"];
fn draw_and_check_flags(
content: Element<'_, Message, iced::Theme, Recorder>,
width: f32,
) -> Vec<String> {
let viewport = Rectangle::with_size(Size::new(width, 24.));
let mut renderer = Recorder {
clips: vec![viewport],
transforms: vec![Transformation::IDENTITY],
clusters: Vec::new(),
};
let mut view: Element<'_, Message, iced::Theme, Recorder> =
container(content).width(Fill).height(24).clip(true).into();
let mut tree = Tree::new(view.as_widget());
let node = view.as_widget_mut().layout(
&mut tree,
&renderer,
&layout::Limits::new(Size::ZERO, viewport.size()),
);
assert_eq!(node.size(), viewport.size());
view.as_widget().draw(
&tree,
&mut renderer,
&iced::Theme::Light,
&renderer::Style::default(),
Layout::new(&node),
iced::mouse::Cursor::Unavailable,
&viewport,
);
let mut failures = Vec::new();
for flag in FLAGS {
for byte in 0..flag.len() {
let matching: Vec<_> = renderer
.clusters
.iter()
.filter(|cluster| {
cluster
.paragraph
.find(flag)
.is_some_and(|start| cluster.bytes.contains(&(start + byte)))
})
.collect();
if matching.is_empty() {
failures.push(format!("missing shaped flag byte: {flag}[{byte}]"));
}
for cluster in matching {
let b = cluster.bounds;
let c = cluster.clip;
if !(b.x >= c.x - 0.5
&& b.y >= c.y - 0.5
&& b.x + b.width <= c.x + c.width + 0.5
&& b.y + b.height <= c.y + c.height + 0.5)
{
failures.push(format!("flag {flag} is clipped: glyph {b:?}, clip {c:?}"));
}
}
}
}
failures
}
fn old_summary(value: String) -> Element<'static, Message, iced::Theme, Recorder> {
text(value)
.size(12)
.wrapping(widget::text::Wrapping::None)
.into()
}
#[test]
fn all_reference_exceptions_fit_allocated_width_with_wide_alias_and_large_counts() {
let mut app = App::with_project(oa_core::Project::empty());
let old_key = App::with_project(oa_core::Project::empty()).source_key();
assert_ne!(old_key, app.source_key());
for alias in ["雨".repeat(32), "W".repeat(32)] {
// Unicode aliases are presentation stress: the current lexer admits
// ASCII identifiers only. Six-digit totals are within the query's
// 250,000-record cap; this does not build that many source records.
let old_value = format!(
"@{alias} · 100000 declarations · 100000 uses · Stale · Partial · Collision · Detached · Unverified"
);
app.source_symbols.results = Some(Arc::new(query::Results {
stamp: query::QueryStamp {
snapshot: old_key,
serial: 1,
},
action: query::Action::References,
origin_path: app.source_workspace.active.clone(),
origin_offset: 0,
origin: None,
alias: Some(alias.clone()),
scope: query::Scope {
closure_complete: false,
applied: false,
validated: false,
included_files: 28,
detached_files: 1,
unverified_files: 1,
issues: Vec::new(),
},
disposition: query::Disposition::LexicalReferences,
definitions: Vec::new(),
references: Vec::new(),
total_definitions: 100_000,
total_references: 100_000,
omitted_definitions: 100_000,
omitted_references: 100_000,
}));
for width in [840., 720.] {
let positive = draw_and_check_flags(reference_summary(&app), width);
assert!(
positive.is_empty(),
"reserved flags, width {width}, alias {alias:?}: {positive:?}"
);
// Observe the exact old presentation without predicting font widths.
// This is emitted under --nocapture, whether it clips or fits.
let old = draw_and_check_flags(old_summary(old_value.clone()), width);
println!(
"old alias-first summary width={width} alias={alias:?}: {} missing/clipped flag-byte checks; first={:?}",
old.len(),
old.first()
);
}
}
// Deliberate evaluator stress, beyond the production 32-character subject
// bound: require the same evaluator to reject a clipped old-style paragraph.
// This is not evidence of a production/native overflow at 840 pixels.
let stress = format!("@{} · {}", "W".repeat(256), FLAGS.join(" · "));
let rejected = draw_and_check_flags(old_summary(stress), 720.);
assert!(
!rejected.is_empty(),
"negative control must detect missing/clipped warning text"
);
}