1use std::cell::{Cell, RefCell};
8use std::rc::Rc;
9
10use ravenrs_app_i18n::{Language, Texts};
11use ravenrs_core::{AccessTree, Border, Color, CursorIcon, Point, Primitive, Rect, Role, Size};
12use ravenrs_layout::{Align, Axis, LayoutStyle};
13use ravenrs_reactive::{Binding, State};
14use ravenrs_style::{Semantic, TextStyle};
15use ravenrs_widgets::{
16 BuildCx, Button, ButtonStyle, Dialog, Drag, DragPhase, IconName, NodeId, Paint, Placement, QuadPaint,
17 SegmentedControl, Semantics, Text, TextField, Toggle, UiActivity, VStack, View, ViewExt,
18};
19use serde::{Deserialize, Serialize};
20
21use crate::editor::{EditorLabels, EditorState, MovieEditor};
22use crate::movie::{FilmBuilder, Movie, MovieError};
23use crate::player::{MovieLabels, MoviePlayer};
24use crate::project::Region;
25
26const REPORT_MAGIC: &[u8] = b"RAVENBUG1\n";
27const SHORTEST_GAP: f64 = 1.0 / 15.0;
30const PREVIEW_WIDTH: f32 = 640.0;
31const SMALLEST_MARK: f32 = 12.0;
33const MARKED_ELEMENTS: usize = 12;
35const MARKING_DIM: f32 = 0.25;
37
38#[derive(Clone, Debug)]
39struct Stretch {
40 film: FilmBuilder,
41 started: f64,
42}
43
44#[derive(Debug)]
45struct Flight {
46 keep_seconds: f64,
47 size: Size,
48 newer: Option<Stretch>,
49 older: Option<Stretch>,
50 last_frame: Vec<Primitive>,
51 last_at: f64,
52 now: f64,
53 paused: bool,
54 described: Option<AccessTree>,
56}
57
58#[cfg_attr(feature = "live", ravenrs_live::expose)]
59impl Flight {
60 fn stretch(&mut self, now: f64, background: ravenrs_core::Color) -> &mut Stretch {
64 let full = self.newer.as_ref().is_none_or(|s| now - s.started >= self.keep_seconds / 2.0);
65 if full {
66 self.older = self.newer.take();
67 self.last_frame.clear();
68 self.newer = Some(Stretch { film: FilmBuilder::new(self.size, background), started: now });
69 }
70 self.newer
71 .get_or_insert_with(|| Stretch { film: FilmBuilder::new(self.size, background), started: now })
72 }
73
74 fn painted(&mut self, scene: &ravenrs_core::Scene, now: f64) {
75 self.now = now;
76 let primitives = scene.primitives();
77 if self.paused || (now - self.last_at < SHORTEST_GAP && !self.last_frame.is_empty()) {
78 return;
79 }
80 if self.last_frame == primitives
81 && self.newer.as_ref().is_some_and(|s| now - s.started < self.keep_seconds / 2.0)
82 {
83 return;
84 }
85 let stretch = self.stretch(now, scene.clear_color());
86 let at = ((now - stretch.started).max(0.0) * 1000.0) as u64;
87 stretch.film.frame(at, primitives);
88 self.last_frame = primitives.to_vec();
89 self.last_at = now;
90 }
91
92 fn pointer(&mut self, position: Point, clicked: bool, now: f64) {
93 self.now = now;
94 if self.paused {
95 return;
96 }
97 if let Some(stretch) = &mut self.newer {
98 let at = ((now - stretch.started).max(0.0) * 1000.0) as u64;
99 stretch.film.pointer(at, position);
100 if clicked {
101 stretch.film.click(at, position);
102 }
103 }
104 }
105
106 fn film(&self) -> Option<Movie> {
108 let newer = self.newer.as_ref()?;
109 let stretches: Vec<&Stretch> = self.older.iter().chain([newer]).collect();
110 let origin = stretches[0].started;
111 let mut whole: Option<FilmBuilder> = None;
112 let mut last_ms = 0;
113 for stretch in stretches {
114 let ms = ((self.now - stretch.started).max(0.0) * 1000.0) as u64;
115 let Ok(part) = stretch.film.clone().finish(ms) else {
116 continue;
117 };
118 let shift = ((stretch.started - origin).max(0.0) * 1000.0) as u64;
119 let logical = part.logical_size();
120 let background = part.background();
121 let film = whole.get_or_insert_with(|| FilmBuilder::new(logical, background));
122 for (at, list) in part.frames() {
123 let drawn: Vec<Primitive> = list.iter().filter_map(|i| part.primitive(*i).cloned()).collect();
124 film.frame(at + shift, &drawn);
125 }
126 for key in part.pointer_keys() {
127 film.pointer(key.at_ms + shift, Point::new(key.x, key.y));
128 }
129 for key in part.click_keys() {
130 film.click(key.at_ms + shift, Point::new(key.x, key.y));
131 }
132 last_ms = last_ms.max(ms + shift);
133 }
134 whole?.finish(last_ms).ok()
135 }
136}
137
138#[derive(Clone, Debug)]
154#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
155pub struct FlightRecorder {
156 flight: Rc<RefCell<Flight>>,
157}
158
159#[cfg_attr(feature = "live", ravenrs_live::expose)]
160impl FlightRecorder {
161 pub fn new(seconds: f64) -> Self {
163 Self {
164 flight: Rc::new(RefCell::new(Flight {
165 keep_seconds: seconds.max(1.0),
166 size: Size::new(1.0, 1.0),
167 newer: None,
168 older: None,
169 last_frame: Vec::new(),
170 last_at: f64::NEG_INFINITY,
171 now: 0.0,
172 paused: false,
173 described: None,
174 })),
175 }
176 }
177
178 pub fn film(&self) -> Option<Movie> {
180 self.flight.borrow().film()
181 }
182
183 fn take_described(&self) -> Option<AccessTree> {
184 self.flight.borrow_mut().described.take()
185 }
186
187 pub fn set_paused(&self, paused: bool) {
189 self.flight.borrow_mut().paused = paused;
190 }
191}
192
193impl View for FlightRecorder {
194 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
195 let size = cx.viewport();
196 {
197 let mut flight = self.flight.borrow_mut();
198 if flight.size != size {
199 flight.size = size;
201 flight.newer = None;
202 flight.older = None;
203 flight.last_frame.clear();
204 }
205 }
206 let flight = self.flight.clone();
207 cx.observe_activity(Rc::new(move |activity: UiActivity<'_>, now: f64| {
208 let Ok(mut flight) = flight.try_borrow_mut() else {
211 return;
212 };
213 match activity {
214 UiActivity::Painted(scene) => flight.painted(scene, now),
215 UiActivity::PointerMoved(position) => flight.pointer(position, false, now),
216 UiActivity::Clicked(position) => flight.pointer(position, true, now),
217 UiActivity::Described(tree) => flight.described = Some(tree.clone()),
218 _ => {}
219 }
220 }));
221 cx.empty()
222 }
223}
224
225#[derive(Clone, Debug, Default)]
227#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
228pub struct BugReport {
229 pub kind: String,
231 pub description: String,
233 pub area: Option<Region>,
235 pub elements: Vec<String>,
237 pub errors: Vec<String>,
239 pub film: Option<Movie>,
241 pub details: Vec<(String, String)>,
243}
244
245#[derive(Serialize, Deserialize)]
247struct ReportHeader {
248 #[serde(default)]
249 kind: String,
250 description: String,
251 #[serde(default)]
252 area: Option<Region>,
253 #[serde(default)]
254 elements: Vec<String>,
255 #[serde(default)]
256 errors: Vec<String>,
257 details: Vec<(String, String)>,
258}
259
260#[cfg_attr(feature = "live", ravenrs_live::expose)]
261impl BugReport {
262 pub fn to_bytes(&self) -> Vec<u8> {
264 let header = ReportHeader {
265 kind: self.kind.clone(),
266 description: self.description.clone(),
267 area: self.area,
268 elements: self.elements.clone(),
269 errors: self.errors.clone(),
270 details: self.details.clone(),
271 };
272 let header = serde_json::to_vec(&header).unwrap_or_default();
273 let mut bytes = Vec::new();
274 bytes.extend_from_slice(REPORT_MAGIC);
275 bytes.extend_from_slice(&(header.len() as u64).to_le_bytes());
276 bytes.extend_from_slice(&header);
277 if let Some(film) = &self.film {
278 bytes.extend_from_slice(&film.to_bytes());
279 }
280 bytes
281 }
282
283 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MovieError> {
285 let damaged = |why: &str| MovieError::Damaged(why.to_owned());
286 let rest =
287 bytes.strip_prefix(REPORT_MAGIC).ok_or_else(|| damaged("it does not start like a report"))?;
288 let (length, rest) = rest.split_first_chunk::<8>().ok_or_else(|| damaged("no header"))?;
289 let length = usize::try_from(u64::from_le_bytes(*length)).map_err(|_| damaged("header too long"))?;
290 let header = rest.get(..length).ok_or_else(|| damaged("the header was cut"))?;
291 let header: ReportHeader =
292 serde_json::from_slice(header).map_err(|e| MovieError::Damaged(e.to_string()))?;
293 let rest = &rest[length..];
294 let film = if rest.is_empty() { None } else { Some(Movie::from_bytes(rest)?) };
295 Ok(Self {
296 kind: header.kind,
297 description: header.description,
298 area: header.area,
299 elements: header.elements,
300 errors: header.errors,
301 film,
302 details: header.details,
303 })
304 }
305}
306
307#[derive(Clone, Debug)]
309#[non_exhaustive]
310#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
311pub struct BugLabels {
312 pub title: String,
314 pub intro: String,
316 pub description: String,
318 pub include_film: String,
320 pub film_warning: String,
322 pub edit_film: String,
324 pub done_editing: String,
326 pub send: String,
328 pub cancel: String,
330 pub kind: String,
332 pub kinds: Vec<String>,
334 pub mark: String,
336 pub mark_again: String,
338 pub mark_hint: String,
340 pub goes_along: String,
342 pub elements: String,
344 pub errors: String,
346 pub player: MovieLabels,
348 pub editor: EditorLabels,
350}
351
352impl Default for BugLabels {
353 fn default() -> Self {
354 Self::from_texts(&Texts::new(Language::English))
355 }
356}
357
358#[cfg_attr(feature = "live", ravenrs_live::expose)]
359impl BugLabels {
360 pub fn from_texts(texts: &Texts) -> Self {
362 Self {
363 title: texts.get("movie.bug.title"),
364 intro: texts.get("movie.bug.intro"),
365 description: texts.get("movie.bug.description"),
366 include_film: texts.get("movie.bug.include_film"),
367 film_warning: texts.get("movie.bug.film_warning"),
368 edit_film: texts.get("movie.bug.edit_film"),
369 done_editing: texts.get("movie.bug.done_editing"),
370 send: texts.get("movie.bug.send"),
371 cancel: texts.get("movie.bug.cancel"),
372 kind: texts.get("movie.bug.kind"),
373 kinds: texts.list("movie.bug.kinds"),
374 mark: texts.get("movie.bug.mark"),
375 mark_again: texts.get("movie.bug.mark_again"),
376 mark_hint: texts.get("movie.bug.mark_hint"),
377 goes_along: texts.get("movie.bug.goes_along"),
378 elements: texts.get("movie.bug.elements"),
379 errors: texts.get("movie.bug.errors"),
380 player: MovieLabels::from_texts(texts),
381 editor: EditorLabels::from_texts(texts),
382 }
383 }
384
385 pub fn pt_br() -> Self {
387 Self::from_texts(&Texts::new(Language::BrazilianPortuguese))
388 }
389}
390
391#[derive(Clone)]
393struct Draft {
394 editor: Option<EditorState>,
395 light: Option<u64>,
397 described: Option<AccessTree>,
398 elements: Vec<String>,
399}
400
401#[derive(Clone)]
409#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
410pub struct BugReporter {
411 recorder: FlightRecorder,
412 open: State<bool>,
413 editing: State<bool>,
414 include: State<bool>,
415 description: State<String>,
416 kind: State<usize>,
417 marking: State<bool>,
418 marked: State<Option<Rect>>,
419 drawing: State<Option<Rect>>,
420 asked: Rc<Cell<bool>>,
421 draft: Rc<RefCell<Draft>>,
422 labels: Rc<BugLabels>,
423 details: Rc<dyn Fn() -> Vec<(String, String)>>,
424 errors: Rc<dyn Fn() -> Vec<String>>,
425 on_send: Rc<dyn Fn(BugReport)>,
426}
427
428impl std::fmt::Debug for BugReporter {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 f.debug_struct("BugReporter").field("recorder", &self.recorder).finish_non_exhaustive()
431 }
432}
433
434#[cfg_attr(feature = "live", ravenrs_live::expose)]
435impl BugReporter {
436 pub fn new(recorder: FlightRecorder, on_send: impl Fn(BugReport) + 'static) -> Self {
438 Self {
439 recorder,
440 open: State::new(false),
441 editing: State::new(false),
442 include: State::new(true),
443 description: State::new(String::new()),
444 kind: State::new(0),
445 marking: State::new(false),
446 marked: State::new(None),
447 drawing: State::new(None),
448 asked: Rc::default(),
449 draft: Rc::new(RefCell::new(Draft {
450 editor: None,
451 light: None,
452 described: None,
453 elements: Vec::new(),
454 })),
455 labels: Rc::new(BugLabels::default()),
456 details: Rc::new(Vec::new),
457 errors: Rc::new(Vec::new),
458 on_send: Rc::new(on_send),
459 }
460 }
461
462 pub fn labels(mut self, labels: BugLabels) -> Self {
464 self.labels = Rc::new(labels);
465 self
466 }
467
468 pub fn details(mut self, details: impl Fn() -> Vec<(String, String)> + 'static) -> Self {
470 self.details = Rc::new(details);
471 self
472 }
473
474 pub fn errors(mut self, errors: impl Fn() -> Vec<String> + 'static) -> Self {
477 self.errors = Rc::new(errors);
478 self
479 }
480
481 pub fn open(&self) {
483 *self.draft.borrow_mut() = Draft {
484 editor: self.recorder.film().map(EditorState::new),
485 light: None,
486 described: None,
487 elements: Vec::new(),
488 };
489 self.recorder.set_paused(true);
490 self.description.set(String::new());
491 self.kind.set(0);
492 self.marking.set(false);
493 self.marked.set(None);
494 self.drawing.set(None);
495 self.include.set(true);
496 self.editing.set(false);
497 self.open.set(true);
498 }
499
500 fn close(&self) {
501 self.open.set(false);
502 self.editing.set(false);
503 self.marking.set(false);
504 self.draft.borrow_mut().editor = None;
505 self.recorder.set_paused(false);
506 }
507
508 fn start_marking(&self) {
510 self.draft.borrow_mut().described = None;
511 self.asked.set(false);
512 self.drawing.set(None);
513 self.marking.set(true);
514 }
515
516 fn finish_marking(&self, area: Rect) {
518 let region = Region::new(area.x(), area.y(), area.width(), area.height());
519 let mut draft = self.draft.borrow_mut();
520 draft.elements = draft.described.as_ref().map(|tree| named_under(tree, area)).unwrap_or_default();
521 if let Some(editor) = draft.editor.clone() {
522 draft.light = Some(editor.light_at_end(region, draft.light));
523 }
524 self.marked.set(Some(area));
525 self.marking.set(false);
526 }
527
528 fn send(&self) {
529 let film = if self.include.get() {
530 self.draft.borrow().editor.as_ref().map(EditorState::movie)
531 } else {
532 None
533 };
534 let kind = self.labels.kinds.get(self.kind.get()).cloned().unwrap_or_default();
535 let area = self.marked.get().map(|a| Region::new(a.x(), a.y(), a.width(), a.height()));
536 (self.on_send)(BugReport {
537 kind,
538 description: self.description.get(),
539 area,
540 elements: self.draft.borrow().elements.clone(),
541 errors: (self.errors)(),
542 film,
543 details: (self.details)(),
544 });
545 self.close();
546 }
547}
548
549impl View for BugReporter {
550 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
551 if !self.open.get() {
552 return cx.empty();
553 }
554 if self.marking.get() {
555 if let Some(tree) = self.recorder.take_described() {
556 self.draft.borrow_mut().described = Some(tree);
557 }
558 if self.draft.borrow().described.is_none() && !self.asked.replace(true) {
561 cx.ask_for_access_tree();
562 cx.wake_at(cx.now());
563 return cx.empty();
564 }
565 let anchor = cx.empty();
566 let leave = self.marking.clone();
567 cx.overlay(
568 anchor,
569 Placement::Fill,
570 &MarkingLayer(self.clone()),
571 Some(Rc::new(move || leave.set(false))),
572 );
573 return anchor;
574 }
575 let labels = &self.labels;
576 let viewport = cx.viewport();
577 let (closing, read_open, cancel) = (self.clone(), self.open.clone(), self.clone());
578 let open = Binding::new(
579 move || read_open.get(),
580 move |open: bool| {
581 if !open {
582 closing.close();
583 }
584 },
585 );
586 let editor = self.draft.borrow().editor.clone();
587 if self.editing.get()
588 && let Some(state) = editor
589 {
590 let area = Size::new(viewport.width - 120.0, viewport.height - 200.0);
591 let back = self.editing.clone();
592 let dialog = Dialog::new(open, labels.edit_film.clone())
593 .width(area.width + 48.0)
594 .content(
595 MovieEditor::new(state)
596 .labels(labels.editor.clone())
597 .area(area)
598 .frame(Some(area.width), Some(area.height)),
599 )
600 .action(Button::new(labels.done_editing.clone()).on_click(move || back.set(false)));
601 return cx.child(&dialog);
602 }
603 let include = self.include.get();
604 let has_film = editor.is_some();
605 let film = editor.filter(|_| include).map(|state| {
606 let edit = self.editing.clone();
607 VStack::new((
608 Text::new(labels.film_warning.clone())
609 .style(TextStyle::Callout)
610 .foreground(Semantic::InkMuted),
611 MoviePlayer::new(state.movie())
612 .labels(labels.player.clone())
613 .width(PREVIEW_WIDTH.min(viewport.width - 120.0)),
614 Button::new(labels.edit_film.clone())
615 .icon(IconName::EyeOff)
616 .style(ButtonStyle::Secondary)
617 .on_click(move || edit.set(true)),
618 ))
619 .spacing(10.0)
620 .align(Align::Start)
621 });
622 let send = self.clone();
623 let written = !self.description.get().trim().is_empty();
624 let mark = self.clone();
625 let marked = self.marked.get().is_some();
626 let (elements, errors) = (self.draft.borrow().elements.clone(), (self.errors)());
627 let details = (self.details)();
628 let along = (!elements.is_empty() || !errors.is_empty() || !details.is_empty()).then(|| {
629 let mut lines: Vec<ravenrs_widgets::AnyView> =
630 vec![Box::new(Text::new(labels.goes_along.clone()).style(TextStyle::Callout))];
631 let mut line = |text: String| {
632 lines
633 .push(Box::new(Text::new(text).style(TextStyle::Caption).foreground(Semantic::InkMuted)));
634 };
635 details.iter().for_each(|(name, value)| line(format!("{name}: {value}")));
636 if !elements.is_empty() {
637 line(format!("{}: {}", labels.elements, elements.join(", ")));
638 }
639 errors.iter().for_each(|error| line(format!("{}: {error}", labels.errors)));
640 VStack::new(lines).spacing(4.0).align(Align::Stretch)
641 });
642 let dialog = Dialog::new(open, labels.title.clone())
643 .description(labels.intro.clone())
644 .width(PREVIEW_WIDTH + 64.0)
645 .content(
646 ravenrs_widgets::ScrollView::new((VStack::new((
647 Text::new(labels.kind.clone()).style(TextStyle::Callout),
648 SegmentedControl::new(self.kind.binding(), labels.kinds.iter().map(String::as_str))
649 .label(labels.kind.clone()),
650 Text::new(labels.description.clone()).style(TextStyle::Callout),
651 TextField::new(self.description.binding()),
652 Button::new(if marked { labels.mark_again.clone() } else { labels.mark.clone() })
653 .icon(IconName::Crosshair)
654 .style(ButtonStyle::Secondary)
655 .on_click(move || mark.start_marking()),
656 has_film.then(|| Toggle::new(self.include.binding()).label(labels.include_film.clone())),
657 along,
658 film,
659 ))
660 .spacing(10.0)
661 .align(Align::Stretch),))
662 .frame(None, Some((viewport.height - 260.0).max(200.0))),
663 )
664 .action(
665 Button::new(labels.cancel.clone())
666 .style(ButtonStyle::Secondary)
667 .on_click(move || cancel.close()),
668 )
669 .action(Button::new(labels.send.clone()).disabled(!written).on_click(move || send.send()));
670 cx.child(&dialog)
671 }
672}
673
674fn named_under(tree: &AccessTree, area: Rect) -> Vec<String> {
677 let mut names: Vec<String> = Vec::new();
678 for node in &tree.nodes {
679 let Some(name) = node.name.as_deref().map(str::trim).filter(|name| !name.is_empty()) else {
680 continue;
681 };
682 let bounds = node.bounds;
683 let inside = bounds.intersect(&area);
684 let is_under = inside.width() * inside.height() * 2.0 >= bounds.width() * bounds.height()
685 && bounds.width() * bounds.height() > 0.0;
686 if !is_under || node.role == Role::Window {
687 continue;
688 }
689 if !names.iter().any(|known| known == name) {
690 names.push(name.to_owned());
691 }
692 if names.len() == MARKED_ELEMENTS {
693 break;
694 }
695 }
696 names
697}
698
699struct MarkingLayer(BugReporter);
701
702impl View for MarkingLayer {
703 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
704 let theme = *cx.theme();
705 let viewport = cx.viewport();
706 let reporter = &self.0;
707 let leave = reporter.marking.clone();
708 let hint = cx.child(
709 &VStack::new((
710 Text::new(reporter.labels.mark_hint.clone()),
711 Button::new(reporter.labels.cancel.clone())
712 .style(ButtonStyle::Secondary)
713 .on_click(move || leave.set(false)),
714 ))
715 .spacing(theme.spacing.s2)
716 .align(Align::Center)
717 .padding(theme.spacing.s3)
718 .background(Semantic::Surface)
719 .corner_radius(theme.radii.md),
720 );
721 let mut children = vec![hint];
722 if let Some(area) = reporter.drawing.get() {
723 let color = theme.color(Semantic::FocusRing);
724 let box_node = cx.container(
725 LayoutStyle {
726 absolute: Some((area.x(), area.y())),
727 width: Some(area.width()),
728 height: Some(area.height()),
729 ..LayoutStyle::stack(Axis::Vertical)
730 },
731 &[],
732 Paint::Quad(QuadPaint {
733 fill: Color::TRANSPARENT.into(),
734 corner_radius: 0.0,
735 border: Some(Border { width: 2.0, color }),
736 shadow: None,
737 }),
738 );
739 cx.semantics(box_node, Semantics::hidden());
740 children.push(box_node);
741 }
742 let mut dim = theme.color(Semantic::Scrim);
743 dim.a = MARKING_DIM;
744 let node = cx.container(
745 LayoutStyle {
746 width: Some(viewport.width),
747 height: Some(viewport.height),
748 padding: theme.spacing.s4.into(),
749 align: Align::Center,
750 ..LayoutStyle::stack(Axis::Vertical)
751 },
752 &children,
753 Paint::Quad(QuadPaint::new(dim)),
754 );
755 cx.cursor(node, CursorIcon::Default);
756 let drawing = reporter.clone();
757 let from = Rc::new(Cell::new(Point::ZERO));
758 let from = cx.use_state(move || from).get();
759 cx.on_drag(
760 node,
761 Rc::new(move |drag: Drag| {
762 if drag.phase == DragPhase::Start {
763 from.set(drag.position);
764 }
765 let start = from.get();
766 let (left, top) = (start.x.min(drag.position.x), start.y.min(drag.position.y));
767 let area = Rect::from_origin_size(
768 Point::new(left, top),
769 Size::new((start.x - drag.position.x).abs(), (start.y - drag.position.y).abs()),
770 );
771 if drag.phase != DragPhase::End {
772 drawing.drawing.set(Some(area));
773 } else if area.width() >= SMALLEST_MARK && area.height() >= SMALLEST_MARK {
774 drawing.finish_marking(area);
775 } else {
776 drawing.drawing.set(None);
777 }
778 }),
779 );
780 node
781 }
782}