Something went wrong. Try again.
POC of simple editor based on Automerge
Something went wrong. Try again.
12 kB · 353 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354//! Pixel scrolling around the full-height editor, with a draggable scrollbar.use iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer, widget};use iced::{Element, Event, Length, Rectangle, Renderer, Size, Theme, Vector};
pub const SCROLL_ID: &str = "note-scroll";
pub fn viewport<'a, Message: 'a>( content: Element<'a, Message>, scale_factor: f32,) -> Element<'a, Message> { let scroll = iced::widget::scrollable(content) .id(SCROLL_ID) .height(Length::Fill) .width(Length::Fill) .direction(iced::widget::scrollable::Direction::Vertical( iced::widget::scrollable::Scrollbar::new() .width(10) .scroller_width(6) .margin(2), )) .spacing(2) .style(|theme, status| { let mut style = iced::widget::scrollable::default(theme, status); style.vertical_rail.background = None; style.vertical_rail.scroller.background = crate::flexoki::colors(theme).muted.into(); style.vertical_rail.scroller.border = iced::border::rounded(3); style }); Element::new(ScrollInput { inner: scroll.into(), scale_factor, })}
fn logical_scroll(event: &Event, scale: f32) -> Event { match event { Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Pixels { x, y }, }) => Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Pixels { x: x / scale, y: y / scale, }, }), _ => event.clone(), }}
struct ScrollInput<'a, Message> { inner: Element<'a, Message>, scale_factor: f32,}
impl<Message> Widget<Message, Theme, Renderer> for ScrollInput<'_, Message> { fn tag(&self) -> widget::tree::Tag { self.inner.as_widget().tag() } fn state(&self) -> widget::tree::State { self.inner.as_widget().state() } fn children(&self) -> Vec<widget::Tree> { self.inner.as_widget().children() } fn diff(&self, tree: &mut widget::Tree) { self.inner.as_widget().diff(tree); } fn size(&self) -> Size<Length> { self.inner.as_widget().size() } fn layout( &mut self, tree: &mut widget::Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.inner.as_widget_mut().layout(tree, renderer, limits) } fn update( &mut self, tree: &mut widget::Tree, event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { self.inner.as_widget_mut().update( tree, &logical_scroll(event, self.scale_factor), layout, cursor, renderer, clipboard, shell, viewport, ); } fn operate( &mut self, tree: &mut widget::Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn widget::Operation, ) { self.inner .as_widget_mut() .operate(tree, layout, renderer, operation); } fn mouse_interaction( &self, tree: &widget::Tree, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { self.inner .as_widget() .mouse_interaction(tree, layout, cursor, viewport, renderer) } fn draw( &self, tree: &widget::Tree, renderer: &mut Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, ) { self.inner .as_widget() .draw(tree, renderer, theme, style, layout, cursor, viewport); }}
// Query after layout so wrapping and newly inserted lines have current geometry.pub fn reveal_caret<Message: Send + 'static>() -> iced::Task<Message> { widget::operate(RevealCaret::default())}
#[derive(Default)]struct RevealCaret { viewport: Option<(Rectangle, Vector)>, caret: Option<Rectangle>,}
impl<T: 'static> widget::Operation<T> for RevealCaret { fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation<T>)) { visit(self); } fn scrollable( &mut self, id: Option<&widget::Id>, bounds: Rectangle, _: Rectangle, translation: Vector, _: &mut dyn widget::operation::Scrollable, ) { if id == Some(&widget::Id::new(SCROLL_ID)) { self.viewport = Some((bounds, translation)); } } fn custom( &mut self, id: Option<&widget::Id>, bounds: Rectangle, state: &mut dyn std::any::Any, ) { if id == Some(&widget::Id::new("editor-caret")) { if let Some(measurement) = state.downcast_mut::<iced::advanced::graphics::text::Editor>() { self.caret = crate::collaborative_editor::reveal_bounds(measurement, bounds); } } } fn finish(&self) -> widget::operation::Outcome<T> { let (Some((viewport, translation)), Some(caret)) = (self.viewport, self.caret) else { return widget::operation::Outcome::None; }; let top = caret.y - viewport.y; let bottom = top + caret.height; let offset = if top < translation.y { top } else if bottom > translation.y + viewport.height { bottom - viewport.height } else { return widget::operation::Outcome::None; }; widget::operation::Outcome::Chain(Box::new(widget::operation::scrollable::scroll_to( widget::Id::new(SCROLL_ID), widget::operation::scrollable::AbsoluteOffset { x: None, y: Some(offset.max(0.0)), }, ))) }}
#[cfg(test)]mod tests { use super::*; use crate::collaborative_editor::{self, EditorContent, PADDING, TEXT_SIZE}; use iced::advanced::widget::Operation; use iced::widget::text_editor; use iced::{Font, Point};
#[derive(Default)] struct Position { offset: f32, overflow: f32, } impl widget::Operation for Position { fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) { visit(self); } fn scrollable( &mut self, id: Option<&widget::Id>, bounds: Rectangle, content: Rectangle, translation: Vector, _: &mut dyn widget::operation::Scrollable, ) { if id == Some(&widget::Id::new(SCROLL_ID)) { self.offset = translation.y; self.overflow = content.height - bounds.height; } } }
#[test] fn slow_trackpad_and_scrollbar_move_the_actual_viewport() { for scale in [1.0, 1.5, 2.0] { let content = EditorContent::with_text(&"A line of text\n".repeat(100)); let editor = text_editor(content.native()) .font(Font::MONOSPACE) .size(TEXT_SIZE) .padding(PADDING) .height(Length::Shrink) .on_action(|action| action); let mut view = viewport( collaborative_editor::with_cursors(editor, &content, vec![]), scale, ); let mut tree = widget::Tree::new(view.as_widget()); let renderer = Renderer::Secondary(iced_tiny_skia::Renderer::new( Font::MONOSPACE, TEXT_SIZE.into(), )); let size = Size::new(400.0, 200.0); let node = view.as_widget_mut() .layout(&mut tree, &renderer, &layout::Limits::new(size, size)); let bounds = Rectangle::with_size(size); let mut messages = Vec::new(); let mut send = |event: Event, point: Point| { view.as_widget_mut().update( &mut tree, &event, Layout::new(&node), mouse::Cursor::Available(point), &renderer, &mut iced::advanced::clipboard::Null, &mut Shell::new(&mut messages), &bounds, ); }; // Each event is only a quarter of a logical pixel, far below one text line. for _ in 0..4 { send( Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Pixels { x: 0.0, y: -0.25 * scale, }, }), Point::new(50.0, 50.0), ); } let mut position = Position::default(); view.as_widget_mut() .operate(&mut tree, Layout::new(&node), &renderer, &mut position); assert!( (position.offset - 1.0).abs() < 0.001, "scale {scale}: offset {}", position.offset ); assert!(position.overflow > 1000.0, "full document must be laid out"); assert!( messages.is_empty(), "wheel input must not become whole-line editor actions" );
// Drag the actual scrollbar thumb down the rail. for (event, point) in [ ( mouse::Event::ButtonPressed(mouse::Button::Left), Point::new(394.0, 4.0), ), ( mouse::Event::CursorMoved { position: Point::new(394.0, 100.0), }, Point::new(394.0, 100.0), ), ( mouse::Event::ButtonReleased(mouse::Button::Left), Point::new(394.0, 100.0), ), ] { view.as_widget_mut().update( &mut tree, &Event::Mouse(event), Layout::new(&node), mouse::Cursor::Available(point), &renderer, &mut iced::advanced::clipboard::Null, &mut Shell::new(&mut messages), &bounds, ); } view.as_widget_mut() .operate(&mut tree, Layout::new(&node), &renderer, &mut position); assert!( position.offset > 500.0, "dragging the scrollbar should move the document" );
// Revealing the caret at the start returns the viewport to the top. let mut reveal = RevealCaret::default(); view.as_widget_mut() .operate(&mut tree, Layout::new(&node), &renderer, &mut reveal); let widget::operation::Outcome::Chain(mut operation) = <RevealCaret as Operation>::finish(&reveal) else { panic!("offscreen caret should trigger scrolling"); }; view.as_widget_mut().operate( &mut tree, Layout::new(&node), &renderer, operation.as_mut(), ); view.as_widget_mut() .operate(&mut tree, Layout::new(&node), &renderer, &mut position); assert!(position.offset <= PADDING); } }}