lots of cleanup and some ui tweaks
Some checks are pending
/ test (push) Waiting to run

This commit is contained in:
Chris Cochrun 2025-09-23 06:25:25 -05:00
parent bb1057e950
commit 991feb18c8
11 changed files with 219 additions and 374 deletions

View file

@ -1,157 +0,0 @@
use lexpr::Value;
use strum_macros::EnumString;
#[derive(Debug, Clone, Default, PartialEq, Eq, EnumString)]
pub(crate) enum Symbol {
#[strum(ascii_case_insensitive)]
Slide,
#[strum(ascii_case_insensitive)]
Image,
#[strum(ascii_case_insensitive)]
Text,
#[strum(ascii_case_insensitive)]
Video,
#[strum(ascii_case_insensitive)]
Song,
#[strum(disabled)]
ImageFit(ImageFit),
#[strum(disabled)]
VerseOrder(VerseOrder),
#[strum(disabled)]
#[default]
None,
}
#[derive(Debug, Clone, PartialEq, Eq, EnumString)]
pub(crate) enum Keyword {
ImageFit(ImageFit),
}
#[derive(Debug, Default, Clone, PartialEq, Eq, EnumString)]
pub(crate) enum ImageFit {
#[strum(ascii_case_insensitive)]
#[default]
Cover,
#[strum(ascii_case_insensitive)]
Fill,
#[strum(ascii_case_insensitive)]
Crop,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, EnumString)]
pub(crate) enum VerseOrder {
#[strum(ascii_case_insensitive)]
#[default]
V1,
#[strum(ascii_case_insensitive)]
V2,
#[strum(ascii_case_insensitive)]
V3,
#[strum(ascii_case_insensitive)]
V4,
#[strum(ascii_case_insensitive)]
V5,
#[strum(ascii_case_insensitive)]
V6,
#[strum(ascii_case_insensitive)]
C1,
#[strum(ascii_case_insensitive)]
C2,
#[strum(ascii_case_insensitive)]
C3,
#[strum(ascii_case_insensitive)]
C4,
#[strum(ascii_case_insensitive)]
B1,
#[strum(ascii_case_insensitive)]
B2,
#[strum(ascii_case_insensitive)]
B3,
#[strum(ascii_case_insensitive)]
B4,
#[strum(ascii_case_insensitive)]
O1,
#[strum(ascii_case_insensitive)]
O2,
#[strum(ascii_case_insensitive)]
O3,
#[strum(ascii_case_insensitive)]
O4,
#[strum(ascii_case_insensitive)]
E1,
#[strum(ascii_case_insensitive)]
E2,
#[strum(ascii_case_insensitive)]
I1,
#[strum(ascii_case_insensitive)]
I2,
}
#[derive(Clone, Debug, PartialEq, Eq, EnumString)]
pub(crate) enum SongKeyword {
#[strum(ascii_case_insensitive)]
Title,
#[strum(ascii_case_insensitive)]
Author,
#[strum(ascii_case_insensitive)]
Ccli,
#[strum(ascii_case_insensitive)]
Audio,
#[strum(ascii_case_insensitive)]
Font,
#[strum(ascii_case_insensitive)]
FontSize,
#[strum(ascii_case_insensitive)]
Background,
#[strum(ascii_case_insensitive)]
VerseOrder(VerseOrder),
}
#[derive(Clone, Debug, PartialEq, Eq, EnumString)]
pub(crate) enum ImageKeyword {
#[strum(ascii_case_insensitive)]
Source,
#[strum(ascii_case_insensitive)]
Fit,
}
#[derive(Clone, Debug, Eq, PartialEq, EnumString)]
pub(crate) enum VideoKeyword {
#[strum(ascii_case_insensitive)]
Source,
#[strum(ascii_case_insensitive)]
Fit,
}
pub(crate) fn get_lists(exp: &Value) -> Vec<Value> {
if exp.is_cons() {
exp.as_cons().unwrap().to_vec().0
} else {
vec![]
}
}
#[cfg(test)]
mod test {
// #[test]
// fn test_list() {
// let lisp =
// read_to_string("./test_presentation.lisp").expect("oops");
// // println!("{lisp}");
// let mut parser =
// Parser::from_str_custom(&lisp, Options::elisp());
// for atom in parser.value_iter() {
// match atom {
// Ok(atom) => {
// // println!("{atom}");
// let lists = get_lists(&atom);
// assert_eq!(lists, vec![Value::Null])
// }
// Err(e) => {
// panic!("{e}");
// }
// }
// }
// }
}

