use clap::{command, Parser}; use core::service_items::ServiceItem; use core::slide::*; use core::songs::Song; use crisp::types::Value; use iced::keyboard::{Key, Modifiers}; use iced::theme::{self, Palette}; use iced::widget::tooltip::Position as TPosition; use iced::widget::{ button, horizontal_space, mouse_area, slider, text, text_input, tooltip, vertical_space, Space, }; use iced::widget::{column, row}; use iced::window::{Mode, Position}; use iced::{self, event, window, Length, Padding, Point}; use iced::{color, Subscription}; use iced::{executor, Application, Element}; use iced::{widget::Container, Theme}; use iced::{Settings, Task}; use lisp::parse_lisp; use miette::{miette, Result}; use rayon::prelude::*; use std::collections::BTreeMap; use std::fs::read_to_string; use std::path::PathBuf; use tracing::{debug, level_filters::LevelFilter}; use tracing::{error, warn}; use tracing_subscriber::EnvFilter; use ui::library::{self, Library}; use ui::presenter::{self, Presenter}; use ui::song_editor::{self, SongEditor}; use ui::EditorMode; use crate::ui::widgets::icon; pub mod core; pub mod lisp; pub mod ui; #[derive(Debug, Parser)] #[command(name = "lumina", version, about)] struct Cli { #[arg(short, long)] watch: bool, #[arg(short = 'i', long)] ui: bool, file: PathBuf, } fn main() -> Result<()> { let timer = tracing_subscriber::fmt::time::ChronoLocal::new( "%Y-%m-%d_%I:%M:%S%.6f %P".to_owned(), ); let filter = EnvFilter::builder() .with_default_directive(LevelFilter::WARN.into()) .parse_lossy("lumina=debug"); tracing_subscriber::FmtSubscriber::builder() .pretty() .with_line_number(true) .with_level(true) .with_target(true) .with_env_filter(filter) .with_target(true) .with_timer(timer) .init(); // let settings; // if args.ui { // debug!("main view"); // settings = iced::daemon::Settings::default() // .debug(false) // .is_daemon(true) // .transparent(true); // } else { // debug!("window view"); // settings = Settings::default() // .debug(false) // .no_main_window(true) // .is_daemon(true); // } iced::daemon(App::init, App::update, App::view) .settings(Settings::default()) .subscription(App::subscription) .theme(App::theme) .title(App::title) .run() .map_err(|e| miette!("Invalid things... {}", e)) } struct App { file: PathBuf, presenter: Presenter, windows: BTreeMap, service: Vec, current_item: (usize, usize), presentation_open: bool, cli_mode: bool, library: Option, library_open: bool, library_width: f32, editor_mode: Option, song_editor: SongEditor, searching: bool, library_dragged_item: Option, } #[derive(Debug, Clone)] enum Message { Present(presenter::Message), Library(library::Message), SongEditor(song_editor::Message), File(PathBuf), DndEnter(Vec), DndDrop, OpenWindow, CloseWindow(Option), WindowOpened(window::Id, Option), WindowClosed(window::Id), AddLibrary(Library), LibraryToggle, Quit, Key(Key, Modifiers), None, DndLeave(), EditorToggle(bool), SearchFocus, ChangeServiceItem(usize), AddServiceItem(usize, ServiceItem), AddServiceItemDrop(usize), AppendServiceItem(Option), } #[derive(Debug, Clone)] struct Window { title: String, scale_input: String, current_scale: f64, theme: Theme, } impl Default for Window { fn default() -> Self { Self { title: Default::default(), scale_input: Default::default(), current_scale: Default::default(), theme: Theme::custom( "Snazzy", Palette { background: color!(0x282a36), text: color!(0xe2e4e5), primary: color!(0x57c7ff), success: color!(0x5af78e), warning: color!(0xff9f43), danger: color!(0xff5c57), }, ), } } } const HEADER_SPACE: f32 = 6.0; impl App { const APP_ID: &'static str = "lumina"; fn title(&self, id: window::Id) -> String { self.windows .get(&id) .map(|window| window.title.clone()) .unwrap_or(String::from("Lumina")) } fn init() -> (Self, Task) { debug!("init"); let args = Cli::parse(); let mut batch = vec![]; let mut windows = BTreeMap::new(); if args.ui { let settings = window::Settings { ..Default::default() }; let (id, open) = window::open(settings); batch .push(open.map(|id| Message::WindowOpened(id, None))); let window = Window { title: "Lumina".into(), scale_input: "".into(), current_scale: 1.0, theme: Theme::custom( "Snazzy", Palette { background: color!(0x282a36), text: color!(0xe2e4e5), primary: color!(0x57c7ff), success: color!(0x5af78e), warning: color!(0xff9f43), danger: color!(0xff5c57), }, ), }; windows.insert(id, window); } let items = match read_to_string(args.file) { Ok(lisp) => { let mut slide_vector = vec![]; let lisp = crisp::reader::read(&lisp); match lisp { Value::List(vec) => { // let items = vec // .into_par_iter() // .map(|value| parse_lisp(value)) // .collect(); // slide_vector.append(items); for value in vec { let mut inner_vector = parse_lisp(value); slide_vector.append(&mut inner_vector); } } _ => todo!(), } slide_vector } Err(e) => { warn!("Missing file or could not read: {e}"); vec![] } }; let presenter = Presenter::with_items(items.clone()); let song_editor = SongEditor::new(); // for item in items.iter() { // nav_model.insert().text(item.title()).data(item.clone()); // } // nav_model.activate_position(0); let mut app = App { presenter, service: items, file: PathBuf::default(), windows, presentation_open: false, cli_mode: !args.ui, library: None, library_open: true, library_width: 60.0, editor_mode: None, song_editor, searching: false, current_item: (0, 0), library_dragged_item: None, }; if args.ui { debug!("main view"); // batch.push(app.update_title()) } else { debug!("window view"); batch.push(app.show_window()) }; batch.push(app.add_library()); // batch.push(app.add_service(items)); let batch = Task::batch(batch); (app, batch) } fn nav_bar(&self) -> Option> { // if !self.core().nav_bar_active() { // return None; // } // let nav_model = self.nav_model()?; // let mut nav = iced::widget::nav_bar(nav_model, |id| { // iced::Action::Iced(iced::app::Action::NavBar(id)) // }) // .on_dnd_drop::(|entity, data, action| { // debug!(?entity); // debug!(?data); // debug!(?action); // iced::Action::App(Message::DndDrop) // }) // .on_dnd_enter(|entity, data| { // debug!("entered"); // iced::Action::App(Message::DndEnter(entity, data)) // }) // .on_dnd_leave(|entity| { // debug!("left"); // iced::Action::App(Message::DndLeave(entity)) // }) // .drag_id(DragId::new()) // .on_context(|id| { // iced::Action::Iced( // iced::app::Action::NavBarContext(id), // ) // }) // .context_menu(None) // .into_container() // // XXX both must be shrink to avoid flex layout from ignoring it // .width(Length::Shrink) // .height(Length::Shrink); let list = self.service.iter().enumerate().map(|(index, item)| { let icon = match item.kind { core::kinds::ServiceItemKind::Song(_) => { icon::from_name("folder-music-symbolic") } core::kinds::ServiceItemKind::Video(_) => { icon::from_name("folder-videos-symbolic") } core::kinds::ServiceItemKind::Image(_) => { icon::from_name("folder-pictures-symbolic") } core::kinds::ServiceItemKind::Presentation(_) => { icon::from_name( "x-office-presentation-symbolic", ) } core::kinds::ServiceItemKind::Content(_) => { icon::from_name( "x-office-presentation-symbolic", ) } }; let button = button(row![text(item.title.clone()), icon]) .padding(5) .width(Length::Fill) .on_press(Message::ChangeServiceItem(index)); let tooltip = tooltip( button, text(item.kind.to_string()), TPosition::Right, ); mouse_area(tooltip) .on_release(Message::AddServiceItemDrop(index)) .into() }); let end_index = self.service.len(); let column = column![ text("Service List").center().width(280), column(list).spacing(10), mouse_area(vertical_space(),).on_release( Message::AppendServiceItem( self.library_dragged_item.clone() ) ) ] .padding(10) .spacing(10); let padding = Padding::new(0.0).top(20); let mut container = Container::new(column) // .height(Length::Fill) // .style(nav_bar_style) .padding(padding); // if !self.core().is_condensed() { // container = container.max_width(280); // } Some(container.into()) } fn header_start(&self) -> Vec> { vec![] } fn header_center(&self) -> Vec> { vec![text_input("Search...", "") .on_input(|_| Message::None) .on_submit(Message::None) .width(1200) .into()] } fn header_end(&self) -> Vec> { // let editor_toggle = toggler(self.editor_mode.is_some()) // .label("Editor") // .spacing(10) // .width(Length::Shrink) // .on_toggle(Message::EditorToggle); let presenter_window = self.windows.len() > 1; let presentation_button_text = if self.presentation_open { text("End Presentation") } else { text("Present") }; let row = row![ tooltip( button( row!( Container::new( icon::from_name("document-edit-symbolic") .scale(3) ) .center_y(Length::Fill), text(if self.editor_mode.is_some() { "Present Mode" } else { "Edit Mode" }) ) .spacing(5), ) .on_press(Message::EditorToggle( self.editor_mode.is_none(), )), "Enter Edit Mode", TPosition::Bottom, ), tooltip( button( row!( Container::new( icon::from_name( if self.presentation_open { "window-close-symbolic" } else { "view-presentation-symbolic" } ) .scale(3) ) .center_y(Length::Fill), presentation_button_text ) .spacing(5), ) .on_press({ if self.presentation_open { // Message::CloseWindow( // presenter_window.copied(), // ) Message::None } else { Message::OpenWindow } }), "Start Presentation", TPosition::Bottom, ), tooltip( button( row!( Container::new( icon::from_name("view-list-symbolic") .scale(3) ) .center_y(Length::Fill), text(if self.library_open { "Close Library" } else { "Open Library" }) ) .spacing(5), ) .on_press(Message::LibraryToggle), "Open Library", TPosition::Bottom, ) ] .spacing(HEADER_SPACE) .into(); vec![row] } fn footer(&self) -> Option> { let total_items_text = format!("Total Service Items: {}", self.service.len()); let total_slides_text = format!("Total Slides: {}", self.presenter.total_slides); let row = row![text(total_items_text), text(total_slides_text)] .spacing(10); Some( Container::new(row) .align_right(Length::Fill) .padding([5, 0]) .into(), ) } fn subscription(&self) -> Subscription { event::listen_with(|event, _, id| { // debug!(?event); match event { iced::Event::Keyboard(event) => match event { iced::keyboard::Event::KeyReleased { key, modifiers, .. } => Some(Message::Key(key, modifiers)), _ => None, }, iced::Event::Mouse(_event) => None, iced::Event::Window(window_event) => { match window_event { window::Event::CloseRequested => { debug!("Closing window"); Some(Message::CloseWindow(Some(id))) } window::Event::Opened { position, .. } => { debug!(?window_event, ?id); Some(Message::WindowOpened(id, position)) } window::Event::Closed => { debug!("Closed window"); Some(Message::WindowClosed(id)) } _ => None, } } iced::Event::Touch(_touch) => None, iced::Event::InputMethod(event) => todo!(), } }) } fn dialog(&self) -> Option> { if self.searching { Some(text("hello").into()) } else { None } } fn update(&mut self, message: Message) -> Task { match message { Message::Key(key, modifiers) => { self.process_key_press(key, modifiers) } Message::SongEditor(message) => { // debug!(?message); match self.song_editor.update(message) { song_editor::Action::Task(task) => { task.map(|m| Message::SongEditor(m)) } song_editor::Action::UpdateSong(song) => { if let Some(library) = &mut self.library { self.update(Message::Library( library::Message::UpdateSong(song), )) } else { Task::none() } } song_editor::Action::None => Task::none(), } } Message::Present(message) => { // debug!(?message); if self.presentation_open && let Some(video) = &mut self.presenter.video { video.set_muted(false); } match self.presenter.update(message) { presenter::Action::Task(task) => { task.map(|m| Message::Present(m)) } presenter::Action::None => Task::none(), presenter::Action::NextSlide => { let slide_index = self.current_item.1; let item_index = self.current_item.0; let mut tasks = vec![]; if let Some(item) = self.service.get(item_index) { if item.slides.len() > slide_index + 1 { // let slide_length = item.slides.len(); // debug!( // slide_index, // slide_length, // ?item, // "Slides are longer" // ); let slide = item.slides [slide_index + 1] .clone(); let action = self.presenter.update( presenter::Message::SlideChange( slide, ), ); match action { presenter::Action::Task(task) => { tasks.push(task.map(|m| { Message::Present(m) })) } _ => todo!(), } self.current_item = (item_index, slide_index + 1); Task::batch(tasks) } else { // debug!("Slides are not longer"); self.current_item = (item_index + 1, 0); if let Some(item) = self.service.get(item_index + 1) { let action = self.presenter.update(presenter::Message::SlideChange(item.slides[0].clone())); match action { presenter::Action::Task( task, ) => tasks.push(task.map( |m| Message::Present(m), )), _ => todo!(), } } Task::batch(tasks) } } else { Task::none() } } presenter::Action::PrevSlide => { let slide_index = self.current_item.1; let item_index = self.current_item.0; let mut tasks = vec![]; if let Some(item) = self.service.get(item_index) { if slide_index != 0 { let slide = item.slides [slide_index - 1] .clone(); let action = self.presenter.update( presenter::Message::SlideChange( slide, ), ); match action { presenter::Action::Task(task) => { tasks.push(task.map(|m| { Message::Present(m) })) } _ => todo!(), } self.current_item = (item_index, slide_index - 1); Task::batch(tasks) } else if slide_index == 0 && item_index == 0 { Task::none() } else { // debug!("Change slide to previous items slides"); let previous_item_slides_length = if let Some(item) = self .service .get(item_index - 1) { item.slides.len() } else { 0 }; self.current_item = ( item_index - 1, previous_item_slides_length - 1, ); if let Some(item) = self.service.get(item_index - 1) { let action = self.presenter.update(presenter::Message::SlideChange(item.slides[previous_item_slides_length - 1].clone())); match action { presenter::Action::Task( task, ) => tasks.push(task.map( |m| Message::Present(m), )), _ => todo!(), } } Task::batch(tasks) } } else { Task::none() } } } } Message::Library(message) => { let mut song = Song::default(); if let Some(library) = &mut self.library { match library.update(message) { library::Action::OpenItem(None) => { return Task::none(); } library::Action::Task(task) => { return task.map(|message| { Message::Library(message) }); } library::Action::None => return Task::none(), library::Action::OpenItem(Some(( kind, index, ))) => { debug!( "Should get song at index: {:?}", index ); let Some(lib_song) = library.get_song(index) else { return Task::none(); }; self.editor_mode = Some(kind.into()); song = lib_song.to_owned(); debug!( "Should change songs to: {:?}", song ); } library::Action::DraggedItem( service_item, ) => { debug!("hi"); self.library_dragged_item = Some(service_item); // self.nav_model // .insert() // .text(service_item.title.clone()) // .data(service_item); } } } self.update(Message::SongEditor( song_editor::Message::ChangeSong(song), )) } Message::File(file) => { self.file = file; Task::none() } Message::OpenWindow => self.show_window(), Message::CloseWindow(id) => { if let Some(id) = id { window::close(id) } else { Task::none() } } Message::WindowOpened(id, _) => { debug!(?id, "Window opened"); let main_id = self.windows.first_key_value().unwrap().0; if self.cli_mode || &id > main_id { self.presentation_open = true; if let Some(video) = &mut self.presenter.video { video.set_muted(false); } window::set_mode(id, Mode::Fullscreen) } else { Task::none() } } Message::WindowClosed(id) => { warn!("Closing window: {id}"); self.windows.remove(&id); // This closes the app if using the cli example if self.windows.is_empty() { self.update(Message::Quit) } else { self.presentation_open = false; if let Some(video) = &mut self.presenter.video { video.set_muted(true); } Task::none() } } Message::LibraryToggle => { self.library_open = !self.library_open; Task::none() } Message::Quit => iced::exit(), Message::DndEnter(data) => { debug!(?data); Task::none() } Message::DndDrop => { // debug!(?entity); // debug!(?action); // debug!(?service_item); if let Some(library) = &self.library && let Some((lib, item)) = library.dragged_item { // match lib { // core::model::LibraryKind::Song => , // core::model::LibraryKind::Video => todo!(), // core::model::LibraryKind::Image => todo!(), // core::model::LibraryKind::Presentation => todo!(), // } let item = library.get_song(item).unwrap(); let item = ServiceItem::from(item); } Task::none() } Message::AddLibrary(library) => { self.library = Some(library); Task::none() } Message::None => Task::none(), Message::DndLeave() => { // debug!(?entity); Task::none() } Message::EditorToggle(edit) => { if edit { self.editor_mode = Some(EditorMode::Song); } else { self.editor_mode = None; } Task::none() } Message::SearchFocus => { self.searching = true; Task::none() } Message::ChangeServiceItem(index) => { if let Some((index, item)) = self .service .iter() .enumerate() .find(|(id, _)| index == *id) && let Some(slide) = item.slides.first() { self.current_item = (index, 0); self.presenter.update( presenter::Message::SlideChange( slide.clone(), ), ); } Task::none() } Message::AddServiceItem(index, item) => { self.service.insert(index, item); Task::none() } Message::AddServiceItemDrop(index) => { if let Some(item) = &self.library_dragged_item { self.service.insert(index, item.clone()); } Task::none() } Message::AppendServiceItem(item) => { if let Some(item) = item { self.service.push(item); } Task::none() } } } // Main window view fn view(&self, window: window::Id) -> Element { if window == *self.windows.first_key_value().unwrap().0 { self.main_view() } else { self.view_presenter() } } fn main_view(&self) -> Element { let icon_left = icon::from_name("arrow-left"); let icon_right = icon::from_name("arrow-right"); let video_range = self.presenter.video.as_ref().map_or_else( || 0.0, |video| video.duration().as_secs_f32(), ); let video_button_icon = if let Some(video) = &self.presenter.video { let (icon_name, tt) = if video.paused() { ("media-play", "Play") } else { ("media-pause", "Pause") }; tooltip( button(icon::from_name(icon_name)).on_press( Message::Present(presenter::Message::StartVideo), ), tt, TPosition::FollowCursor, ) } else { tooltip( button(icon::from_name("media-play")).on_press( Message::Present(presenter::Message::StartVideo), ), "Play", TPosition::FollowCursor, ) }; let slide_preview = column![ Space::with_height(Length::Fill), Container::new( self.presenter.view_preview().map(Message::Present), ) .height(250) .align_bottom(Length::Fill), Container::new(if self.presenter.video.is_some() { row![ video_button_icon, Container::new( slider( 0.0..=video_range, self.presenter.video_position, |pos| { Message::Present( presenter::Message::VideoPos(pos), ) } ) .step(0.1) ) .center_x(Length::Fill) .padding([7, 0]) ] .padding(5) } else { row![] }) .center_x(Length::Fill), Space::with_height(Length::Fill), ] .spacing(3); let library = if self.library_open { Container::new(if let Some(library) = &self.library { library.view().map(Message::Library) } else { Space::new(0, 0).into() }) // .style(nav_bar_style) .center(Length::FillPortion(2)) } else { Container::new(horizontal_space().width(0)) }; let song_editor = self.song_editor.view().map(Message::SongEditor); let row = row![ Container::new(tooltip( button(icon_left.size(128)).width(128).on_press( Message::Present(presenter::Message::PrevSlide) ), "Previous Slide", TPosition::FollowCursor )) .center_y(Length::Fill) .align_right(Length::FillPortion(1)), Container::new(slide_preview) .center_y(Length::Fill) .width(Length::FillPortion(3)), Container::new(tooltip( button(icon_right.size(128)).width(128).on_press( Message::Present(presenter::Message::NextSlide) ), "Next Slide", TPosition::FollowCursor )) .center_y(Length::Fill) .align_left(Length::FillPortion(1)), library ] .width(Length::Fill) .height(Length::Fill) .spacing(20); let column = column![ Container::new(row).center_y(Length::Fill), Container::new( self.presenter.preview_bar().map(Message::Present) ) .clip(true) .width(Length::Fill) .center_y(180) ]; if let Some(_editor) = &self.editor_mode { Element::from(song_editor) } else { Element::from(column) } } // View for presentation fn view_presenter(&self) -> Element { self.presenter.view().map(Message::Present) } // fn update_title(&mut self) -> Task { // let window_title = "Lumina"; // self.windows.first_key_value().unwrap().1.title = // window_title.to_string(); // Task::none() // } fn show_window(&mut self) -> Task { let (id, spawn_window) = window::open(window::Settings { position: Position::Centered, exit_on_close_request: true, decorations: false, fullscreen: true, ..Default::default() }); let window = Window { title: "Presentation".into(), scale_input: "".into(), current_scale: 1.0, theme: self.theme(id), }; self.windows.insert(id, window); spawn_window.map(|id| Message::WindowOpened(id, None)) } fn add_library(&mut self) -> Task { Task::perform(async move { Library::new().await }, |x| { Message::AddLibrary(x) }) } // fn add_service( // &mut self, // items: Vec, // ) -> Task { // Task::perform( // async move { // for item in items { // debug!(?item, "Item to be appended"); // let slides = item.to_slides().unwrap_or(vec![]); // map.insert(item, slides); // } // let len = map.len(); // debug!(len, "to be append: "); // map // }, // |x| { // let len = x.len(); // debug!(len, "to append: "); // iced::Action::App(Message::AppendService(x)) // }, // ) // } fn process_key_press( &mut self, key: Key, modifiers: Modifiers, ) -> Task { debug!(?key, ?modifiers); if self.editor_mode.is_some() { return Task::none(); } if self.song_editor.editing() { return Task::none(); } match (key, modifiers) { ( Key::Named(iced::keyboard::key::Named::ArrowRight), _, ) => self.update(Message::Present( presenter::Message::NextSlide, )), ( Key::Named(iced::keyboard::key::Named::ArrowLeft), _, ) => self.update(Message::Present( presenter::Message::PrevSlide, )), (Key::Named(iced::keyboard::key::Named::Space), _) => { self.update(Message::Present( presenter::Message::NextSlide, )) } (Key::Character(k), _) if k == *"j" || k == *"l" => self .update(Message::Present( presenter::Message::NextSlide, )), (Key::Character(k), _) if k == *"k" || k == *"h" => self .update(Message::Present( presenter::Message::PrevSlide, )), (Key::Character(k), _) if k == *"q" => { self.update(Message::Quit) } _ => Task::none(), } } fn theme(&self, _window: window::Id) -> Theme { Theme::custom( "Snazzy", Palette { background: color!(0x282a36), text: color!(0xe2e4e5), primary: color!(0x57c7ff), success: color!(0x5af78e), warning: color!(0xff9f43), danger: color!(0xff5c57), }, ) } } #[cfg(test)] mod test { fn test_slide() -> String { let slide = r#"(slide (image :source "./somehting.jpg" :fill cover (text "Something cooler" :font-size 50)))"#; String::from(slide) } // #[test] // fn test_lisp() { // let slide = test_slide(); // if let Ok(data) = lexpr::parse::from_str_elisp(slide.as_str()) { // assert_eq!(slide, data) // } else { // assert!(false) // } // } }