1use std::cell::{Cell, RefCell};
5use std::rc::Rc;
6
7use ravenrs_app_i18n::{Language, Texts};
8use ravenrs_core::{Border, Color, CursorIcon, Key, Point, Rect, Size};
9use ravenrs_layout::{Align, Axis, LayoutStyle};
10use ravenrs_reactive::{Binding, State};
11use ravenrs_style::{Semantic, TextStyle, Weight};
12use ravenrs_widgets::{
13 AnyView, BuildCx, Button, ButtonSize, ButtonStyle, Divider, Drag, DragPhase, HStack, IconButton,
14 IconName, List, ListItem, NodeId, Paint, QuadPaint, ScrollView, Select, Semantics, Slider, Spacer,
15 Stepper, Text, TextField, Timeline, TimelineEdit, TimelineItem, TimelineMarker, TimelineTrack, Toggle,
16 Tone, ToolRail, VStack, View, ViewExt,
17};
18
19use crate::composition::{Composition, camera_at};
20use crate::movie::Movie;
21use crate::player::{MovieLabels, MoviePlayer};
22use crate::project::{CalloutStyle, Clip, ClipKind, Marker, Overlay, OverlayKind, Project, Region};
23
24const RAIL_WIDTH: f32 = 76.0;
25const LIBRARY_WIDTH: f32 = 236.0;
26const PROPERTIES_WIDTH: f32 = 288.0;
27const TOP_BAR: f32 = 52.0;
28const TRANSPORT: f32 = 48.0;
29const TIMELINE_BAR: f32 = 40.0;
30const TRACK_HEADS: f32 = 112.0;
31const OVERLAY_ROW: f32 = 34.0;
32const CLIP_ROW: f32 = 56.0;
33const THUMBNAIL: f32 = 64.0;
34const HANDLE: f32 = 8.0;
35const FRAME: f64 = 1.0 / 30.0;
36const SPEEDS: [f32; 5] = [0.5, 1.0, 1.5, 2.0, 4.0];
37const NEW_EFFECT_MS: u64 = 3000;
39const UNDO_LIMIT: usize = 200;
41
42#[derive(Clone, Debug)]
44#[non_exhaustive]
45#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
46pub struct EditorLabels {
47 pub try_it: String,
49 pub back_to_editing: String,
51 pub chapter: String,
53 pub new_chapter: String,
55 pub tab_annotations: String,
57 pub tab_screen: String,
59 pub tab_transitions: String,
61 pub tab_pointer: String,
63 pub tab_interaction: String,
65 pub tools: String,
67 pub library_hint: String,
69 pub track_film: String,
71 pub style: String,
73 pub style_balloon: String,
75 pub style_label: String,
77 pub soft: String,
79 pub soft_hint: String,
81 pub start: String,
83 pub timeline: String,
85 pub undo: String,
87 pub redo: String,
89 pub split: String,
91 pub delete: String,
93 pub save: String,
95 pub play: String,
97 pub pause: String,
99 pub effects: String,
101 pub properties: String,
103 pub nothing_selected: String,
105 pub film_clip: String,
107 pub title_card: String,
109 pub callout: String,
111 pub caption: String,
113 pub spotlight: String,
115 pub hide: String,
117 pub zoom: String,
119 pub fade_in: String,
121 pub fade_out: String,
123 pub pointer_highlight: String,
125 pub hotspot: String,
127 pub text: String,
129 pub subtitle: String,
131 pub duration: String,
133 pub speed: String,
135 pub wait_for_click: String,
137 pub timeline_zoom: String,
139 pub position: String,
141 pub previous_chapter: String,
143 pub next_chapter: String,
145 pub drag_hint: String,
147 pub new_text: String,
149}
150
151impl Default for EditorLabels {
152 fn default() -> Self {
153 Self::from_texts(&Texts::new(Language::English))
154 }
155}
156
157#[cfg_attr(feature = "live", ravenrs_live::expose)]
158impl EditorLabels {
159 pub fn from_texts(texts: &Texts) -> Self {
161 Self {
162 try_it: texts.get("movie.editor.try_it"),
163 back_to_editing: texts.get("movie.editor.back_to_editing"),
164 chapter: texts.get("movie.editor.chapter"),
165 new_chapter: texts.get("movie.editor.new_chapter"),
166 tab_annotations: texts.get("movie.editor.tab_annotations"),
167 tab_screen: texts.get("movie.editor.tab_screen"),
168 tab_transitions: texts.get("movie.editor.tab_transitions"),
169 tab_pointer: texts.get("movie.editor.tab_pointer"),
170 tab_interaction: texts.get("movie.editor.tab_interaction"),
171 tools: texts.get("movie.editor.tools"),
172 library_hint: texts.get("movie.editor.library_hint"),
173 track_film: texts.get("movie.editor.track_film"),
174 style: texts.get("movie.editor.style"),
175 style_balloon: texts.get("movie.editor.style_balloon"),
176 style_label: texts.get("movie.editor.style_label"),
177 soft: texts.get("movie.editor.soft"),
178 soft_hint: texts.get("movie.editor.soft_hint"),
179 start: texts.get("movie.editor.start"),
180 timeline: texts.get("movie.editor.timeline"),
181 undo: texts.get("movie.editor.undo"),
182 redo: texts.get("movie.editor.redo"),
183 split: texts.get("movie.editor.split"),
184 delete: texts.get("movie.editor.delete"),
185 save: texts.get("movie.editor.save"),
186 play: texts.get("movie.editor.play"),
187 pause: texts.get("movie.editor.pause"),
188 effects: texts.get("movie.editor.effects"),
189 properties: texts.get("movie.editor.properties"),
190 nothing_selected: texts.get("movie.editor.nothing_selected"),
191 film_clip: texts.get("movie.editor.film_clip"),
192 title_card: texts.get("movie.editor.title_card"),
193 callout: texts.get("movie.editor.callout"),
194 caption: texts.get("movie.editor.caption"),
195 spotlight: texts.get("movie.editor.spotlight"),
196 hide: texts.get("movie.editor.hide"),
197 zoom: texts.get("movie.editor.zoom"),
198 fade_in: texts.get("movie.editor.fade_in"),
199 fade_out: texts.get("movie.editor.fade_out"),
200 pointer_highlight: texts.get("movie.editor.pointer_highlight"),
201 hotspot: texts.get("movie.editor.hotspot"),
202 text: texts.get("movie.editor.text"),
203 subtitle: texts.get("movie.editor.subtitle"),
204 duration: texts.get("movie.editor.duration"),
205 speed: texts.get("movie.editor.speed"),
206 wait_for_click: texts.get("movie.editor.wait_for_click"),
207 timeline_zoom: texts.get("movie.editor.timeline_zoom"),
208 position: texts.get("movie.editor.position"),
209 previous_chapter: texts.get("movie.editor.previous_chapter"),
210 next_chapter: texts.get("movie.editor.next_chapter"),
211 drag_hint: texts.get("movie.editor.drag_hint"),
212 new_text: texts.get("movie.editor.new_text"),
213 }
214 }
215
216 pub fn pt_br() -> Self {
218 Self::from_texts(&Texts::new(Language::BrazilianPortuguese))
219 }
220}
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq)]
223enum Tab {
224 Annotations,
225 Screen,
226 Transitions,
227 Pointer,
228 Interaction,
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
232enum Selection {
233 Nothing,
234 Clip(usize),
235 Overlay(u64),
236}
237
238#[derive(Debug)]
239struct Inner {
240 movie: RefCell<Movie>,
241 position: Cell<u64>,
242 running: Cell<Option<(f64, u64)>>,
243 selection: Cell<Selection>,
244 undo: RefCell<Vec<Project>>,
245 redo: RefCell<Vec<Project>>,
246 last_edit: RefCell<Option<String>>,
248 density: Cell<f32>,
250 tab: Cell<Tab>,
251 trying: Cell<bool>,
253}
254
255#[derive(Clone, Debug)]
258#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
259pub struct EditorState {
260 inner: Rc<Inner>,
261 revision: State<u64>,
262}
263
264#[cfg_attr(feature = "live", ravenrs_live::expose)]
265impl EditorState {
266 pub fn new(movie: Movie) -> Self {
268 Self {
269 inner: Rc::new(Inner {
270 movie: RefCell::new(movie),
271 position: Cell::new(0),
272 running: Cell::new(None),
273 selection: Cell::new(Selection::Nothing),
274 undo: RefCell::new(Vec::new()),
275 redo: RefCell::new(Vec::new()),
276 last_edit: RefCell::new(None),
277 density: Cell::new(28.0),
278 tab: Cell::new(Tab::Annotations),
279 trying: Cell::new(false),
280 }),
281 revision: State::new(0),
282 }
283 }
284
285 pub fn movie(&self) -> Movie {
287 self.inner.movie.borrow().clone()
288 }
289
290 fn touch(&self) {
291 self.revision.update(|n| *n += 1);
292 }
293
294 fn edit(&self, key: Option<&str>, work: impl FnOnce(&mut Project)) {
296 let inner = &self.inner;
297 let mut project = inner.movie.borrow().project().clone();
298 let same = key.is_some() && inner.last_edit.borrow().as_deref() == key;
299 if !same {
300 let mut undo = inner.undo.borrow_mut();
301 undo.push(project.clone());
302 if undo.len() > UNDO_LIMIT {
303 undo.remove(0);
304 }
305 inner.redo.borrow_mut().clear();
306 }
307 *inner.last_edit.borrow_mut() = key.map(str::to_owned);
308 work(&mut project);
309 let movie = inner.movie.borrow().with_project(project);
310 *inner.movie.borrow_mut() = movie;
311 self.keep_position_inside();
312 self.touch();
313 }
314
315 fn keep_position_inside(&self) {
316 let duration = self.movie().duration_ms();
317 if self.inner.position.get() > duration {
318 self.inner.position.set(duration);
319 }
320 }
321
322 fn swap(&self, from: &RefCell<Vec<Project>>, to: &RefCell<Vec<Project>>) {
323 let Some(project) = from.borrow_mut().pop() else {
324 return;
325 };
326 to.borrow_mut().push(self.movie().project().clone());
327 let movie = self.movie().with_project(project);
328 *self.inner.movie.borrow_mut() = movie;
329 *self.inner.last_edit.borrow_mut() = None;
330 self.inner.selection.set(Selection::Nothing);
331 self.keep_position_inside();
332 self.touch();
333 }
334
335 pub fn undo(&self) {
337 self.swap(&self.inner.undo, &self.inner.redo);
338 }
339
340 pub fn redo(&self) {
342 self.swap(&self.inner.redo, &self.inner.undo);
343 }
344
345 pub fn position_ms(&self) -> u64 {
347 self.inner.position.get()
348 }
349
350 pub fn seek(&self, ms: u64) {
352 self.inner.position.set(ms.min(self.movie().duration_ms()));
353 if self.inner.running.get().is_some() {
354 self.inner.running.set(Some((f64::NAN, self.inner.position.get())));
355 }
356 self.touch();
357 }
358
359 pub fn toggle_playing(&self) {
361 if self.inner.running.get().is_some() {
362 self.inner.running.set(None);
363 } else {
364 if self.position_ms() >= self.movie().duration_ms() {
365 self.inner.position.set(0);
366 }
367 self.inner.running.set(Some((f64::NAN, self.position_ms())));
368 }
369 self.touch();
370 }
371
372 pub fn split(&self) {
374 let at = self.position_ms();
375 self.edit(None, |project| {
376 project.split(at);
377 });
378 }
379
380 pub fn delete_selected(&self) {
382 match self.inner.selection.get() {
383 Selection::Clip(index) => self.edit(None, |project| project.delete_clip(index)),
384 Selection::Overlay(id) => self.edit(None, |project| project.remove_overlay(id)),
385 Selection::Nothing => return,
386 }
387 self.inner.selection.set(Selection::Nothing);
388 self.touch();
389 }
390
391 pub fn select_clip(&self, index: usize) {
393 self.inner.selection.set(Selection::Clip(index));
394 *self.inner.last_edit.borrow_mut() = None;
395 self.touch();
396 }
397
398 pub fn select_overlay(&self, id: u64) {
400 self.inner.selection.set(Selection::Overlay(id));
401 *self.inner.last_edit.borrow_mut() = None;
402 self.touch();
403 }
404
405 pub fn add_overlay(&self, kind: OverlayKind) -> u64 {
407 let at = self.position_ms();
408 let mut id = 0;
409 self.edit(None, |project| {
410 let length = NEW_EFFECT_MS.min(project.duration_ms().saturating_sub(at).max(500));
411 id = project.add_overlay(kind, at, length);
412 });
413 self.inner.selection.set(Selection::Overlay(id));
414 self.touch();
415 id
416 }
417
418 pub fn light_at_end(&self, region: Region, previous: Option<u64>) -> u64 {
421 let mut id = 0;
422 self.edit(None, |project| {
423 if let Some(previous) = previous {
424 project.remove_overlay(previous);
425 }
426 let length = NEW_EFFECT_MS.min(project.duration_ms()).max(1);
427 let at = project.duration_ms().saturating_sub(length);
428 id = project.add_overlay(OverlayKind::Spotlight { region }, at, length);
429 });
430 self.touch();
431 id
432 }
433
434 pub fn add_title(&self, title: &str) {
436 let at = self.position_ms();
437 let mut index = 0;
438 self.edit(None, |project| index = project.insert_title(at, title, "", NEW_EFFECT_MS));
439 self.inner.selection.set(Selection::Clip(index));
440 self.touch();
441 }
442
443 pub fn add_chapter(&self, title: &str) {
445 let at_ms = self.position_ms();
446 self.edit(None, |project| project.markers.push(Marker { title: title.to_owned(), at_ms }));
447 }
448
449 pub fn set_speed(&self, speed: f32) {
451 if let Selection::Clip(index) = self.inner.selection.get() {
452 self.edit(None, |project| project.set_speed(index, speed));
453 }
454 }
455
456 fn change_overlay(&self, id: u64, key: &str, work: impl FnOnce(&mut Overlay)) {
457 self.edit(Some(&format!("{key}:{id}")), |project| {
458 if let Some(overlay) = project.overlay_mut(id) {
459 work(overlay);
460 }
461 });
462 }
463
464 fn selected_overlay(&self) -> Option<Overlay> {
465 let Selection::Overlay(id) = self.inner.selection.get() else {
466 return None;
467 };
468 self.movie().project().overlays.iter().find(|o| o.id == id).cloned()
469 }
470
471 fn selected_clip(&self) -> Option<(usize, Clip)> {
472 let Selection::Clip(index) = self.inner.selection.get() else {
473 return None;
474 };
475 self.movie().project().clips.get(index).cloned().map(|clip| (index, clip))
476 }
477
478 fn advance(&self, now: f64) {
480 let Some((since, from)) = self.inner.running.get() else {
481 return;
482 };
483 if since.is_nan() {
484 self.inner.running.set(Some((now, from)));
485 return;
486 }
487 let at = from + ((now - since).max(0.0) * 1000.0) as u64;
488 let duration = self.movie().duration_ms();
489 if at >= duration {
490 self.inner.position.set(duration);
491 self.inner.running.set(None);
492 } else {
493 self.inner.position.set(at);
494 }
495 }
496}
497
498fn region_of(kind: &OverlayKind) -> Option<Region> {
499 match kind {
500 OverlayKind::Callout { region, .. }
501 | OverlayKind::Spotlight { region }
502 | OverlayKind::Hide { region }
503 | OverlayKind::Zoom { region }
504 | OverlayKind::Hotspot { region, .. } => Some(*region),
505 _ => None,
506 }
507}
508
509fn set_region(kind: &mut OverlayKind, new: Region) {
510 match kind {
511 OverlayKind::Callout { region, .. }
512 | OverlayKind::Spotlight { region }
513 | OverlayKind::Hide { region }
514 | OverlayKind::Zoom { region }
515 | OverlayKind::Hotspot { region, .. } => *region = new,
516 _ => {}
517 }
518}
519
520fn label_of<'a>(kind: &OverlayKind, labels: &'a EditorLabels) -> &'a str {
521 match kind {
522 OverlayKind::Callout { .. } => &labels.callout,
523 OverlayKind::Caption { .. } => &labels.caption,
524 OverlayKind::Spotlight { .. } => &labels.spotlight,
525 OverlayKind::Hide { .. } => &labels.hide,
526 OverlayKind::Zoom { .. } => &labels.zoom,
527 OverlayKind::FadeIn => &labels.fade_in,
528 OverlayKind::FadeOut => &labels.fade_out,
529 OverlayKind::PointerHighlight => &labels.pointer_highlight,
530 OverlayKind::Hotspot { .. } => &labels.hotspot,
531 }
532}
533
534#[derive(Clone, Copy, PartialEq, Eq)]
536enum Row {
537 Interaction,
538 Annotations,
539 Screen,
540 Pointer,
541}
542
543fn row_of(kind: &OverlayKind) -> Row {
544 match kind {
545 OverlayKind::Hotspot { .. } => Row::Interaction,
546 OverlayKind::Callout { .. } | OverlayKind::Caption { .. } => Row::Annotations,
547 OverlayKind::PointerHighlight => Row::Pointer,
548 _ => Row::Screen,
549 }
550}
551
552const OVERLAY_IDS: u64 = 1 << 32;
554const FILM_STEP_IDS: u64 = 2 << 32;
556const STEP_MS: u64 = 700;
558
559fn on_timeline_edit(state: &EditorState, edit: TimelineEdit) {
560 match edit {
561 TimelineEdit::Began { .. } => *state.inner.last_edit.borrow_mut() = None,
562 TimelineEdit::Moved { id, start_ms } if (OVERLAY_IDS..FILM_STEP_IDS).contains(&id) => {
563 let id = id - OVERLAY_IDS;
564 state.inner.selection.set(Selection::Overlay(id));
565 state.change_overlay(id, "move", |overlay| overlay.start_ms = start_ms);
566 }
567 TimelineEdit::TrimmedStart { id, by_ms } if (OVERLAY_IDS..FILM_STEP_IDS).contains(&id) => {
568 state.change_overlay(id - OVERLAY_IDS, "resize", |overlay| {
569 let end = overlay.end_ms();
570 let start = (overlay.start_ms as i64 + by_ms).clamp(0, end as i64 - 100) as u64;
571 overlay.start_ms = start;
572 overlay.duration_ms = end - start;
573 });
574 }
575 TimelineEdit::TrimmedEnd { id, by_ms } if (OVERLAY_IDS..FILM_STEP_IDS).contains(&id) => {
576 state.change_overlay(id - OVERLAY_IDS, "resize", |overlay| {
577 overlay.duration_ms = (overlay.duration_ms as i64 + by_ms).max(100) as u64;
578 });
579 }
580 TimelineEdit::TrimmedStart { id, by_ms } => {
581 let index = id as usize;
582 state.edit(Some(&format!("trim:{index}")), |project| project.trim_start(index, by_ms));
583 }
584 TimelineEdit::TrimmedEnd { id, by_ms } => {
585 let index = id as usize;
586 state.edit(Some(&format!("trim:{index}")), |project| project.trim_end(index, by_ms));
587 }
588 _ => {}
589 }
590}
591
592fn timeline(state: &EditorState, labels: &EditorLabels) -> Timeline {
593 let movie = state.movie();
594 let project = movie.project();
595 let selection = state.inner.selection.get();
596 let overlays_in = |row: Row| -> Vec<TimelineItem> {
597 project
598 .overlays
599 .iter()
600 .filter(|overlay| row_of(&overlay.kind) == row)
601 .map(|overlay| {
602 let tone = match row {
603 Row::Interaction => Tone::Warning,
604 Row::Annotations => Tone::Accent,
605 Row::Screen => Tone::Success,
606 Row::Pointer => Tone::Neutral,
607 };
608 TimelineItem::new(
609 overlay.id + OVERLAY_IDS,
610 overlay.start_ms,
611 overlay.duration_ms,
612 label_of(&overlay.kind, labels),
613 )
614 .tone(tone)
615 .selected(selection == Selection::Overlay(overlay.id))
616 .movable()
617 .trimmable()
618 })
619 .collect()
620 };
621 let clips: Vec<TimelineItem> = project
622 .clips
623 .iter()
624 .enumerate()
625 .map(|(index, clip)| {
626 let start = project.clip_start_ms(index);
627 let label = match &clip.kind {
628 ClipKind::Film { .. } if (clip.speed - 1.0).abs() > 0.01 => {
629 format!("{} {} · {}×", labels.film_clip, index + 1, clip.speed)
630 }
631 ClipKind::Film { .. } => format!("{} {}", labels.film_clip, index + 1),
632 ClipKind::Title { title, .. } => title.clone(),
633 };
634 let moment = if start == 0 { movie.poster_ms() } else { start + 1 };
635 let item = TimelineItem::new(index as u64, start, clip.duration_ms(), label)
636 .tone(if matches!(clip.kind, ClipKind::Title { .. }) { Tone::Success } else { Tone::Accent })
637 .selected(selection == Selection::Clip(index));
638 if matches!(clip.kind, ClipKind::Film { .. }) {
641 item.leading(Composition::new(movie.clone(), moment).width(THUMBNAIL)).trimmable()
642 } else {
643 item
644 }
645 })
646 .collect();
647 let added: Vec<&str> = project
649 .overlays
650 .iter()
651 .filter_map(|overlay| match &overlay.kind {
652 OverlayKind::Hotspot { title, .. } => Some(title.as_str()),
653 _ => None,
654 })
655 .collect();
656 let mut steps = overlays_in(Row::Interaction);
657 for (index, spot) in movie.hotspots().iter().enumerate() {
658 if !added.contains(&spot.title.as_str()) {
659 let length = spot.until_ms.saturating_sub(spot.from_ms).max(STEP_MS);
660 steps.push(
661 TimelineItem::new(FILM_STEP_IDS + index as u64, spot.from_ms, length, spot.title.clone())
662 .tone(Tone::Warning),
663 );
664 }
665 }
666 let starts: Vec<u64> = movie.hotspots().iter().map(|spot| spot.from_ms).collect();
667 let (select, seek, edit) = (state.clone(), state.clone(), state.clone());
668 Timeline::new(vec![
669 TimelineTrack::new(labels.tab_interaction.clone(), OVERLAY_ROW, steps),
670 TimelineTrack::new(labels.tab_annotations.clone(), OVERLAY_ROW, overlays_in(Row::Annotations)),
671 TimelineTrack::new(labels.tab_screen.clone(), OVERLAY_ROW, overlays_in(Row::Screen)),
672 TimelineTrack::new(labels.tab_pointer.clone(), OVERLAY_ROW, overlays_in(Row::Pointer)),
673 TimelineTrack::new(labels.track_film.clone(), CLIP_ROW, clips),
674 ])
675 .markers(
676 movie.chapters().iter().map(|c| TimelineMarker { at_ms: c.at_ms, title: c.title.clone() }).collect(),
677 )
678 .position_ms(state.position_ms())
679 .pixels_per_second(state.inner.density.get())
680 .heads_width(TRACK_HEADS)
681 .on_seek(move |ms| seek.seek(ms))
682 .on_select(move |id| {
683 if id >= FILM_STEP_IDS {
684 if let Some(at) = usize::try_from(id - FILM_STEP_IDS).ok().and_then(|i| starts.get(i)) {
685 select.seek(*at);
686 }
687 } else if id >= OVERLAY_IDS {
688 select.select_overlay(id - OVERLAY_IDS);
689 } else {
690 select.select_clip(id as usize);
691 }
692 })
693 .on_edit(move |change| on_timeline_edit(&edit, change))
694}
695
696struct RegionHandle {
698 state: EditorState,
699 id: u64,
700 frame: Rect,
701 scale: f32,
702}
703
704impl View for RegionHandle {
705 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
706 let theme = *cx.theme();
707 let color = theme.color(Semantic::FocusRing);
708 let (state, id, scale) = (self.state.clone(), self.id, self.scale);
709 let size = HANDLE * 1.5;
710 let corner = cx.container(
711 LayoutStyle {
712 absolute: Some((self.frame.width() - size, self.frame.height() - size)),
713 width: Some(size),
714 height: Some(size),
715 ..LayoutStyle::stack(Axis::Vertical)
716 },
717 &[],
718 Paint::Quad(QuadPaint { corner_radius: 2.0, ..QuadPaint::new(color) }),
719 );
720 cx.semantics(corner, Semantics::hidden());
721 let start = cx.use_state(|| Rc::new(Cell::new(None::<(Point, Region)>))).get();
722 let resize = state.clone();
723 cx.on_drag(
724 corner,
725 Rc::new(move |drag: Drag| {
726 if let Some(mut region) = resize.selected_overlay().and_then(|o| region_of(&o.kind)) {
727 if drag.phase == DragPhase::Start {
728 *resize.inner.last_edit.borrow_mut() = None;
729 }
730 region.width = ((drag.position.x - drag.container.x()) / scale).max(16.0);
731 region.height = ((drag.position.y - drag.container.y()) / scale).max(16.0);
732 resize.change_overlay(id, "region", |overlay| set_region(&mut overlay.kind, region));
733 }
734 }),
735 );
736 cx.cursor(corner, CursorIcon::ColResize);
737 let node = cx.container(
738 LayoutStyle {
739 absolute: Some((self.frame.x(), self.frame.y())),
740 width: Some(self.frame.width()),
741 height: Some(self.frame.height()),
742 ..LayoutStyle::stack(Axis::Vertical)
743 },
744 &[corner],
745 Paint::Quad(QuadPaint {
746 fill: Color::TRANSPARENT.into(),
747 corner_radius: 0.0,
748 border: Some(Border { width: 2.0, color }),
749 shadow: None,
750 }),
751 );
752 cx.on_drag(
753 node,
754 Rc::new(move |drag: Drag| {
755 let Some(region) = state.selected_overlay().and_then(|o| region_of(&o.kind)) else {
756 return;
757 };
758 if drag.phase == DragPhase::Start {
759 start.set(Some((drag.position, region)));
760 *state.inner.last_edit.borrow_mut() = None;
761 return;
762 }
763 if let Some((from, was)) = start.get() {
764 let moved = Region {
765 x: was.x + (drag.position.x - from.x) / scale,
766 y: was.y + (drag.position.y - from.y) / scale,
767 ..was
768 };
769 state.change_overlay(id, "region", |overlay| set_region(&mut overlay.kind, moved));
770 }
771 }),
772 );
773 cx.cursor(node, CursorIcon::Pointer);
774 cx.semantics(node, Semantics::hidden());
775 node
776 }
777}
778
779struct Canvas {
781 state: EditorState,
782 width: f32,
783}
784
785impl View for Canvas {
786 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
787 let state = &self.state;
788 let movie = state.movie();
789 let logical = movie.logical_size();
790 let height = self.width * logical.height / logical.width.max(1.0);
791 let selected = state.selected_overlay();
792 let placing_zoom = matches!(selected.as_ref().map(|o| &o.kind), Some(OverlayKind::Zoom { .. }));
793 let at = state.position_ms();
794 let mut film = Composition::new(movie.clone(), at).width(self.width);
795 if placing_zoom {
796 film = film.ignoring_zoom();
797 }
798 let mut children = vec![cx.child(&film)];
799 if let Some(overlay) = &selected
800 && let Some(region) = region_of(&overlay.kind)
801 && overlay.covers(at)
802 {
803 let camera = camera_at(&movie, at, self.width, placing_zoom);
804 children.push(cx.child(&RegionHandle {
805 state: state.clone(),
806 id: overlay.id,
807 frame: camera.region(region),
808 scale: camera.scale(),
809 }));
810 }
811 let node = cx.container(
812 LayoutStyle {
813 width: Some(self.width),
814 height: Some(height),
815 overflow_hidden: true,
816 ..LayoutStyle::stack(Axis::Vertical)
817 },
818 &children,
819 Paint::None,
820 );
821 cx.clip(node);
822 node
823 }
824}
825
826type SaveHandler = Rc<dyn Fn(&Movie)>;
828
829#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
849pub struct MovieEditor {
850 state: EditorState,
851 labels: EditorLabels,
852 player_labels: MovieLabels,
853 title: String,
854 on_save: Option<SaveHandler>,
855 area: Option<Size>,
856}
857
858impl std::fmt::Debug for MovieEditor {
859 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
860 f.debug_struct("MovieEditor").field("state", &self.state).finish_non_exhaustive()
861 }
862}
863
864fn clock(ms: u64) -> String {
866 format!("{}:{:02}", ms / 60_000, ms / 1000 % 60)
867}
868
869fn panel_title(text: &str) -> Text {
870 Text::new(text.to_owned()).style(TextStyle::Headline)
871}
872
873fn muted(text: &str) -> Text {
874 Text::new(text.to_owned()).style(TextStyle::Callout).foreground(Semantic::InkMuted)
875}
876
877#[cfg_attr(feature = "live", ravenrs_live::expose)]
878impl MovieEditor {
879 pub fn new(state: EditorState) -> Self {
881 Self {
882 state,
883 labels: EditorLabels::default(),
884 player_labels: MovieLabels::default(),
885 title: String::new(),
886 on_save: None,
887 area: None,
888 }
889 }
890
891 pub fn labels(mut self, labels: EditorLabels) -> Self {
893 self.labels = labels;
894 self
895 }
896
897 pub fn player_labels(mut self, labels: MovieLabels) -> Self {
899 self.player_labels = labels;
900 self
901 }
902
903 pub fn title(mut self, title: impl Into<String>) -> Self {
905 self.title = title.into();
906 self
907 }
908
909 pub fn on_save(mut self, handler: impl Fn(&Movie) + 'static) -> Self {
911 self.on_save = Some(Rc::new(handler));
912 self
913 }
914
915 pub fn area(mut self, area: Size) -> Self {
917 self.area = Some(area);
918 self
919 }
920
921 fn top_bar(&self) -> impl View + use<> {
922 let labels = &self.labels;
923 let state = &self.state;
924 let movie = state.movie();
925 let (undo, redo, trying) = (state.clone(), state.clone(), state.clone());
926 let is_trying = state.inner.trying.get();
927 let can_undo = !state.inner.undo.borrow().is_empty();
928 let can_redo = !state.inner.redo.borrow().is_empty();
929 let kilobytes = movie.to_bytes().len() / 1024;
930 let save = self.on_save.clone().map(|save| {
931 let state = state.clone();
932 Button::new(labels.save.clone())
933 .icon(IconName::Save)
934 .size(ButtonSize::Small)
935 .on_click(move || save(&state.movie()))
936 });
937 HStack::new((
938 VStack::new((
939 (!self.title.is_empty()).then(|| Text::new(self.title.clone()).font_weight(Weight::SEMIBOLD)),
940 muted(&format!("{kilobytes} KB · {}", clock(movie.duration_ms()))),
941 ))
942 .spacing(0.0),
943 IconButton::new(IconName::Undo2)
944 .label(labels.undo.clone())
945 .disabled(!can_undo)
946 .on_click(move || undo.undo()),
947 IconButton::new(IconName::Redo2)
948 .label(labels.redo.clone())
949 .disabled(!can_redo)
950 .on_click(move || redo.redo()),
951 Spacer::new(),
952 Button::new(if is_trying { labels.back_to_editing.clone() } else { labels.try_it.clone() })
953 .icon(if is_trying { IconName::Pencil } else { IconName::Play })
954 .style(ButtonStyle::Secondary)
955 .size(ButtonSize::Small)
956 .on_click(move || {
957 trying.inner.trying.set(!trying.inner.trying.get());
958 trying.inner.running.set(None);
959 trying.touch();
960 }),
961 save,
962 ))
963 .spacing(8.0)
964 .align(Align::Center)
965 .padding((0.0, 12.0))
966 .frame(None, Some(TOP_BAR))
967 .background(Semantic::Surface)
968 }
969
970 fn rail(&self) -> impl View + use<> {
971 let labels = &self.labels;
972 const TABS: [Tab; 5] =
973 [Tab::Annotations, Tab::Screen, Tab::Transitions, Tab::Pointer, Tab::Interaction];
974 let (read, write) = (self.state.clone(), self.state.clone());
975 let selected = Binding::new(
976 move || TABS.iter().position(|tab| *tab == read.inner.tab.get()).unwrap_or(0),
977 move |index: usize| {
978 write.inner.tab.set(TABS.get(index).copied().unwrap_or(Tab::Annotations));
979 write.touch();
980 },
981 );
982 ToolRail::new(
983 selected,
984 [
985 (IconName::MessageSquare, labels.tab_annotations.clone()),
986 (IconName::Maximize2, labels.tab_screen.clone()),
987 (IconName::Layers, labels.tab_transitions.clone()),
988 (IconName::MousePointer, labels.tab_pointer.clone()),
989 (IconName::Crosshair, labels.tab_interaction.clone()),
990 ],
991 )
992 .width(RAIL_WIDTH)
993 .background(Semantic::Surface)
994 .frame(Some(RAIL_WIDTH), None)
995 }
996
997 fn library(&self) -> impl View + use<> {
998 let labels = &self.labels;
999 let logical = self.state.movie().logical_size();
1000 let middle = Region::new(
1001 logical.width * 0.3,
1002 logical.height * 0.3,
1003 logical.width * 0.4,
1004 logical.height * 0.25,
1005 );
1006 let note = Region::new(
1007 logical.width * 0.62,
1008 logical.height * 0.12,
1009 logical.width * 0.3,
1010 logical.height * 0.12,
1011 );
1012 let text = labels.new_text.clone();
1013 let tab = self.state.inner.tab.get();
1014 let kinds: Vec<(String, IconName, OverlayKind)> = match tab {
1015 Tab::Annotations => vec![
1016 (
1017 labels.callout.clone(),
1018 IconName::MessageSquare,
1019 OverlayKind::Callout {
1020 text: text.clone(),
1021 region: note,
1022 style: CalloutStyle::Balloon,
1023 soft: true,
1024 },
1025 ),
1026 (labels.caption.clone(), IconName::Type, OverlayKind::Caption { text: text.clone() }),
1027 ],
1028 Tab::Screen => vec![
1029 (labels.spotlight.clone(), IconName::Sparkles, OverlayKind::Spotlight { region: middle }),
1030 (labels.zoom.clone(), IconName::ZoomIn, OverlayKind::Zoom { region: middle }),
1031 (labels.hide.clone(), IconName::EyeOff, OverlayKind::Hide { region: middle }),
1032 ],
1033 Tab::Transitions => vec![
1034 (labels.fade_in.clone(), IconName::Eye, OverlayKind::FadeIn),
1035 (labels.fade_out.clone(), IconName::EyeOff, OverlayKind::FadeOut),
1036 ],
1037 Tab::Pointer => {
1038 vec![(
1039 labels.pointer_highlight.clone(),
1040 IconName::MousePointer,
1041 OverlayKind::PointerHighlight,
1042 )]
1043 }
1044 Tab::Interaction => vec![(
1045 labels.hotspot.clone(),
1046 IconName::Crosshair,
1047 OverlayKind::Hotspot { region: note, title: labels.hotspot.clone(), text, stop: true },
1048 )],
1049 };
1050 let title = match tab {
1051 Tab::Annotations => &labels.tab_annotations,
1052 Tab::Screen => &labels.tab_screen,
1053 Tab::Transitions => &labels.tab_transitions,
1054 Tab::Pointer => &labels.tab_pointer,
1055 Tab::Interaction => &labels.tab_interaction,
1056 };
1057 let mut rows: Vec<ListItem> = Vec::with_capacity(kinds.len() + 1);
1058 if tab == Tab::Annotations {
1059 let (titles, first_words) = (self.state.clone(), labels.new_text.clone());
1060 rows.push(
1061 ListItem::new(labels.title_card.clone())
1062 .icon(IconName::Layers)
1063 .on_click(move || titles.add_title(&first_words)),
1064 );
1065 }
1066 for (name, icon, kind) in kinds {
1067 let state = self.state.clone();
1068 rows.push(ListItem::new(name).icon(icon).on_click(move || {
1069 state.add_overlay(kind.clone());
1070 }));
1071 }
1072 VStack::new((panel_title(title), ScrollView::new((List::new(rows),)), muted(&labels.library_hint)))
1073 .spacing(12.0)
1074 .align(Align::Stretch)
1075 .padding(12.0)
1076 .background(Semantic::Surface)
1077 .frame(Some(LIBRARY_WIDTH), None)
1078 }
1079
1080 fn properties(&self) -> impl View + use<> {
1081 let labels = &self.labels;
1082 let state = &self.state;
1083 let mut items: Vec<AnyView> = Vec::new();
1084 if let Some(overlay) = state.selected_overlay() {
1085 let id = overlay.id;
1086 items.push(Box::new(panel_title(label_of(&overlay.kind, labels))));
1087 let has_text = matches!(
1088 overlay.kind,
1089 OverlayKind::Callout { .. } | OverlayKind::Caption { .. } | OverlayKind::Hotspot { .. }
1090 );
1091 if has_text {
1092 let (read, write) = (state.clone(), state.clone());
1093 let text = Binding::new(
1094 move || match read.selected_overlay().map(|o| o.kind) {
1095 Some(
1096 OverlayKind::Callout { text, .. }
1097 | OverlayKind::Caption { text }
1098 | OverlayKind::Hotspot { text, .. },
1099 ) => text,
1100 _ => String::new(),
1101 },
1102 move |new: String| {
1103 write.change_overlay(id, "text", |overlay| match &mut overlay.kind {
1104 OverlayKind::Callout { text, .. }
1105 | OverlayKind::Caption { text }
1106 | OverlayKind::Hotspot { text, .. } => *text = new,
1107 _ => {}
1108 });
1109 },
1110 );
1111 items.push(Box::new(muted(&labels.text)));
1112 items.push(Box::new(TextField::new(text)));
1113 }
1114 if let OverlayKind::Callout { style, .. } = &overlay.kind {
1115 let (balloon, tag) = (state.clone(), state.clone());
1116 let pick = |state: EditorState, chosen: CalloutStyle| {
1117 move || {
1118 state.change_overlay(id, "style", |overlay| {
1119 if let OverlayKind::Callout { style, .. } = &mut overlay.kind {
1120 *style = chosen;
1121 }
1122 });
1123 }
1124 };
1125 let chosen = |this: CalloutStyle| {
1126 if *style == this { ButtonStyle::Primary } else { ButtonStyle::Secondary }
1127 };
1128 items.push(Box::new(muted(&labels.style)));
1129 items.push(Box::new(
1130 HStack::new((
1131 Button::new(labels.style_balloon.clone())
1132 .size(ButtonSize::Small)
1133 .style(chosen(CalloutStyle::Balloon))
1134 .on_click(pick(balloon, CalloutStyle::Balloon)),
1135 Button::new(labels.style_label.clone())
1136 .size(ButtonSize::Small)
1137 .style(chosen(CalloutStyle::Label))
1138 .on_click(pick(tag, CalloutStyle::Label)),
1139 ))
1140 .spacing(4.0),
1141 ));
1142 let (read, write) = (state.clone(), state.clone());
1143 let soft = Binding::new(
1144 move || {
1145 matches!(
1146 read.selected_overlay().map(|o| o.kind),
1147 Some(OverlayKind::Callout { soft: true, .. })
1148 )
1149 },
1150 move |new: bool| {
1151 write.change_overlay(id, "soft", |overlay| {
1152 if let OverlayKind::Callout { soft, .. } = &mut overlay.kind {
1153 *soft = new;
1154 }
1155 });
1156 },
1157 );
1158 items.push(Box::new(Toggle::new(soft).label(labels.soft.clone())));
1159 }
1160 if matches!(overlay.kind, OverlayKind::Hotspot { .. }) {
1161 let (read, write) = (state.clone(), state.clone());
1162 let stop = Binding::new(
1163 move || {
1164 matches!(
1165 read.selected_overlay().map(|o| o.kind),
1166 Some(OverlayKind::Hotspot { stop: true, .. })
1167 )
1168 },
1169 move |new: bool| {
1170 write.change_overlay(id, "stop", |overlay| {
1171 if let OverlayKind::Hotspot { stop, .. } = &mut overlay.kind {
1172 *stop = new;
1173 }
1174 });
1175 },
1176 );
1177 items.push(Box::new(Toggle::new(stop).label(labels.wait_for_click.clone())));
1178 }
1179 let total = state.movie().duration_ms().max(1) as f32 / 1000.0;
1180 let (read, write) = (state.clone(), state.clone());
1181 let start = Binding::new(
1182 move || read.selected_overlay().map_or(0.0, |o| o.start_ms as f32 / 1000.0),
1183 move |seconds: f32| {
1184 write.change_overlay(id, "start", |o| o.start_ms = (seconds * 1000.0) as u64)
1185 },
1186 );
1187 items.push(Box::new(muted(&format!("{}: {}", labels.start, clock(overlay.start_ms)))));
1188 items.push(Box::new(Slider::new(start).range(0.0, total).step(0.1).label(labels.start.clone())));
1189 let (read, write) = (state.clone(), state.clone());
1190 let duration = Binding::new(
1191 move || read.selected_overlay().map_or(1.0, |o| o.duration_ms as f32 / 1000.0),
1192 move |seconds: f32| {
1193 write.change_overlay(id, "duration", |o| o.duration_ms = (seconds * 1000.0) as u64)
1194 },
1195 );
1196 items.push(Box::new(muted(&format!(
1197 "{}: {:.1} s",
1198 labels.duration,
1199 overlay.duration_ms as f32 / 1000.0
1200 ))));
1201 items.push(Box::new(
1202 Slider::new(duration).range(0.5, total.max(1.0)).step(0.1).label(labels.duration.clone()),
1203 ));
1204 if region_of(&overlay.kind).is_some() {
1205 let axis = |state: &EditorState, vertical: bool, name: &str| {
1206 let (read, write) = (state.clone(), state.clone());
1207 let value = Binding::new(
1208 move || {
1209 let region = read.selected_overlay().and_then(|o| region_of(&o.kind));
1210 region.map_or(0, |r| if vertical { r.y as i32 } else { r.x as i32 })
1211 },
1212 move |new: i32| {
1213 write.change_overlay(id, "region", |overlay| {
1214 if let Some(mut region) = region_of(&overlay.kind) {
1215 if vertical {
1216 region.y = new as f32;
1217 } else {
1218 region.x = new as f32;
1219 }
1220 set_region(&mut overlay.kind, region);
1221 }
1222 });
1223 },
1224 );
1225 Stepper::new(value).range(0, 8000).label(name.to_owned())
1226 };
1227 items.push(Box::new(axis(state, false, "X")));
1228 items.push(Box::new(axis(state, true, "Y")));
1229 items.push(Box::new(muted(&labels.drag_hint)));
1230 }
1231 } else if let Some((index, clip)) = state.selected_clip() {
1232 match &clip.kind {
1233 ClipKind::Film { .. } => {
1234 items.push(Box::new(panel_title(&labels.film_clip)));
1235 items.push(Box::new(muted(&clock(clip.duration_ms()))));
1236 }
1237 ClipKind::Title { .. } => {
1238 items.push(Box::new(panel_title(&labels.title_card)));
1239 let words = |second: bool| {
1240 let (read, write) = (state.clone(), state.clone());
1241 Binding::new(
1242 move || match read.selected_clip().map(|(_, clip)| clip.kind) {
1243 Some(ClipKind::Title { title, subtitle, .. }) => {
1244 if second {
1245 subtitle
1246 } else {
1247 title
1248 }
1249 }
1250 _ => String::new(),
1251 },
1252 move |new: String| {
1253 write.edit(Some(&format!("card:{index}:{second}")), |project| {
1254 if let Some(ClipKind::Title { title, subtitle, .. }) =
1255 project.clips.get_mut(index).map(|clip| &mut clip.kind)
1256 {
1257 *(if second { subtitle } else { title }) = new;
1258 }
1259 });
1260 },
1261 )
1262 };
1263 items.push(Box::new(muted(&labels.text)));
1264 items.push(Box::new(TextField::new(words(false))));
1265 items.push(Box::new(muted(&labels.subtitle)));
1266 items.push(Box::new(TextField::new(words(true))));
1267 }
1268 }
1269 } else {
1270 items.push(Box::new(panel_title(&labels.properties)));
1271 items.push(Box::new(muted(&labels.nothing_selected)));
1272 }
1273 ScrollView::new((VStack::new(items).spacing(10.0).align(Align::Stretch),))
1274 .padding(12.0)
1275 .background(Semantic::Surface)
1276 .frame(Some(PROPERTIES_WIDTH), None)
1277 }
1278
1279 fn transport(&self) -> impl View + use<> {
1280 let labels = &self.labels;
1281 let state = &self.state;
1282 let running = state.inner.running.get().is_some();
1283 let movie = state.movie();
1284 let at = state.position_ms();
1285 let chapter_to = |target: Option<u64>| {
1286 let seek = state.clone();
1287 move || {
1288 if let Some(ms) = target {
1289 seek.seek(ms);
1290 }
1291 }
1292 };
1293 let previous = movie.chapters().iter().rev().find(|c| c.at_ms < at).map(|c| c.at_ms);
1294 let next = movie.chapters().iter().find(|c| c.at_ms > at).map(|c| c.at_ms);
1295 let toggle = state.clone();
1296 HStack::new((
1297 IconButton::new(IconName::ChevronLeft)
1298 .label(labels.previous_chapter.clone())
1299 .disabled(previous.is_none())
1300 .on_click(chapter_to(previous)),
1301 Button::new(if running { labels.pause.clone() } else { labels.play.clone() })
1302 .icon(if running { IconName::Pause } else { IconName::Play })
1303 .size(ButtonSize::Small)
1304 .on_click(move || toggle.toggle_playing()),
1305 IconButton::new(IconName::ChevronRight)
1306 .label(labels.next_chapter.clone())
1307 .disabled(next.is_none())
1308 .on_click(chapter_to(next)),
1309 Text::new(format!("{} / {}", clock(at), clock(movie.duration_ms()))).style(TextStyle::Callout),
1310 ))
1311 .spacing(6.0)
1312 .align(Align::Center)
1313 .frame(None, Some(TRANSPORT))
1314 }
1315
1316 fn timeline_bar(&self) -> impl View + use<> {
1317 let labels = &self.labels;
1318 let state = &self.state;
1319 let (split, delete, chapter) = (state.clone(), state.clone(), state.clone());
1320 let new_chapter = labels.new_chapter.clone();
1321 let selected = state.inner.selection.get() != Selection::Nothing;
1322 let speed_clip = state.selected_clip().filter(|(_, clip)| matches!(clip.kind, ClipKind::Film { .. }));
1323 let (read, write) = (state.clone(), state.clone());
1324 let speed = Binding::new(
1325 move || {
1326 let speed = read.selected_clip().map_or(1.0, |(_, clip)| clip.speed);
1327 SPEEDS.iter().position(|s| (*s - speed).abs() < 0.01).unwrap_or(1)
1328 },
1329 move |choice: usize| write.set_speed(SPEEDS.get(choice).copied().unwrap_or(1.0)),
1330 );
1331 let (read, write) = (state.clone(), state.clone());
1332 let density = Binding::new(
1333 move || read.inner.density.get(),
1334 move |value: f32| {
1335 write.inner.density.set(value);
1336 write.touch();
1337 },
1338 );
1339 let tool = |label: &str, icon: IconName| {
1340 Button::new(label.to_owned()).icon(icon).style(ButtonStyle::Ghost).size(ButtonSize::Small)
1341 };
1342 HStack::new((
1343 tool(&labels.split, IconName::Scissors).on_click(move || split.split()),
1344 tool(&labels.delete, IconName::Trash2)
1345 .disabled(!selected)
1346 .on_click(move || delete.delete_selected()),
1347 tool(&labels.chapter, IconName::Flag).on_click(move || chapter.add_chapter(&new_chapter)),
1348 speed_clip.map(|_| {
1349 HStack::new((
1350 muted(&labels.speed),
1351 Select::new(speed, SPEEDS.iter().map(|s| format!("{s}×"))).width(90.0),
1352 ))
1353 .spacing(6.0)
1354 .align(Align::Center)
1355 }),
1356 Spacer::new(),
1357 Slider::new(density).range(5.0, 200.0).step(1.0).label(labels.timeline_zoom.clone()).width(130.0),
1358 ))
1359 .spacing(4.0)
1360 .align(Align::Center)
1361 .padding((0.0, 12.0))
1362 .frame(None, Some(TIMELINE_BAR))
1363 }
1364}
1365
1366impl View for MovieEditor {
1367 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
1368 let _ = self.state.revision.get();
1369 let now = cx.now();
1370 self.state.advance(now);
1371 if self.state.inner.running.get().is_some() {
1372 cx.wake_at(now + FRAME);
1373 }
1374 let (undo, redo, delete) = (self.state.clone(), self.state.clone(), self.state.clone());
1375 cx.shortcut(Key::Character("z".into()), true, Rc::new(move || undo.undo()));
1376 cx.shortcut(Key::Character("y".into()), true, Rc::new(move || redo.redo()));
1377 cx.shortcut(Key::Delete, false, Rc::new(move || delete.delete_selected()));
1378
1379 let viewport = self.area.unwrap_or_else(|| cx.viewport());
1380 let movie = self.state.movie();
1381 let logical = movie.logical_size();
1382 let timeline = timeline(&self.state, &self.labels);
1383 let timeline_height = timeline.total_height() + TIMELINE_BAR;
1384 let middle_height = (viewport.height - TOP_BAR - timeline_height).max(200.0);
1385 let by_width = (viewport.width - RAIL_WIDTH - LIBRARY_WIDTH - PROPERTIES_WIDTH - 48.0).max(240.0);
1386 let by_height =
1387 (middle_height - TRANSPORT - 16.0).max(120.0) * logical.width / logical.height.max(1.0);
1388 let canvas_width = by_width.min(by_height);
1389
1390 let center: AnyView = if self.state.inner.trying.get() {
1391 Box::new(
1392 ScrollView::new((MoviePlayer::new(movie.clone())
1393 .labels(self.player_labels.clone())
1394 .width(by_width.max(480.0)),))
1395 .padding(8.0)
1396 .grow(1.0),
1397 )
1398 } else {
1399 Box::new(
1400 VStack::new((
1401 Spacer::new(),
1402 Canvas { state: self.state.clone(), width: canvas_width },
1403 Spacer::new(),
1404 self.transport(),
1405 ))
1406 .spacing(0.0)
1407 .align(Align::Center)
1408 .grow(1.0),
1409 )
1410 };
1411 let tree = VStack::new((
1412 self.top_bar(),
1413 Divider::horizontal(),
1414 HStack::new((self.rail(), self.library(), center, self.properties()))
1415 .spacing(0.0)
1416 .align(Align::Stretch)
1417 .grow(1.0),
1418 Divider::horizontal(),
1419 VStack::new((self.timeline_bar(), timeline))
1420 .spacing(0.0)
1421 .align(Align::Stretch)
1422 .background(Semantic::Surface),
1423 ))
1424 .spacing(0.0)
1425 .align(Align::Stretch)
1426 .grow(1.0);
1427 cx.child(&tree)
1428 }
1429}