View file

@ -1,7 +1,6 @@
pub mod content;
pub mod images;
pub mod kinds;
pub mod lisp;
pub mod model;
pub mod presentations;
pub mod service_items;

View file

@ -9,9 +9,11 @@ use cosmic::app::{Core, Settings, Task};
use cosmic::iced::alignment::Vertical;
use cosmic::iced::keyboard::{Key, Modifiers};
use cosmic::iced::window::{Mode, Position};
use cosmic::iced::{self, event, window, Color, Length, Point};
use cosmic::iced::{
self, event, window, Background as IcedBackground, Border, Length,
};
use cosmic::iced_core::text::Wrapping;
use cosmic::iced_futures::Subscription;
use cosmic::iced_runtime::dnd::DndAction;
use cosmic::iced_widget::{column, row, stack};
use cosmic::theme;
use cosmic::widget::dnd_destination::dnd_destination;
@ -20,8 +22,8 @@ use cosmic::widget::menu::{ItemWidth, KeyBind};
use cosmic::widget::nav_bar::nav_bar_style;
use cosmic::widget::tooltip::Position as TPosition;
use cosmic::widget::{
button, dnd_source, horizontal_space, mouse_area, nav_bar,
search_input, tooltip, vertical_space, RcElementWrapper, Space,
button, horizontal_space, mouse_area, nav_bar, responsive,
search_input, tooltip, vertical_space, Space,
};
use cosmic::widget::{container, text};
use cosmic::widget::{icon, slider};
@ -58,7 +60,7 @@ struct Cli {
watch: bool,
#[arg(short = 'i', long)]
ui: bool,
file: PathBuf,
file: Option<PathBuf>,
}
fn main() -> Result<()> {
@ -111,6 +113,7 @@ struct App {
windows: Vec<window::Id>,
service: Vec<ServiceItem>,
current_item: (usize, usize),
hovered_item: Option<usize>,
presentation_open: bool,
cli_mode: bool,
library: Option<Library>,
@ -134,7 +137,7 @@ enum Message {
File(PathBuf),
OpenWindow,
CloseWindow(Option<window::Id>),
WindowOpened(window::Id, Option<Point>),
WindowOpened(window::Id),
WindowClosed(window::Id),
AddLibrary(Library),
LibraryToggle,
@ -143,10 +146,10 @@ enum Message {
None,
EditorToggle(bool),
ChangeServiceItem(usize),
HoveredServiceItem(Option<usize>),
AddServiceItem(usize, ServiceItem),
AddServiceItemDrop(usize),
AppendServiceItem(ServiceItem),
AddService(Vec<ServiceItem>),
SearchFocus,
Search(String),
CloseSearch,
@ -213,30 +216,36 @@ impl cosmic::Application for App {
windows.push(core.main_window_id().unwrap());
}
let items = match read_to_string(input.file) {
Ok(lisp) => {
let mut service_items = 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);
service_items.append(&mut inner_vector);
let items = if let Some(file) = input.file {
match read_to_string(file) {
Ok(lisp) => {
let mut service_items = 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);
service_items
.append(&mut inner_vector);
}
}
_ => todo!(),
}
_ => todo!(),
service_items
}
Err(e) => {
warn!("Missing file or could not read: {e}");
vec![]
}
service_items
}
Err(e) => {
warn!("Missing file or could not read: {e}");
vec![]
}
} else {
vec![]
};
let items: Vec<ServiceItem> = items
@ -308,6 +317,7 @@ impl cosmic::Application for App {
library_dragged_item: None,
fontdb: Arc::clone(&fontdb),
menu_keys,
hovered_item: None,
};
let mut batch = vec![];
@ -530,23 +540,33 @@ impl cosmic::Application for App {
debug!("Closing window");
Some(Message::CloseWindow(Some(id)))
}
window::Event::Opened {
position, ..
} => {
window::Event::Opened { .. } => {
debug!(?window_event, ?id);
Some(Message::WindowOpened(id, position))
Some(Message::WindowOpened(id))
}
window::Event::Closed => {
debug!("Closed window");
Some(Message::WindowClosed(id))
}
window::Event::FileHovered(file) => {
debug!(?file);
None
}
window::Event::FileDropped(file) => {
debug!(?file);
None
}
_ => None,
}
}
iced::Event::Touch(_touch) => None,
iced::Event::A11y(_id, _action_request) => None,
iced::Event::Dnd(_dnd_event) => None,
iced::Event::PlatformSpecific(_platform_specific) => {
iced::Event::Dnd(_dnd_event) => {
// debug!(?dnd_event);
None
}
iced::Event::PlatformSpecific(platform_specific) => {
debug!(?platform_specific);
None
}
}
@ -676,7 +696,7 @@ impl cosmic::Application for App {
})
}
song_editor::Action::UpdateSong(song) => {
if let Some(library) = &mut self.library {
if let Some(_) = &mut self.library {
self.update(Message::Library(
library::Message::UpdateSong(song),
))
@ -914,9 +934,7 @@ impl cosmic::Application for App {
.set_window_title(format!("window_{count}"), id);
spawn_window.map(|id| {
cosmic::Action::App(Message::WindowOpened(
id, None,
))
cosmic::Action::App(Message::WindowOpened(id))
})
}
Message::CloseWindow(id) => {
@ -926,7 +944,7 @@ impl cosmic::Application for App {
Task::none()
}
}
Message::WindowOpened(id, _) => {
Message::WindowOpened(id) => {
debug!(?id, "Window opened");
if self.cli_mode
|| id > self.core.main_window_id().expect("Cosmic core seems to be missing a main window, was this started in cli mode?")
@ -969,10 +987,6 @@ impl cosmic::Application for App {
self.library = Some(library);
Task::none()
}
Message::AddService(service) => {
self.service = service;
Task::none()
}
Message::None => Task::none(),
Message::EditorToggle(edit) => {
if edit {
@ -988,6 +1002,10 @@ impl cosmic::Application for App {
self.search_id.clone(),
)
}
Message::HoveredServiceItem(index) => {
self.hovered_item = index;
Task::none()
}
Message::ChangeServiceItem(index) => {
if let Some((index, item)) = self
.service
@ -1283,9 +1301,8 @@ where
});
self.windows.push(id);
_ = self.set_window_title("Lumina Presenter".to_owned(), id);
spawn_window.map(|id| {
cosmic::Action::App(Message::WindowOpened(id, None))
})
spawn_window
.map(|id| cosmic::Action::App(Message::WindowOpened(id)))
}
fn add_library(&self) -> Task<Message> {
@ -1309,36 +1326,6 @@ where
}
}
fn add_service(
&self,
items: Vec<ServiceItem>,
fontdb: Arc<fontdb::Database>,
) -> Task<Message> {
Task::perform(
async move {
let items: Vec<ServiceItem> = items
.into_par_iter()
.map(|mut item| {
item.slides = item
.slides
.into_par_iter()
.map(|mut slide| {
text_svg::text_svg_generator(
&mut slide,
Arc::clone(&fontdb),
);
slide
})
.collect();
item
})
.collect();
items
},
|x| cosmic::Action::App(Message::AddService(x)),
)
}
fn process_key_press(
&mut self,
key: Key,
@ -1411,56 +1398,123 @@ where
}
fn service_list(&self) -> Element<Message> {
let list = self
.service
.iter()
.enumerate()
.map(|(index, item)| {
let button = button::standard(item.title.clone())
.leading_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")
},
}
})
// .icon_size(cosmic::theme::spacing().space_l)
.class(cosmic::theme::style::Button::HeaderBar)
// .spacing(cosmic::theme::spacing().space_l)
.height(cosmic::theme::spacing().space_xl)
.width(Length::Fill)
.on_press(Message::ChangeServiceItem(index));
let tooltip = tooltip(button,
text::body(item.kind.to_string()),
TPosition::Right);
dnd_destination(tooltip, vec!["application/service-item".into()])
.data_received_for::<ServiceItem>( move |item| {
if let Some(item) = item {
Message::AddServiceItem(index, item)
} else {
Message::None
}
}).on_finish(move |mime, data, action, x, y| {
debug!(mime, ?data, ?action, x, y);
let Ok(item) = ServiceItem::try_from((data, mime)) else {
return Message::None;
};
debug!(?item);
Message::AddServiceItem(index, item)
})
let list =
self.service.iter().enumerate().map(|(index, item)| {
let icon = match item.kind {
ServiceItemKind::Song(_) => {
icon::from_name("folder-music-symbolic")
}
ServiceItemKind::Video(_) => {
icon::from_name("folder-videos-symbolic")
}
ServiceItemKind::Image(_) => {
icon::from_name("folder-pictures-symbolic")
}
ServiceItemKind::Presentation(_) => {
icon::from_name(
"x-office-presentation-symbolic",
)
}
ServiceItemKind::Content(_) => icon::from_name(
"x-office-presentation-symbolic",
),
};
let title = responsive(|size| {
text::heading(library::elide_text(
&item.title,
size.width,
))
.wrapping(Wrapping::None)
.into()
});
let container = container(
row![icon, title]
.align_y(Vertical::Center)
.spacing(cosmic::theme::spacing().space_xs),
)
.padding(cosmic::theme::spacing().space_s)
.class(cosmic::theme::style::Container::Secondary)
.style(move |t| {
container::Style::default()
.background(IcedBackground::Color(
if self.hovered_item.is_some_and(
|hovered_index| {
index == hovered_index
},
) {
t.cosmic().button.hover.into()
} else {
t.cosmic().button.base.into()
},
))
.border(Border::default().rounded(
t.cosmic().corner_radii.radius_m,
))
})
.width(Length::Fill);
let mouse_area = mouse_area(container)
.on_enter(Message::HoveredServiceItem(Some(
index,
)))
.on_exit(Message::HoveredServiceItem(None))
.on_press(Message::ChangeServiceItem(index));
// let button = button::standard(item.title.clone())
// .leading_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")
// },
// }
// })
// // .icon_size(cosmic::theme::spacing().space_l)
// .class(cosmic::theme::style::Button::HeaderBar)
// // .spacing(cosmic::theme::spacing().space_l)
// .height(cosmic::theme::spacing().space_xl)
// .width(Length::Fill)
// .on_press(Message::ChangeServiceItem(index));
let tooltip = tooltip(
mouse_area,
text::body(item.kind.to_string()),
TPosition::Right,
)
.gap(cosmic::theme::spacing().space_xs);
dnd_destination(
tooltip,
vec![
"application/service-item".into(),
"video/mp4".into(),
],
)
.data_received_for::<ServiceItem>(move |item| {
if let Some(item) = item {
Message::AddServiceItem(index, item)
} else {
Message::None
}
})
.on_finish(move |mime, data, action, x, y| {
debug!(mime, ?data, ?action, x, y);
let Ok(item) =
ServiceItem::try_from((data, mime))
else {
return Message::None;
};
debug!(?item);
Message::AddServiceItem(index, item)
})
.into()
});
let column = column![
@ -1469,6 +1523,7 @@ where
.width(Length::Fill),
iced::widget::horizontal_rule(1),
column(list).spacing(10),
// service::service(&self.service),
dnd_destination(
vertical_space().width(Length::Fill),
vec!["application/service-item".into()]
@ -1494,9 +1549,7 @@ where
]
.padding(10)
.spacing(10);
let container = Container::new(column)
// .height(Length::Fill)
.style(nav_bar_style);
let container = Container::new(column).style(nav_bar_style);
container.center(Length::FillPortion(2)).into()
}

View file

@ -51,7 +51,6 @@ pub struct Library {
enum MenuMessage {
Delete((LibraryKind, i32)),
Open,
None,
}
impl MenuAction for MenuMessage {
@ -63,7 +62,6 @@ impl MenuAction for MenuMessage {
Message::DeleteItem((*kind, *index))
}
MenuMessage::Open => todo!(),
MenuMessage::None => todo!(),
}
}
}
@ -827,7 +825,7 @@ async fn add_db() -> Result<SqlitePool> {
SqlitePool::connect(&db_url).await.into_diagnostic()
}
fn elide_text(text: impl AsRef<str>, width: f32) -> String {
pub fn elide_text(text: impl AsRef<str>, width: f32) -> String {
const CHAR_SIZE: f32 = 8.0;
let text: String = text.as_ref().to_owned();
let text_length = text.len() as f32 * CHAR_SIZE;

View file

@ -1,5 +1,10 @@
use miette::{IntoDiagnostic, Result};
use std::{fs::File, io::BufReader, path::PathBuf, sync::Arc};
use std::{
fs::File,
io::BufReader,
path::PathBuf,
sync::{Arc, LazyLock},
};
use cosmic::{
iced::{
@ -31,7 +36,8 @@ use crate::{
};
const REFERENCE_WIDTH: f32 = 1920.0;
const REFERENCE_HEIGHT: f32 = 1080.0;
static DEFAULT_SLIDE: LazyLock<Slide> =
LazyLock::new(|| Slide::default());
// #[derive(Default, Clone, Debug)]
pub(crate) struct Presenter {
@ -147,14 +153,22 @@ impl Presenter {
let total_slides: usize =
items.iter().fold(0, |a, item| a + item.slides.len());
let slide =
items.get(0).map(|item| item.slides.get(0)).flatten();
let audio = items
.get(0)
.map(|item| item.slides.get(0).map(|slide| slide.audio()))
.flatten()
.flatten();
Self {
current_slide: items[0].slides[0].clone(),
current_slide: slide.unwrap_or(&DEFAULT_SLIDE).clone(),
current_item: 0,
current_slide_index: 0,
absolute_slide_index: 0,
total_slides,
video,
audio: items[0].slides[0].audio(),
audio,
service: items,
video_position: 0.0,
hovered_slide: None,

View file

@ -1,22 +1,19 @@
use std::any::Any;
use cosmic::iced::{self, Size};
use cosmic::iced_core::window;
use cosmic::iced::Size;
use cosmic::iced_core::widget::tree;
use cosmic::{
iced::{
clipboard::dnd::{DndAction, DndEvent, SourceEvent},
event, mouse, overlay, Event, Length, Point, Rectangle,
Vector,
clipboard::dnd::{DndEvent, SourceEvent},
event, mouse, Event, Length, Point, Rectangle, Vector,
},
iced_core::{
self, layout, renderer,
widget::{tree, Tree},
self, image::Renderer, layout, renderer, widget::Tree,
Clipboard, Shell,
},
widget::{container, Id, Widget},
widget::Widget,
Element,
};
use tracing::debug;
use crate::core::service_items::ServiceItem;
@ -133,6 +130,10 @@ impl<Message: Clone + 'static>
layout::atomic(limits, self.width, self.height)
}
fn state(&self) -> iced_core::widget::tree::State {
tree::State::new(State::new())
}
// fn operate(
// &self,
// tree: &mut Tree,
@ -231,6 +232,7 @@ impl<Message: Clone + 'static>
_ => return event::Status::Ignored,
},
Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => {
debug!("canceled");
if state.is_dragging {
if let Some(m) = self.on_cancelled.as_ref() {
shell.publish(m.clone());
@ -241,6 +243,7 @@ impl<Message: Clone + 'static>
return event::Status::Ignored;
}
Event::Dnd(DndEvent::Source(SourceEvent::Finished)) => {
debug!("dropped");
if state.is_dragging {
if let Some(m) = self.on_finish.as_ref() {
shell.publish(m.clone());
@ -250,6 +253,7 @@ impl<Message: Clone + 'static>
}
return event::Status::Ignored;
}
Event::Dnd(event) => debug!(?event),
_ => return event::Status::Ignored,
}
event::Status::Ignored
@ -286,10 +290,8 @@ impl<Message: Clone + 'static>
cursor_position: mouse::Cursor,
viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State>();
for item in self.service {
todo!()
}
// let state = tree.state.downcast_mut::<State>();
for item in self.service {}
}
// fn overlay<'b>(

View file

@ -2,7 +2,7 @@ use std::{io, path::PathBuf, sync::Arc};
use cosmic::{
dialog::file_chooser::{open::Dialog, FileFilter},
iced::{alignment::Vertical, Font, Length},
iced::{alignment::Vertical, Length},
iced_wgpu::graphics::text::cosmic_text::fontdb,
iced_widget::{column, row},
theme,

View file

@ -354,14 +354,6 @@ impl TextSvg {
.height(Length::Fill)
.into()
}
fn text_spans(&self) -> Vec<String> {
self.text
.lines()
.enumerate()
.map(|(i, t)| format!("<tspan x=\"50%\">{t}</tspan>"))
.collect()
}
}
pub fn shadow(
@ -408,27 +400,3 @@ pub fn text_svg_generator(
slide.text_svg = Some(text_svg);
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::TextSvg;
#[test]
fn test_text_spans() {
let mut text = TextSvg::new("yes");
text.text = "This is
multiline
text."
.into();
assert_eq!(
vec![
String::from("<tspan>This is</tspan>"),
String::from("<tspan>multiline</tspan>"),
String::from("<tspan>text.</tspan>"),
],
text.text_spans()
)
}
}