//! 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, } #[derive(Debug, PartialEq)] struct Geometry { scroll_line: usize, scroll_vertical: f32, runs: Vec, } 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 } 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) {} fn allocate_image( &mut self, handle: &iced::advanced::image::Handle, callback: impl FnOnce(Result) + 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, tree: &mut Option, 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::::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."); }