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.
166 lines
8.0 KiB
Rust
166 lines
8.0 KiB
Rust
//! Foundational CPU geometry seam only. No window/GPU/diagnostic algorithm.
|
|
use iced::advanced::{Layout, graphics, layout, renderer, text as shaped, widget};
|
|
use iced::widget::text_editor::{self, Action, Cursor, Position};
|
|
use iced::{Background, Color, Element, Font, Pixels, Point, Rectangle, Size, Transformation};
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
struct Glyph {
|
|
start: usize,
|
|
end: usize,
|
|
x: f32,
|
|
y: f32,
|
|
width: f32,
|
|
level: u8,
|
|
}
|
|
#[derive(Debug, PartialEq)]
|
|
struct Run {
|
|
line: usize,
|
|
text: String,
|
|
top: f32,
|
|
baseline: f32,
|
|
height: f32,
|
|
glyphs: Vec<Glyph>,
|
|
}
|
|
#[derive(Debug, PartialEq)]
|
|
struct Geometry {
|
|
scroll_line: usize,
|
|
scroll_vertical: f32,
|
|
runs: Vec<Run>,
|
|
}
|
|
fn geometry(editor: &graphics::text::Editor) -> Geometry {
|
|
let buffer = editor.buffer();
|
|
Geometry {
|
|
scroll_line: buffer.scroll().line,
|
|
scroll_vertical: buffer.scroll().vertical,
|
|
runs: buffer.layout_runs().map(|r| Run {
|
|
line: r.line_i,
|
|
text: r.text.to_owned(),
|
|
top: r.line_top,
|
|
baseline: r.line_y,
|
|
height: r.line_height,
|
|
glyphs: r.glyphs.iter().map(|g| Glyph {
|
|
start: g.start, end: g.end, x: g.x, y: g.y, width: g.w,
|
|
level: g.level.number(),
|
|
}).collect(),
|
|
}).collect(),
|
|
}
|
|
}
|
|
#[derive(Debug)]
|
|
struct Paint {
|
|
editor_address: usize,
|
|
geometry: Geometry,
|
|
origin: Point,
|
|
clip: Rectangle,
|
|
}
|
|
#[derive(Default)]
|
|
struct Recorder { paints: Vec<Paint> }
|
|
impl renderer::Renderer for Recorder {
|
|
fn start_layer(&mut self, _: Rectangle) {}
|
|
fn end_layer(&mut self) {}
|
|
fn start_transformation(&mut self, _: Transformation) {}
|
|
fn end_transformation(&mut self) {}
|
|
fn reset(&mut self, _: Rectangle) { self.paints.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 (), handle, callback); }
|
|
}
|
|
impl shaped::Renderer for Recorder {
|
|
type Font = Font;
|
|
type Paragraph = graphics::text::Paragraph;
|
|
type Editor = graphics::text::Editor;
|
|
const ICON_FONT: Font = Font::DEFAULT;
|
|
const CHECKMARK_ICON: char = '0';
|
|
const ARROW_DOWN_ICON: char = '0';
|
|
const SCROLL_UP_ICON: char = '0';
|
|
const SCROLL_DOWN_ICON: char = '0';
|
|
const SCROLL_LEFT_ICON: char = '0';
|
|
const SCROLL_RIGHT_ICON: char = '0';
|
|
const ICED_LOGO: char = '0';
|
|
fn default_font(&self) -> Font { Font::DEFAULT }
|
|
fn default_size(&self) -> Pixels { Pixels(16.) }
|
|
fn fill_paragraph(&mut self, _: &Self::Paragraph, _: Point, _: Color, _: Rectangle) {}
|
|
fn fill_editor(&mut self, editor: &Self::Editor, origin: Point, _: Color, clip: Rectangle) {
|
|
self.paints.push(Paint { editor_address: editor as *const _ as usize, geometry: geometry(editor), origin, clip });
|
|
}
|
|
fn fill_text(&mut self, _: shaped::Text, _: Point, _: Color, _: Rectangle) {}
|
|
}
|
|
const ORIGIN: Point = Point::new(23., 31.);
|
|
const PAD: f32 = 13.;
|
|
fn draw(content: &text_editor::Content<Recorder>, tree: &mut Option<widget::Tree>, width: f32, label: &str) -> Geometry {
|
|
let height = 170.;
|
|
let mut renderer = Recorder::default();
|
|
let mut element: Element<'_, Action, iced::Theme, Recorder> = iced::widget::text_editor(content)
|
|
.on_action(|action| action)
|
|
.font(Font::MONOSPACE).size(16).padding(PAD)
|
|
.wrapping(shaped::Wrapping::WordOrGlyph).width(width).height(height).into();
|
|
let state = tree.get_or_insert_with(|| widget::Tree::new(element.as_widget()));
|
|
state.diff(element.as_widget());
|
|
let node = element.as_widget_mut().layout(state, &renderer, &layout::Limits::new(Size::ZERO, Size::new(width, height))).move_to(ORIGIN);
|
|
let bounds = Layout::new(&node).bounds();
|
|
assert_eq!(bounds, Rectangle::new(ORIGIN, Size::new(width, height)));
|
|
element.as_widget().draw(state, &mut renderer, &iced::Theme::Light, &renderer::Style::default(), Layout::new(&node), iced::mouse::Cursor::Unavailable, &bounds);
|
|
assert_eq!(renderer.paints.len(), 1, "exactly one real fill_editor call");
|
|
let paint = renderer.paints.pop().unwrap();
|
|
let (address, actual) = content.with_editor(|e| (e as *const _ as usize, geometry(e)));
|
|
assert_eq!(address, paint.editor_address, "accessor must borrow the exact painted editor, not a copy");
|
|
assert_eq!(actual, paint.geometry, "post-draw accessor must match all actual painted run/cluster geometry");
|
|
let inner = bounds.shrink(iced::Padding::new(PAD));
|
|
assert_eq!(paint.origin, inner.position());
|
|
assert_eq!(paint.clip, inner);
|
|
assert!(!actual.runs.is_empty());
|
|
for run in &actual.runs {
|
|
for glyph in &run.glyphs {
|
|
assert!(glyph.start <= glyph.end && run.text.is_char_boundary(glyph.start) && run.text.is_char_boundary(glyph.end));
|
|
}
|
|
}
|
|
println!("FRAME {label} origin={:?} clip={:?} geometry={actual:?}", paint.origin, paint.clip);
|
|
actual
|
|
}
|
|
fn main() {
|
|
let source = concat!(
|
|
"alpha alpha 救援 e\u{301} العربية many words to force actual wrapping across several visual lines at narrow width.\r\n",
|
|
"second line repeated alias alpha 救援 more words for wrapping.\n",
|
|
"third line alpha and final glyph Ω\r\n",
|
|
"fourth line with several words and Unicode 救援\n",
|
|
"fifth line tail\n",
|
|
"sixth line end\n",
|
|
);
|
|
let mut content = text_editor::Content::<Recorder>::with_text(source);
|
|
let mut tree = None;
|
|
let narrow = draw(&content, &mut tree, 260., "narrow-original");
|
|
assert!(narrow.runs.windows(2).any(|r| r[0].line == r[1].line), "fixture must really wrap one source line");
|
|
assert!(narrow.runs.iter().flat_map(|r| &r.glyphs).any(|g| g.end - g.start > 1), "fixture must expose actual multibyte clusters");
|
|
let wider = draw(&content, &mut tree, 560., "wider-original");
|
|
let count_line0 = |g: &Geometry| g.runs.iter().filter(|r| r.line == 0).count();
|
|
assert!(count_line0(&narrow) > count_line0(&wider), "different actual width must change wrapping");
|
|
for (label, action, selected) in [("word", Action::SelectWord, "alpha"), ("line", Action::SelectLine, source.split('\r').next().unwrap())] {
|
|
content.perform(Action::Move(text_editor::Motion::DocumentStart));
|
|
content.move_to(Cursor { position: Position { line: 0, column: 2 }, selection: None });
|
|
content.perform(action);
|
|
assert_eq!(content.selection().as_deref(), Some(selected));
|
|
let cursor = content.cursor();
|
|
let selection = content.selection();
|
|
let before = content.text();
|
|
let _ = draw(&content, &mut tree, 260., label);
|
|
assert_eq!(content.cursor(), cursor);
|
|
assert_eq!(content.selection(), selection);
|
|
assert_eq!(content.text(), before);
|
|
let snapshot = content.with_editor(geometry);
|
|
for _ in 0..3 { assert_eq!(content.with_editor(geometry), snapshot); }
|
|
assert_eq!(content.selection(), selection, "read-only geometry must preserve Word/Line selection");
|
|
content.perform(Action::Scroll { lines: 2 });
|
|
let scroll_before = content.with_editor(|e| (e.buffer().scroll().line, e.buffer().scroll().vertical));
|
|
let after = draw(&content, &mut tree, 260., &format!("{label}-scrolled"));
|
|
assert_eq!((after.scroll_line, after.scroll_vertical), scroll_before, "geometry/layout/draw must not reset scroll");
|
|
assert!(after.scroll_line > 0 || after.scroll_vertical > 0., "fixture must really scroll");
|
|
assert_ne!(after, snapshot, "actual visible geometry changes on scroll");
|
|
assert_eq!(content.cursor(), cursor);
|
|
assert_eq!(content.selection(), selection);
|
|
assert_eq!(content.text(), source);
|
|
}
|
|
println!("PASS actual painted editor identity/geometry/origin/clip; wrap change; Unicode clusters; retained Tree; Word/Line selection/text/scroll preserved. CPU only, no GPU/native/tooltip claim.");
|
|
}
|