diff --git a/res/chad.png b/res/chad.png new file mode 100644 index 0000000..44f21f3 Binary files /dev/null and b/res/chad.png differ diff --git a/res/list-add-above.svg b/res/list-add-above.svg new file mode 100644 index 0000000..27f02ba --- /dev/null +++ b/res/list-add-above.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/res/list-add-below.svg b/res/list-add-below.svg new file mode 100644 index 0000000..99adb9e --- /dev/null +++ b/res/list-add-below.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/res/split-above.svg b/res/split-above.svg new file mode 100644 index 0000000..b105436 --- /dev/null +++ b/res/split-above.svg @@ -0,0 +1,54 @@ + + + + + + diff --git a/res/split-below.svg b/res/split-below.svg new file mode 100644 index 0000000..072feb7 --- /dev/null +++ b/res/split-below.svg @@ -0,0 +1,60 @@ + + + + + + diff --git a/src/ui/presentation_editor.rs b/src/ui/presentation_editor.rs index 8088bea..3499cec 100644 --- a/src/ui/presentation_editor.rs +++ b/src/ui/presentation_editor.rs @@ -1,15 +1,20 @@ -use std::{io, path::Path, path::PathBuf}; +use std::{ + collections::HashMap, + io, + path::{Path, PathBuf}, +}; use crate::core::presentations::Presentation; use cosmic::{ Element, Task, dialog::file_chooser::{FileFilter, open::Dialog}, - iced::{ContentFit, Length, alignment::Vertical}, + iced::{Background, ContentFit, Length, alignment::Vertical}, iced_widget::{column, row}, theme, widget::{ - self, Space, button, container, horizontal_space, icon, - image::Handle, scrollable, text, text_input, + self, Space, button, container, context_menu, + horizontal_space, icon, image::Handle, menu, mouse_area, + scrollable, text, text_input, }, }; use miette::IntoDiagnostic; @@ -26,6 +31,8 @@ pub struct PresentationEditor { current_slide_index: Option, title: String, editing: bool, + hovered_slide: Option, + context_menu_id: Option, } pub enum Action { @@ -46,6 +53,28 @@ pub enum Message { None, ChangePresentationFile(Presentation), AddSlides(Option>), + ChangeSlide(usize), + HoverSlide(Option), + ContextMenu(usize), + SplitBefore, + SplitAfter, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MenuAction { + SplitBefore, + SplitAfter, +} + +impl menu::Action for MenuAction { + type Message = Message; + + fn message(&self) -> Self::Message { + match self { + MenuAction::SplitBefore => Message::SplitBefore, + MenuAction::SplitAfter => Message::SplitAfter, + } + } } impl PresentationEditor { @@ -59,6 +88,8 @@ impl PresentationEditor { current_slide_index: None, page_count: None, slides: None, + hovered_slide: None, + context_menu_id: None, } } pub fn update(&mut self, message: Message) -> Action { @@ -179,21 +210,15 @@ impl PresentationEditor { doc.load_page(previous_index).ok()?; let matrix = Matrix::IDENTITY; let colorspace = Colorspace::device_rgb(); - let Ok(pixmap) = page + let pixmap = page .to_pixmap( &matrix, &colorspace, true, true, ) - .into_diagnostic() - else { - error!( - "Can't turn this page into pixmap" - ); - return None; - }; - debug!(?pixmap); + .ok()?; + Some(Handle::from_rgba( pixmap.width(), pixmap.height(), @@ -202,6 +227,50 @@ impl PresentationEditor { }); self.current_slide_index = Some(previous_index); } + Message::ChangeSlide(index) => { + self.current_slide = + self.document.as_ref().and_then(|doc| { + let page = + doc.load_page(index as i32).ok()?; + let matrix = Matrix::IDENTITY; + let colorspace = Colorspace::device_rgb(); + let pixmap = page + .to_pixmap( + &matrix, + &colorspace, + true, + true, + ) + .ok()?; + + Some(Handle::from_rgba( + pixmap.width(), + pixmap.height(), + pixmap.samples().to_vec(), + )) + }); + self.current_slide_index = Some(index as i32); + } + Message::HoverSlide(slide) => { + self.hovered_slide = slide; + } + Message::ContextMenu(index) => { + self.context_menu_id = Some(index as i32); + } + Message::SplitBefore => { + if let Some(index) = self.context_menu_id { + debug!("split before {index}"); + } else { + error!("split before no index"); + } + } + Message::SplitAfter => { + if let Some(index) = self.context_menu_id { + debug!("split after {index}"); + } else { + error!("split after no index"); + } + } } Action::None } @@ -212,32 +281,75 @@ impl PresentationEditor { widget::image(slide) .content_fit(ContentFit::ScaleDown), ) + .style(|_| { + container::background(Background::Color( + cosmic::iced::Color::WHITE, + )) + }) } else { container(Space::new(0, 0)) }; - let pdf_pages: Vec> = - if let Some(pages) = &self.slides { - pages - .iter() - .map(|page| { - let image = widget::image(page) - .height(theme::spacing().space_xxxl * 3) - .content_fit(ContentFit::ScaleDown); - container(image).into() - }) - .collect() - } else { - vec![horizontal_space().into()] - }; - let pages_column = container(scrollable( - column(pdf_pages) - .spacing(theme::active().cosmic().space_xs()) - .padding(theme::spacing().space_l), - )) + let pdf_pages: Vec> = if let Some(pages) = + &self.slides + { + pages + .iter() + .enumerate() + .map(|(index, page)| { + let image = widget::image(page) + .height(theme::spacing().space_xxxl * 3) + .content_fit(ContentFit::ScaleDown); + let slide = container(image).style(|_| { + container::background(Background::Color( + cosmic::iced::Color::WHITE, + )) + }); + let clickable_slide = container( + mouse_area(slide) + .on_enter(Message::HoverSlide(Some( + index as i32, + ))) + .on_exit(Message::HoverSlide(None)) + .on_right_press(Message::ContextMenu( + index, + )) + .on_press(Message::ChangeSlide(index)), + ) + .padding(theme::spacing().space_m) + .clip(true) + .class( + if let Some(hovered_index) = + self.hovered_slide + { + if index as i32 == hovered_index { + theme::Container::Primary + } else { + theme::Container::Card + } + } else { + theme::Container::Card + }, + ); + clickable_slide.into() + }) + .collect() + } else { + vec![horizontal_space().into()] + }; + let pages_column = container( + self.context_menu( + scrollable( + column(pdf_pages) + .spacing(theme::active().cosmic().space_xs()) + .padding(theme::spacing().space_xs), + ) + .into(), + ), + ) .class(theme::Container::Card); let main_row = row![ pages_column, - presentation.center(Length::FillPortion(2)) + container(presentation).center(Length::FillPortion(2)) ] .spacing(theme::spacing().space_xxl); let control_buttons = row![ @@ -279,6 +391,44 @@ impl PresentationEditor { self.editing } + fn context_menu<'b>( + &self, + items: Element<'b, Message>, + ) -> Element<'b, Message> { + if self.context_menu_id.is_some() { + let before_icon = + icon::from_path("./res/split-above.svg".into()) + .symbolic(true); + let after_icon = + icon::from_path("./res/split-below.svg".into()) + .symbolic(true); + let menu_items = vec![ + menu::Item::Button( + "Spit Before", + Some(before_icon), + MenuAction::SplitBefore, + ), + menu::Item::Button( + "Split After", + Some(after_icon), + MenuAction::SplitAfter, + ), + ]; + let context_menu = context_menu( + items, + self.context_menu_id.map_or_else( + || None, + |_| { + Some(menu::items(&HashMap::new(), menu_items)) + }, + ), + ); + Element::from(context_menu) + } else { + items + } + } + fn update_entire_presentation( &mut self, presentation: &Presentation, @@ -296,17 +446,13 @@ impl PresentationEditor { let page = doc.load_page(0).ok()?; let matrix = Matrix::IDENTITY; let colorspace = Colorspace::device_rgb(); - let Ok(pixmap) = page + let pixmap = page .to_pixmap(&matrix, &colorspace, true, true) - .into_diagnostic() - else { - error!("Can't turn this page into pixmap"); - return None; - }; - debug!(?pixmap); + .ok()?; + Some(Handle::from_rgba( - pixmap.width() / 3, - pixmap.height() / 3, + pixmap.width(), + pixmap.height(), pixmap.samples().to_vec(), )) }); @@ -329,16 +475,12 @@ async fn get_all_pages( pages .filter_map(|page| { let page = page.ok()?; - let matrix = Matrix::IDENTITY; let colorspace = Colorspace::device_rgb(); - let Ok(pixmap) = page + let pixmap = page .to_pixmap(&matrix, &colorspace, true, true) - .into_diagnostic() - else { - error!("Can't turn this page into pixmap"); - return None; - }; + .ok()?; + Some(Handle::from_rgba( pixmap.width(), pixmap.height(),