//! The design panel: layout, master, theme, size (SHOW-01, 02). use std::collections::HashMap; use ely_gpui_component::{ buttons::{Button, ButtonVariant}, data_display::DescriptionList, forms::{Choice, NumberInput, Select}, layout::ScrollArea, }; use ely_lok::show::LAYOUTS; use ely_office::prelude::*; use gpui::{ AnyElement, App, Context, IntoElement, ParentElement as _, Render, SharedString, Styled as _, WeakEntity, Window, div, }; use crate::deck::Deck; /// Slide sizes in 1/100 mm. const SIZES: [(&str, (i64, i64)); 5] = [ ("wide", (33_867, 19_050)), ("hd", (28_000, 15_750)), ("standard", (25_400, 19_050)), ("a4", (29_700, 21_000)), ("letter", (27_940, 21_590)), ]; /// Twips to 1/100 mm. pub fn mm100((w, h): (i64, i64)) -> (i64, i64) { ((w * 127 + 36) / 72, (h * 127 + 36) / 72) } /// The preset a 1/100 mm size matches, within 0.5 mm. fn preset((w, h): (i64, i64)) -> Option<&'static str> { SIZES .iter() .find(|(_, (pw, ph))| (pw - w).abs() <= 50 && (ph - h).abs() <= 50) .map(|(key, _)| *key) } impl Deck { /// Runs a design command; the next read shows what changed. pub fn design_command(&mut self, id: &str, values: &[&str], cx: &mut Context) { let Some(tab) = self.core.active else { return; }; self.run_in(Some(tab), id, values, cx); self.force_reread(tab, cx); } /// Resizes the slides; the asked size shows until read back. pub(crate) fn resize(&mut self, (w, h): (i64, i64), cx: &mut Context) { let Some(tab) = self.core.active.and_then(|id| self.tab_mut(id)) else { return; }; let (sw, sh) = tab.extra.shown_size(); if (sw - w).abs() <= 2 && (sh - h).abs() <= 2 { log::info!("show: the slides already measure {w} x {h}"); return; } tab.extra.size_asked = Some((w, h)); let (w, h) = (w.to_string(), h.to_string()); self.design_command("slide.size", &[&w, &h, "true"], cx); } /// Enters or leaves the master view. pub(crate) fn masters(&mut self, on: bool, cx: &mut Context) { let Some(tab) = self.active_tab() else { return; }; let id = tab.id; self.run_in( Some(id), if on { "master.edit" } else { "master.close" }, &[], cx, ); self.force_reread(id, cx); } } /// The right column over the active deck. pub struct Panel { pub(crate) deck: WeakEntity, /// Per field: value sent, value read, until the next read. pub(crate) sent: HashMap)>, /// The tab and read the sends belong to. pub(crate) seen: Option<(u64, u64)>, /// The effect picked in the list. pub(crate) effect: Option, } impl Panel { pub fn new(deck: WeakEntity, cx: &mut Context) -> Self { if let Some(deck) = deck.upgrade() { cx.observe(&deck, |_, _, cx| cx.notify()).detach(); } Panel { deck, sent: HashMap::new(), seen: None, effect: None, } } /// The kernel's font families, once read. pub(crate) fn fonts(&self, cx: &App) -> Vec { self.deck .upgrade() .map(|deck| deck.read(cx).fonts.clone()) .unwrap_or_default() } } /// Runs `then` on the deck behind `deck`. pub(crate) fn on_deck( deck: &WeakEntity, cx: &mut App, then: impl FnOnce(&mut Deck, &mut Context), ) { if let Some(deck) = deck.upgrade() { deck.update(cx, then); } } impl Render for Panel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let Some(deck) = self.deck.upgrade() else { return div(); }; let Some(objects) = deck .read(cx) .active_tab() .and_then(|tab| tab.extra.objects.clone()) else { return div(); }; let top = match &objects.selection { Some(selection) => self.object_grid(selection, window, cx).into_any_element(), None => self.design_view(cx), }; let list = (!objects.objects.is_empty()).then(|| { div().px_3().child(DescriptionList::new().stacked().item( Deck::text(cx, "show.objects", &[]), self.object_list(&objects.objects, cx), )) }); div().size_full().child( ScrollArea::new("show-panel-scroll").vertical().size_full().child( div() .flex() .flex_col() .gap_3() .pb_3() .child(top) .children(self.motion_view(objects.selection.is_some(), cx)) .children(list), ), ) } } impl Panel { /// Layout, master, theme and size of the shown slide. fn design_view(&self, cx: &mut Context) -> AnyElement { let label = |key: &str| Deck::text(cx, key, &[]); let Some(deck) = self.deck.upgrade() else { return div().into_any_element(); }; let read = deck.read(cx); let Some(tab) = read.active_tab() else { return div().into_any_element(); }; let Some(design) = tab.extra.design.clone() else { return div().into_any_element(); }; let (masters, size) = (tab.extra.masters, tab.extra.shown_size()); let weak = self.deck.clone(); let theme = Select::new( "show-theme", design .themes .iter() .map(|name| Choice::new(name.clone(), name.clone())), ) .label(label("show.design.theme")) .selected(design.theme.clone()) .on_change({ let weak = weak.clone(); move |value, _, cx| { let value = value.to_string(); on_deck(&weak, cx, |deck, cx| { deck.design_command("slide.theme", &[&value], cx) }) } }); let mut rows = DescriptionList::new(); if !masters { let mut layouts: Vec = LAYOUTS .iter() .map(|n| Choice::new(n.to_string(), label(&format!("show.layout.{n}")))) .collect(); if !LAYOUTS.contains(&design.layout) { layouts.push(Choice::new( design.layout.to_string(), label("show.layout.other"), )); } let layout = Select::new("show-layout", layouts) .label(label("show.design.layout")) .selected(design.layout.to_string()) .on_change({ let weak = weak.clone(); move |value, _, cx| { let value = value.to_string(); on_deck(&weak, cx, |deck, cx| { deck.design_command("slide.layout", &[&value], cx) }) } }); let master = Select::new( "show-master", design .masters .iter() .map(|name| Choice::new(name.clone(), name.clone())), ) .label(label("show.design.master")) .selected(design.master.clone()) .on_change({ let weak = weak.clone(); move |value, _, cx| { let value = value.to_string(); on_deck(&weak, cx, |deck, cx| { deck.design_command("slide.master", &[&value], cx) }) } }); rows = rows .item(label("show.design.layout"), field(layout)) .item(label("show.design.master"), field(master)); } rows = rows.item(label("show.design.theme"), field(theme)); if !masters { rows = size_rows(rows, &weak, size, cx); } let toggle = { let weak = weak.clone(); let key = if masters { "show.masters.close" } else { "show.masters.edit" }; Button::new("show-masters", label(key)) .variant(ButtonVariant::Ghost) .on_click(move |_, _, cx| on_deck(&weak, cx, |deck, cx| deck.masters(!masters, cx))) }; let insert = masters.then(|| { Button::new("show-master-new", label("show.masters.new")) .variant(ButtonVariant::Ghost) .on_click(move |_, _, cx| { on_deck(&weak, cx, |deck, cx| { deck.run_active("master.insert", &[], cx); if let Some(id) = deck.core.active { deck.force_reread(id, cx); } }) }) }); div() .size_full() .p_3() .flex() .flex_col() .gap_3() .child(rows) .child(div().flex().gap_2().child(toggle).children(insert)) .into_any_element() } } fn field(control: impl IntoElement) -> gpui::Div { div().w_full().max_w_40().child(control) } /// The size preset, then width and height in centimetres. fn size_rows( rows: DescriptionList, weak: &WeakEntity, size: (i64, i64), cx: &App, ) -> DescriptionList { let label = |key: &str| Deck::text(cx, key, &[]); let current = preset(size).unwrap_or("custom"); let mut sizes: Vec = SIZES .iter() .map(|(key, _)| Choice::new(*key, label(&format!("show.size.{key}")))) .collect(); if current == "custom" { sizes.push(Choice::new("custom", label("show.size.custom"))); } let picking = weak.clone(); let presets = Select::new("show-size", sizes) .label(label("show.design.size")) .selected(current) .on_change(move |value, _, cx| { let Some((_, size)) = SIZES.iter().find(|(key, _)| *key == value.as_ref()) else { return; }; let size = *size; on_deck(&picking, cx, |deck, cx| deck.resize(size, cx)) }); let (w, h) = size; let side = |id: &'static str, value: i64, width: bool| { let weak = weak.clone(); let name = label(if width { "show.design.width" } else { "show.design.height" }); NumberInput::new(id, value as f64 / 1000.0) .label(name) .range(1.0, 300.0) .step(0.1) .precision(2) .on_commit(move |cm, _, cx| { let value = (cm * 1000.0).round() as i64; // Build on a size still on its way. on_deck(&weak, cx, |deck, cx| { let Some((w, h)) = deck.active_tab().map(|tab| tab.extra.shown_size()) else { return; }; deck.resize(if width { (value, h) } else { (w, value) }, cx) }) }) }; rows.item(label("show.design.size"), field(presets)) .item( label("show.design.width"), div().w_full().max_w_32().child(side("show-width", w, true)), ) .item( label("show.design.height"), div().w_full().max_w_32().child(side("show-height", h, false)), ) } #[cfg(test)] mod tests { #[test] fn sizes_match_presets_by_millimetre() { assert_eq!(super::preset(super::mm100((15875, 8930))), Some("hd")); assert_eq!( super::preset(super::mm100((14401, 10801))), Some("standard") ); assert_eq!(super::preset((10000, 10000)), None); assert_eq!(super::mm100((14400, 10800)), (25400, 19050)); } }