1use std::cell::RefCell;
2use std::rc::Rc;
3use std::time::Duration;
4
5use blockworx_doc::{block_model::Asset, hash::AssetHash};
6use blockworx_geom::{Rect, WorldPx};
7use blockworx_paint::edit::{EDITOR_BORDER, EDITOR_PAD, EDITOR_ROUNDING};
8use blockworx_paint::ground::{GridLine, horizontals, verticals};
9use blockworx_paint::{
10 Button, Color, Cursor, DrawOp, EditColors, EditField, Factor, Input, Keys, Move, Raw, ScrollPx,
11 TextEvent, Tick, Vantage,
12};
13use egui::{PointerButton, Sense, Stroke};
14
15use crate::convert::{IntoEgui as _, IntoGeom};
16use crate::image::ImageRegistry;
17use crate::replay::replay;
18
19pub use blockworx_paint::Camera;
20pub use blockworx_paint::Ground as CanvasChrome;
23
24fn grid_stroke(line: GridLine, color: Color) -> Stroke {
25 Stroke::new(line.width(), color.egui())
26}
27
28fn pan_key_held(ui: &egui::Ui) -> bool {
32 !ui.ctx().egui_wants_keyboard_input() && ui.input(|i| i.key_down(egui::Key::Space))
33}
34
35pub struct View {
40 draft: Option<Draft>,
44 hovered: bool,
48 primary_drag_pans: bool,
52 touch_pans: bool,
57 camera: Camera,
60 images: Rc<RefCell<ImageRegistry>>,
63}
64
65pub fn tick(ctx: &egui::Context) -> Tick {
68 let (now, predicted_dt) = ctx.input(|i| (i.time, i.predicted_dt));
69 Tick::predicting(
72 Duration::try_from_secs_f64(now).unwrap_or_default(),
73 Duration::try_from_secs_f32(predicted_dt).unwrap_or_default(),
74 )
75}
76
77impl Default for View {
78 fn default() -> Self {
79 Self {
80 draft: None,
81 hovered: false,
82 primary_drag_pans: false,
83 touch_pans: false,
84 camera: Camera::Settled,
85 images: Rc::new(RefCell::new(ImageRegistry::default())),
86 }
87 }
88}
89
90impl View {
91 pub fn register_image(&self, ctx: &egui::Context, hash: AssetHash, asset: &Asset) {
94 self.images.borrow_mut().register(ctx, hash, asset);
95 }
96
97 #[cfg(any(test, feature = "test-support"))]
100 pub fn holds_image(&self, hash: AssetHash) -> bool {
101 self.images.borrow().uri(hash).is_some()
102 }
103
104 pub fn canvas_hovered(&self) -> bool {
108 self.hovered
109 }
110
111 pub fn camera(&self) -> Camera {
114 self.camera
115 }
116
117 pub fn focused_edit_id(&self) -> Option<egui::Id> {
120 self.draft.as_ref().map(|draft| draft.id)
121 }
122
123 pub fn begin(&mut self, ui: &mut egui::Ui) -> Canvas {
129 let (rect, response) = ui.allocate_exact_size(ui.available_size(), Sense::click_and_drag());
130 let diagram = ui.painter().add(egui::Shape::Noop);
131 let rect = rect.geom();
132 self.hovered = response.hovered();
133
134 let mut moves = Vec::new();
135 let pan = self.handle_pan_zoom(ui, &response, rect, &mut moves);
136 self.camera = pan;
137
138 Canvas {
139 rect,
140 response,
141 diagram,
142 pan,
143 moves,
144 }
145 }
146
147 pub fn show(&mut self, ui: &egui::Ui, canvas: &Canvas, diagram: Diagram<'_>) -> Option<Cursor> {
155 let painter = ui.painter().with_clip_rect(canvas.rect.egui());
156 let mut shapes = View::grid(diagram.vantage, canvas.rect, diagram.ground);
157 shapes.extend(replay(diagram.draw_list, &painter, &self.images.borrow()));
158 painter.set(canvas.diagram, egui::Shape::Vec(shapes));
159 if canvas.pan == Camera::Moving {
160 Some(Cursor::Grabbing)
161 } else if self.hovered && pan_key_held(ui) {
162 Some(Cursor::Grab)
163 } else {
164 diagram.cursor
165 }
166 }
167
168 fn handle_pan_zoom(
172 &mut self,
173 ui: &egui::Ui,
174 response: &egui::Response,
175 rect: Rect,
176 moves: &mut Vec<Move>,
177 ) -> Camera {
178 if let Some(pan) = self.handle_touch_gesture(ui, response, moves) {
179 return pan;
180 }
181 if response.hovered() {
185 let cursor = ui
186 .ctx()
187 .pointer_hover_pos()
188 .map_or(rect.center(), IntoGeom::geom);
189 let (scroll, pinch) = ui.input(|i| (i.smooth_scroll_delta.y, i.zoom_delta()));
195 let factor = Factor::of_scroll(ScrollPx::up(scroll)).get() * pinch;
196 if factor != 1.0 {
197 moves.push(Move::Zoom {
198 factor: Factor::new(factor),
199 anchor: cursor,
200 });
201 }
202 }
203
204 if response.drag_started() {
208 self.primary_drag_pans = pan_key_held(ui);
209 }
210 let primary_pan = self.primary_drag_pans
213 && (response.dragged_by(PointerButton::Primary) || response.drag_stopped());
214 if response.drag_stopped() {
215 self.primary_drag_pans = false;
216 }
217 if primary_pan
218 || response.dragged_by(PointerButton::Secondary)
219 || response.dragged_by(PointerButton::Middle)
220 {
221 moves.push(Move::Pan(response.drag_delta().geom()));
222 Camera::Moving
223 } else {
224 Camera::Settled
225 }
226 }
227
228 fn handle_touch_gesture(
236 &mut self,
237 ui: &egui::Ui,
238 response: &egui::Response,
239 moves: &mut Vec<Move>,
240 ) -> Option<Camera> {
241 let (gesture, touching) = ui.input(|i| (i.multi_touch(), i.any_touches()));
242 if !touching {
243 self.touch_pans = false;
244 return None;
245 }
246 let ours = self.touch_pans || response.hovered() || response.is_pointer_button_down_on();
249 let Some(gesture) = gesture.filter(|_| ours) else {
250 return self.touch_pans.then_some(Camera::Moving);
253 };
254 self.touch_pans = true;
255 moves.push(Move::Pan(gesture.translation_delta.geom()));
256 if gesture.zoom_delta != 1.0 {
257 moves.push(Move::Zoom {
258 factor: Factor::new(gesture.zoom_delta),
259 anchor: gesture.center_pos.geom(),
260 });
261 }
262 Some(Camera::Moving)
263 }
264
265 fn grid(vantage: Vantage, rect: Rect, chrome: CanvasChrome) -> Vec<egui::Shape> {
269 let down = verticals(vantage, rect).map(|(x, line)| {
270 egui::Shape::vline(x, rect.y_range().egui(), grid_stroke(line, chrome.grid))
271 });
272 let across = horizontals(vantage, rect).map(|(y, line)| {
273 egui::Shape::hline(rect.x_range().egui(), y, grid_stroke(line, chrome.grid))
274 });
275 std::iter::once(egui::Shape::rect_filled(
276 rect.egui(),
277 0.0,
278 chrome.background.egui(),
279 ))
280 .chain(down)
281 .chain(across)
282 .collect()
283 }
284
285 pub fn capture(
290 &mut self,
291 ui: &mut egui::Ui,
292 field: Option<&EditField>,
293 vantage: Vantage,
294 ) -> Option<TextEvent> {
295 let Some(edit) = field else {
296 self.draft = None;
297 return None;
298 };
299 let id = edit.id;
300 let edit_id = id.egui();
301 let (draft, opening) = match &mut self.draft {
302 Some(draft) if draft.id == edit_id => (draft, false),
303 slot => (
304 slot.insert(Draft {
305 id: edit_id,
306 text: edit.text.clone(),
307 }),
308 true,
309 ),
310 };
311 if opening {
312 let chars = draft.text.chars().count();
316 let range = if edit.tab_cycle || edit.select_all_on_focus {
317 egui::text::CCursorRange::two(
318 egui::text::CCursor::new(0),
319 egui::text::CCursor::new(chars),
320 )
321 } else {
322 egui::text::CCursorRange::one(egui::text::CCursor::new(chars))
323 };
324 let mut state =
325 egui::text_edit::TextEditState::load(ui.ctx(), edit_id).unwrap_or_default();
326 state.cursor.set_char_range(Some(range));
327 state.store(ui.ctx(), edit_id);
328 }
329 let screen_font = vantage.remap_font(&edit.font).egui();
330 let ink = edit.colors.text.egui();
331 let layout_font = screen_font.clone();
332 let wrap_px = edit.wrap_width.map(|w| w.get() * vantage.zoom.get());
333 let mut wrap_at = move |ui: &egui::Ui, buf: &dyn egui::TextBuffer, _avail: f32| {
334 let job = egui::text::LayoutJob::simple(
335 buf.as_str().to_owned(),
336 layout_font.clone(),
337 ink,
338 wrap_px.unwrap_or(f32::INFINITY),
339 );
340 ui.fonts_mut(|f| f.layout_job(job))
341 };
342 let mut widget = if edit.multiline {
343 egui::TextEdit::multiline(&mut draft.text)
344 } else {
345 egui::TextEdit::singleline(&mut draft.text)
346 }
347 .id(edit_id)
348 .desired_width(f32::INFINITY)
349 .font(screen_font)
350 .frame(editor_frame(&edit.colors, vantage))
351 .min_size(edit.rect.size().egui())
352 .desired_rows(1)
353 .horizontal_align(edit.align.x().egui())
354 .vertical_align(edit.align.y().egui())
355 .text_color(ink)
356 .layouter(&mut wrap_at);
357 if let Some(hint) = &edit.hint {
358 widget =
359 widget.hint_text(egui::RichText::new(hint.as_ref()).color(edit.colors.hint.egui()));
360 }
361 if let Some(limit) = edit.char_limit {
362 widget = widget.char_limit(limit);
363 }
364 let resp = ui
365 .scope(|ui| {
366 let visuals = ui.visuals_mut();
367 visuals.selection.bg_fill = edit.colors.selection.egui();
368 visuals.selection.stroke = egui::Stroke::NONE;
369 visuals.text_cursor.stroke.color = edit.colors.caret.egui();
370 ui.place(edit.rect.egui(), widget)
371 })
372 .inner;
373 ui.memory_mut(|m| {
380 m.set_focus_lock_filter(
381 edit_id,
382 egui::EventFilter {
383 horizontal_arrows: true,
384 vertical_arrows: true,
385 tab: edit.tab_cycle,
386 escape: true,
387 },
388 );
389 });
390 if opening {
394 resp.request_focus();
395 }
396 if resp.has_focus() {
397 let (tab, escape) = ui.input_mut(|i| {
398 (
399 edit.tab_cycle && i.consume_key(egui::Modifiers::NONE, egui::Key::Tab),
400 i.consume_key(egui::Modifiers::NONE, egui::Key::Escape),
401 )
402 });
403 if tab {
404 return Some(TextEvent::TabPressed {
405 id,
406 text: draft.text.clone(),
407 });
408 }
409 if escape {
410 return Some(TextEvent::Cancelled { id });
411 }
412 }
413 resp.lost_focus().then(|| TextEvent::Committed {
414 id,
415 text: draft.text.clone(),
416 })
417 }
418}
419
420struct Draft {
423 id: egui::Id,
424 text: String,
425}
426
427fn editor_frame(colors: &EditColors, vantage: Vantage) -> egui::Frame {
431 let margin = |len: f32| vantage.remap_len(WorldPx::new(len)).round() as i8;
432 egui::Frame::new()
433 .fill(colors.background.egui())
434 .stroke(egui::Stroke::new(
435 vantage.remap_len(EDITOR_BORDER),
436 colors.border.egui(),
437 ))
438 .corner_radius(vantage.remap_len(EDITOR_ROUNDING))
439 .inner_margin(egui::Margin::symmetric(
440 margin(EDITOR_PAD.x),
441 margin(EDITOR_PAD.y),
442 ))
443}
444
445#[derive(Clone, Copy)]
448pub struct Diagram<'a> {
449 pub draw_list: &'a [DrawOp],
450 pub vantage: Vantage,
451 pub ground: CanvasChrome,
452 pub cursor: Option<Cursor>,
453}
454
455pub struct Canvas {
460 rect: Rect,
461 response: egui::Response,
462 diagram: egui::layers::ShapeIdx,
464 pan: Camera,
465 moves: Vec<Move>,
466}
467
468impl Canvas {
469 pub fn viewport(&self) -> Rect {
471 self.rect
472 }
473
474 pub fn moves(&mut self) -> Vec<Move> {
478 std::mem::take(&mut self.moves)
479 }
480
481 pub fn input(&self, ui: &egui::Ui, glass: &[Rect]) -> Input {
494 let response = &self.response;
495 let over = response.hovered();
496 let held_here = response.is_pointer_button_down_on();
497 let mut raw = Vec::new();
498 if over || held_here {
499 ui.input(|input| {
500 for event in &input.events {
501 match *event {
502 egui::Event::PointerMoved(pos) => raw.push(Raw::Moved(pos.geom())),
503 egui::Event::PointerButton {
504 pos,
505 button,
506 pressed,
507 ..
508 } => {
509 let Some(button) = button_of(button) else {
510 continue;
511 };
512 let pos = pos.geom();
513 if pressed && glass.iter().any(|rect| rect.contains(pos)) {
514 continue;
515 }
516 raw.push(if pressed {
517 Raw::Down { pos, button }
518 } else {
519 Raw::Up { pos, button }
520 });
521 }
522 egui::Event::PointerGone => raw.push(Raw::Gone),
523 _ => {}
524 }
525 }
526 });
527 if over
530 && !raw.iter().any(|raw| matches!(raw, Raw::Moved(_)))
531 && let Some(pos) = response.hover_pos()
532 {
533 raw.push(Raw::Moved(pos.geom()));
534 }
535 } else {
536 raw.push(Raw::Gone);
537 }
538 if ui.input(|input| input.key_pressed(egui::Key::Escape)) {
540 raw.push(Raw::Cancelled);
541 }
542 if self.pan == Camera::Moving {
543 raw.push(Raw::Panning);
544 }
545 let keys = keys(response, ui);
546 Input { raw, keys }
547 }
548}
549
550fn button_of(button: PointerButton) -> Option<Button> {
551 match button {
552 PointerButton::Primary => Some(Button::Primary),
553 PointerButton::Secondary => Some(Button::Secondary),
554 PointerButton::Middle => Some(Button::Middle),
555 PointerButton::Extra1 | PointerButton::Extra2 => None,
556 }
557}
558
559fn keys(response: &egui::Response, ui: &egui::Ui) -> Keys {
561 let other_focus = ui.memory(|m| m.focused().is_some_and(|id| id != response.id));
566 let canvas_keys = !response.has_focus() && !other_focus;
567 Keys {
568 escape: canvas_keys && ui.input(|i| i.key_pressed(egui::Key::Escape)),
569 delete: canvas_keys
570 && ui
571 .input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)),
572 shift: ui.input(|i| i.modifiers.shift),
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579 use blockworx_geom::{Pos2, Vec2, pos2};
580 use blockworx_paint::{EditId, Font, Zoom};
581
582 fn at_rest(ground: CanvasChrome) -> Diagram<'static> {
584 Diagram {
585 draw_list: &[],
586 vantage: Vantage::resting(),
587 ground,
588 cursor: None,
589 }
590 }
591
592 fn frame(
595 ctx: &egui::Context,
596 view: &mut View,
597 events: Vec<egui::Event>,
598 ) -> (Vec<Raw>, Vec<Move>) {
599 let screen = Rect::from_min_size(Pos2::ZERO, Vec2::new(800.0, 600.0));
600 let input = egui::RawInput {
601 screen_rect: Some(screen.egui()),
602 events,
603 ..Default::default()
604 };
605 let mut seen = (Vec::new(), Vec::new());
606 let chrome = CanvasChrome {
607 background: Color::BLACK,
608 grid: Color::GRAY,
609 };
610 ctx.run_ui(input, |ui| {
611 let mut canvas = view.begin(ui);
612 seen = (canvas.input(ui, &[]).raw, canvas.moves());
613 view.show(ui, &canvas, at_rest(chrome));
614 })
615 .drop_without_applying_deltas();
616 seen
617 }
618
619 fn panning(seen: &[Raw]) -> bool {
622 seen.contains(&Raw::Panning)
623 }
624
625 fn key_down(key: egui::Key) -> egui::Event {
626 egui::Event::Key {
627 key,
628 physical_key: None,
629 pressed: true,
630 repeat: false,
631 modifiers: egui::Modifiers::default(),
632 }
633 }
634
635 fn key_up(key: egui::Key) -> egui::Event {
636 egui::Event::Key {
637 key,
638 physical_key: None,
639 pressed: false,
640 repeat: false,
641 modifiers: egui::Modifiers::default(),
642 }
643 }
644
645 fn press(pos: Pos2) -> egui::Event {
648 egui::Event::PointerButton {
649 pos: pos.egui(),
650 button: PointerButton::Primary,
651 pressed: true,
652 modifiers: egui::Modifiers::default(),
653 }
654 }
655
656 #[test]
659 fn space_plus_left_drag_pans_instead_of_reaching_the_tool() {
660 let ctx = egui::Context::default();
661 let mut view = View::default();
662 let start = pos2(400.0, 300.0);
663 frame(&ctx, &mut view, vec![key_down(egui::Key::Space)]);
664 frame(
665 &ctx,
666 &mut view,
667 vec![egui::Event::PointerMoved(start.egui())],
668 );
669 frame(&ctx, &mut view, vec![press(start)]);
670 let (seen, moves) = frame(
671 &ctx,
672 &mut view,
673 vec![egui::Event::PointerMoved(
674 (start + Vec2::new(60.0, -25.0)).egui(),
675 )],
676 );
677 assert_eq!(moves, vec![Move::Pan(Vec2::new(60.0, -25.0))]);
678 assert!(panning(&seen), "the pan leaked to the tool as {seen:?}");
679 }
680
681 #[test]
684 fn a_plain_left_drag_is_left_to_the_tool() {
685 let ctx = egui::Context::default();
686 let mut view = View::default();
687 let start = pos2(400.0, 300.0);
688 frame(
689 &ctx,
690 &mut view,
691 vec![egui::Event::PointerMoved(start.egui())],
692 );
693 frame(&ctx, &mut view, vec![press(start)]);
694 let (seen, moves) = frame(
695 &ctx,
696 &mut view,
697 vec![egui::Event::PointerMoved(
698 (start + Vec2::new(60.0, -25.0)).egui(),
699 )],
700 );
701 assert!(moves.is_empty(), "a plain drag must not pan: {moves:?}");
702 assert!(
703 !panning(&seen) && seen.iter().any(|raw| matches!(raw, Raw::Moved(_))),
704 "the tool never saw the drag ({seen:?})"
705 );
706 }
707
708 #[test]
711 fn releasing_the_key_mid_drag_keeps_panning() {
712 let ctx = egui::Context::default();
713 let mut view = View::default();
714 let start = pos2(400.0, 300.0);
715 frame(&ctx, &mut view, vec![key_down(egui::Key::Space)]);
716 frame(
717 &ctx,
718 &mut view,
719 vec![egui::Event::PointerMoved(start.egui())],
720 );
721 frame(&ctx, &mut view, vec![press(start)]);
722 let mut at = start + Vec2::new(30.0, 0.0);
723 frame(&ctx, &mut view, vec![egui::Event::PointerMoved(at.egui())]);
724 at += Vec2::new(20.0, 10.0);
725 let (seen, moves) = frame(
726 &ctx,
727 &mut view,
728 vec![
729 key_up(egui::Key::Space),
730 egui::Event::PointerMoved(at.egui()),
731 ],
732 );
733 assert_eq!(moves, vec![Move::Pan(Vec2::new(20.0, 10.0))]);
734 assert!(panning(&seen), "{seen:?}");
735 }
736
737 fn touch(id: u64, phase: egui::TouchPhase, pos: Pos2) -> egui::Event {
740 egui::Event::Touch {
741 device_id: egui::TouchDeviceId(0),
742 id: egui::TouchId(id),
743 phase,
744 pos: pos.egui(),
745 force: None,
746 }
747 }
748
749 fn two_fingers(phase: egui::TouchPhase, at: [Pos2; 2]) -> Vec<egui::Event> {
753 vec![
754 egui::Event::PointerMoved(at[0].egui()),
755 touch(0, phase, at[0]),
756 touch(1, phase, at[1]),
757 ]
758 }
759
760 fn start_two_finger_gesture(ctx: &egui::Context, view: &mut View, at: [Pos2; 2]) {
765 frame(ctx, view, Vec::new());
766 let mut down = two_fingers(egui::TouchPhase::Start, at);
767 down.push(press(at[0]));
768 frame(ctx, view, down);
769 frame(ctx, view, two_fingers(egui::TouchPhase::Move, at));
770 }
771
772 #[test]
777 fn two_fingers_pan_the_canvas() {
778 let ctx = egui::Context::default();
779 let mut view = View::default();
780 let start = [pos2(360.0, 300.0), pos2(440.0, 300.0)];
781 let step = Vec2::new(20.0, -12.0);
782
783 start_two_finger_gesture(&ctx, &mut view, start);
784 frame(
785 &ctx,
786 &mut view,
787 two_fingers(egui::TouchPhase::Move, start.map(|p| p + step)),
788 );
789 let (seen, moves) = frame(
790 &ctx,
791 &mut view,
792 two_fingers(egui::TouchPhase::Move, start.map(|p| p + step * 2.0)),
793 );
794
795 assert_eq!(
796 moves,
797 vec![Move::Pan(step)],
798 "the canvas did not follow the fingers, or zoomed though they held their spacing"
799 );
800 assert!(
801 panning(&seen),
802 "the primary finger reached the tool as a drag ({seen:?})"
803 );
804 }
805
806 #[test]
809 fn pinching_zooms_about_the_gesture_center() {
810 let ctx = egui::Context::default();
811 let mut view = View::default();
812 let center = pos2(400.0, 300.0);
813 let reach = Vec2::new(40.0, 0.0);
814 let start = [center - reach, center + reach];
815
816 start_two_finger_gesture(&ctx, &mut view, start);
817
818 let (_, moves) = frame(
819 &ctx,
820 &mut view,
821 two_fingers(
822 egui::TouchPhase::Move,
823 [center - reach * 2.0, center + reach * 2.0],
824 ),
825 );
826
827 let Some(Move::Zoom { factor, anchor }) = moves
828 .iter()
829 .find(|moved| matches!(moved, Move::Zoom { .. }))
830 else {
831 panic!("spreading the fingers reported no zoom: {moves:?}");
832 };
833 assert!(
834 factor.get() > 1.5,
835 "spreading the fingers should have doubled the zoom, got {factor:?}"
836 );
837 assert_eq!(*anchor, center, "the pinch zooms about its own centre");
838 }
839
840 #[test]
843 fn the_last_finger_of_a_pan_never_becomes_a_tool_drag() {
844 let ctx = egui::Context::default();
845 let mut view = View::default();
846 let start = [pos2(360.0, 300.0), pos2(440.0, 300.0)];
847 let step = Vec2::new(25.0, 0.0);
848
849 start_two_finger_gesture(&ctx, &mut view, start);
850 frame(
851 &ctx,
852 &mut view,
853 two_fingers(egui::TouchPhase::Move, start.map(|p| p + step)),
854 );
855 frame(
857 &ctx,
858 &mut view,
859 vec![touch(1, egui::TouchPhase::End, start[1] + step)],
860 );
861 let (seen, _) = frame(
862 &ctx,
863 &mut view,
864 vec![
865 egui::Event::PointerMoved((start[0] + step * 2.0).egui()),
866 touch(0, egui::TouchPhase::Move, start[0] + step * 2.0),
867 ],
868 );
869 assert!(
870 panning(&seen),
871 "the trailing finger reached the tool as a drag ({seen:?})"
872 );
873
874 frame(
876 &ctx,
877 &mut view,
878 vec![touch(0, egui::TouchPhase::End, start[0] + step * 2.0)],
879 );
880 assert!(!view.touch_pans, "the gesture latch outlived the gesture");
881 }
882
883 #[test]
888 fn an_open_editor_settles() {
889 let mut view = View::default();
890 let chrome = CanvasChrome {
891 background: Color::BLACK,
892 grid: Color::GRAY,
893 };
894 let field = EditField {
895 id: EditId::of("settle_title_edit"),
896 rect: Rect::from_min_size(pos2(100.0, 100.0), Vec2::new(120.0, 20.0)),
897 angle: blockworx_geom::Angle::ZERO,
898 font: Font::proportional(14.0),
899 align: blockworx_geom::Align2::LEFT_CENTER,
900 wrap_width: None,
901 text: "Amplifier".to_owned(),
902 multiline: false,
903 char_limit: Some(blockworx_geom::grid::MAX_LABEL_CHARS),
904 tab_cycle: true,
905 select_all_on_focus: false,
906 hint: None,
907 colors: blockworx_paint::EditColors {
908 text: Color::WHITE,
909 background: Color::BLACK,
910 caret: Color::WHITE,
911 selection: Color::GRAY,
912 hint: Color::GRAY,
913 border: Color::WHITE,
914 },
915 };
916 let settle = crate::settle::probe(30, |ui| {
917 ui.style_mut().visuals.text_cursor.blink = false;
921 let canvas = view.begin(ui);
922 view.show(ui, &canvas, at_rest(chrome));
923 let _ = view.capture(ui, Some(&field), Vantage::resting());
924 });
925 crate::settle::assert_settles(&settle, 8);
926 }
927
928 #[test]
929 fn frame_zoom_clamps_huge_content_to_the_floor() {
930 let viewport = Vec2::new(800.0, 600.0);
931 let huge = Vec2::new(100_000.0, 100_000.0);
932 assert_eq!(
933 Zoom::framing(viewport, huge, Zoom::max_value()),
934 Zoom::min_value()
935 );
936 }
937}