Working to actually create an iced lumina

This commit is contained in:
Chris Cochrun 2024-10-30 10:34:13 -05:00
parent 0121dfa8f3
commit 084ce2fc32
8 changed files with 3928 additions and 674 deletions

View file

@ -1,91 +1,385 @@
use iced::widget::{button, column, row, Button, Column, Container, Row, Text};
use iced::{executor, Alignment, Length, Rectangle, Renderer};
use iced::{Application, Command, Element, Settings, Theme};
use iced_aw::native::TabBar;
use iced_aw::split::Axis;
use iced_aw::{split, Split};
use clap::{command, Parser};
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::window::Position;
use cosmic::iced::{self, event, window, ContentFit, Font, Length, Point};
use cosmic::iced_core::id;
use cosmic::iced_widget::{stack, text};
use cosmic::widget::{button, image, nav_bar};
use cosmic::{executor, Also, Application, ApplicationExt, Element};
use cosmic::{widget::Container, Theme};
use lexpr::{parse, Value};
use miette::{miette, Result};
use std::collections::HashMap;
use std::fs::read_to_string;
use std::path::PathBuf;
use tracing::error;
use tracing::{debug, level_filters::LevelFilter};
use tracing_subscriber::EnvFilter;
fn main() -> iced::Result {
std::env::set_var("WINIT_UNIX_BACKEND", "wayland");
App::run(Settings::default())
pub mod slide;
use slide::*;
#[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 args = Cli::parse();
let settings;
if args.ui {
settings = Settings::default().debug(false);
} else {
settings = Settings::default().debug(false).no_main_window(true);
}
Ok(cosmic::app::run::<App>(settings, args).map_err(|e| miette!("Invalid things... {}", e))?)
}
fn theme(_state: &App) -> Theme {
Theme::dark()
}
#[derive(Default)]
struct App {
divider_position: Option<u16>,
core: Core,
nav_model: nav_bar::Model,
file: PathBuf,
windows: Vec<window::Id>,
}
struct Window {
id: id::Id,
value: String,
}
#[derive(Debug, Clone)]
enum Message {
OnVerResize(u16),
Enchant(String),
File(PathBuf),
OpenWindow,
CloseWindow(window::Id),
WindowOpened(window::Id, Option<Point>),
WindowClosed(window::Id),
}
impl Application for App {
impl cosmic::Application for App {
type Executor = executor::Default;
type Flags = ();
type Flags = Cli;
type Message = Message;
type Theme = Theme;
fn new(_flags: ()) -> (App, Command<Self::Message>) {
(
App {
divider_position: None,
},
Command::none(),
)
const APP_ID: &'static str = "org.chriscochrun.Lumina";
fn core(&self) -> &Core {
&self.core
}
fn title(&self) -> String {
String::from("A cool application")
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
fn init(core: Core, input: Self::Flags) -> (Self, Task<Self::Message>) {
debug!("init");
let mut nav_model = nav_bar::Model::default();
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::OnVerResize(position) => self.divider_position = Some(position),
nav_model.insert().text("Preview").data("Preview");
nav_model.activate_position(0);
let mut windows = vec![];
if input.ui {
windows.push(core.main_window_id().unwrap());
}
let mut app = App {
core,
nav_model,
file: input.file,
windows,
};
Command::none()
let command;
if input.ui {
command = app.update_title()
} else {
command = app.show_window()
};
(app, command)
}
/// Allows COSMIC to integrate with your application's [`nav_bar::Model`].
fn nav_model(&self) -> Option<&nav_bar::Model> {
Some(&self.nav_model)
}
/// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Self::Message> {
self.nav_model.activate(id);
self.update_title()
}
fn header_start(&self) -> Vec<Element<Self::Message>> {
vec![button::standard("Open Window")
.on_press(Message::OpenWindow)
.into()]
}
fn header_center(&self) -> Vec<Element<Self::Message>> {
vec![]
}
fn header_end(&self) -> Vec<Element<Self::Message>> {
vec![]
}
fn subscription(&self) -> cosmic::iced_futures::Subscription<Self::Message> {
event::listen_with(|event, _, id| {
if let iced::Event::Window(window_event) = event {
match window_event {
window::Event::CloseRequested => Some(Message::CloseWindow(id)),
window::Event::Opened { position, .. } => {
debug!(?window_event, ?id);
Some(Message::WindowOpened(id, position))
}
window::Event::Closed => Some(Message::WindowClosed(id)),
_ => None,
}
} else {
None
}
})
}
fn update(&mut self, message: Message) -> cosmic::Task<cosmic::app::Message<Message>> {
match message {
Message::Enchant(slide) => {
debug!(slide);
Task::none()
}
Message::File(file) => {
self.file = file;
Task::none()
}
Message::OpenWindow => {
let count = self.windows.len() + 1;
let (id, spawn_window) = window::open(window::Settings {
position: Position::Centered,
exit_on_close_request: count % 2 == 0,
decorations: false,
..Default::default()
});
self.windows.push(id);
_ = self.set_window_title(format!("window_{}", count), id);
spawn_window.map(|id| cosmic::app::Message::App(Message::WindowOpened(id, None)))
}
Message::CloseWindow(id) => window::close(id),
Message::WindowOpened(id, _) => {
debug!(?id, "Window opened");
if let Some(window) = self.windows.get(&id) {
cosmic::widget::text_input::focus(window.id.clone())
} else {
Task::none()
}
}
Message::WindowClosed(id) => {
let window = self.windows.iter().position(|w| *w == id).unwrap();
self.windows.remove(window);
Task::none()
}
}
}
// Main window view
fn view(&self) -> Element<Message> {
let top: _ = Container::new(
row(vec![
Button::<Message, Renderer>::new("Edit")
.height(Length::Fill)
.padding(10)
.into(),
Button::new("Present")
.height(Length::Fill)
.padding(10)
.into(),
Button::new("Close Library")
.height(Length::Fill)
.padding(10)
.into(),
])
.width(Length::Fill)
.height(Length::Fill)
.align_items(Alignment::End),
);
let first = Container::new(Text::new("First"))
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y();
let second = Container::new(Text::new("Second"))
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y();
Split::new(
first,
second,
self.divider_position,
Axis::Vertical,
Message::OnVerResize,
)
.into()
let text = text!("This is frodo").size(20);
let text = Container::new(text).center(Length::Fill);
let image =
Container::new(image("/home/chris/pics/frodo.jpg").content_fit(ContentFit::Cover))
.center(Length::FillPortion(2));
let stack = stack!(image, text).width(Length::Fill).height(Length::Fill);
stack.into()
}
fn theme(&self) -> Self::Theme {
Theme::Dark
// View for presentation
fn view_window(&self, id: window::Id) -> Element<Message> {
if let Some(_window) = self.windows.get(&id) {}
let text = text!("This is frodo").size(50);
let text = Container::new(text).center(Length::Fill);
let image =
Container::new(image("/home/chris/pics/frodo.jpg").content_fit(ContentFit::Cover))
.center(Length::Fill);
let stack = stack!(image, text).width(Length::Fill).height(Length::Fill);
stack.into()
}
}
impl App
where
Self: cosmic::Application,
{
fn active_page_title(&mut self) -> &str {
// self.nav_model
// .text(self.nav_model.active())
// .unwrap_or("Unknown Page")
"Lumina"
}
fn update_title(&mut self) -> Task<Message> {
let header_title = self.active_page_title().to_owned();
let window_title = format!("{header_title} — Lumina");
// self.set_header_title(header_title);
if let Some(id) = self.core.main_window_id() {
self.set_window_title(window_title, id)
} else {
Task::none()
}
}
fn show_window(&mut self) -> Task<Message> {
let (id, task) = window::open(window::Settings {
position: Position::Centered,
exit_on_close_request: true,
decorations: false,
..Default::default()
});
task.map(|id| cosmic::app::Message::App(Message::WindowOpened(id, None)))
}
}
fn test_slide<'a>() -> Element<'a, Message> {
if let Ok(slide) = SlideBuilder::new()
.background(PathBuf::from("/home/chris/pics/frodo.jpg"))
.unwrap()
.text("This is a frodo")
.text_alignment(TextAlignment::TopCenter)
.font_size(50)
.font("Quicksand")
.build()
{
let font = Font::with_name("Noto Sans");
let stack = stack!(
image(slide.background()),
text(slide.text()).size(slide.font_size() as u16).font(font)
);
stack.into()
} else {
text("Slide is broken").into()
}
}
// fn build_slide<'a>(
// lisp_data: &Value,
// layer: LayerContainer<'a, Message, Renderer>,
// ) -> LayerContainer<'a, Message, Renderer> {
// let current_symbol;
// // let current_element;
// let slide_builder = SlideBuilder::new();
// for atom in lisp_data.list_iter().unwrap() {
// match atom {
// Value::Symbol(symbol) => {
// let symbol = atom.as_symbol().unwrap();
// debug!(symbol);
// match symbol {
// "slide" => {
// current_symbol = "slide";
// debug!("I am a slide");
// return layer;
// }
// "song" => {
// current_symbol = "song";
// debug!("I am a song");
// return layer;
// }
// "image" => {
// current_symbol = "image";
// debug!("I am an image");
// return layer;
// }
// "text" => {
// current_symbol = "text";
// debug!("I am some text");
// return layer;
// }
// _ => {
// error!("We shouldn't get here");
// return layer;
// }
// }
// return layer;
// }
// Value::Keyword(keyword) => {
// debug!(keyword);
// return layer;
// }
// Value::Cons(cons) => {
// debug!(?cons);
// let stack = build_slide(cons.car(), layer);
// return stack;
// }
// Value::String(string) => {
// debug!(string);
// return layer;
// }
// Value::Bool(b) => {
// debug!(b);
// return layer;
// }
// Value::Number(int) => {
// debug!(?int);
// return layer;
// }
// _ => {
// debug!("idk");
// return layer;
// }
// }
// }
// layer
// }
// fn create_image(item: Value) -> Element<Message> {
// // We expect this value to look like (image :source "./something.jpg")
// todo!()
// }
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
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()) {
// println!("{data:?}");
// assert_eq!(slide, data)
// } else {
// assert!(false)
// }
// }
}

404
src/slide.rs Normal file
View file

@ -0,0 +1,404 @@
use lexpr::{parse::from_str_elisp, Value};
use miette::{miette, IntoDiagnostic, Result};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fmt::Display,
path::{Path, PathBuf},
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum TextAlignment {
TopLeft,
TopCenter,
TopRight,
MiddleLeft,
#[default]
MiddleCenter,
MiddleRight,
BottomLeft,
BottomCenter,
BottomRight,
}
impl From<Value> for TextAlignment {
fn from(value: Value) -> Self {
if value == Value::Symbol("middle-center".into()) {
Self::MiddleCenter
} else {
Self::TopCenter
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Background {
pub path: PathBuf,
pub kind: BackgroundKind,
}
impl TryFrom<String> for Background {
type Error = ParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let value = value.trim_start_matches("file://");
let path = PathBuf::from(value);
if !path.exists() {
return Err(ParseError::DoesNotExist);
}
let extension = value.rsplit_once('.').unwrap_or_default();
match extension.1 {
"jpg" | "png" | "webp" | "html" => Ok(Self {
path,
kind: BackgroundKind::Image,
}),
"mp4" | "mkv" | "webm" => Ok(Self {
path,
kind: BackgroundKind::Video,
}),
_ => Err(ParseError::NonBackgroundFile),
}
}
}
impl TryFrom<PathBuf> for Background {
type Error = ParseError;
fn try_from(value: PathBuf) -> Result<Self, Self::Error> {
let extension = value
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default();
match extension {
"jpg" | "png" | "webp" | "html" => Ok(Self {
path: value,
kind: BackgroundKind::Image,
}),
"mp4" | "mkv" | "webm" => Ok(Self {
path: value,
kind: BackgroundKind::Video,
}),
_ => Err(ParseError::NonBackgroundFile),
}
}
}
impl TryFrom<&str> for Background {
type Error = ParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(Self::try_from(String::from(value))?)
}
}
impl TryFrom<&Path> for Background {
type Error = ParseError;
fn try_from(value: &Path) -> Result<Self, Self::Error> {
Ok(Self::try_from(PathBuf::from(value))?)
}
}
#[derive(Debug)]
pub enum ParseError {
NonBackgroundFile,
DoesNotExist,
}
impl std::error::Error for ParseError {}
impl Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self {
Self::NonBackgroundFile => "The file is not a recognized image or video type",
Self::DoesNotExist => "This file doesn't exist",
};
write!(f, "Error: {message}")
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum BackgroundKind {
#[default]
Image,
Video,
}
impl From<String> for BackgroundKind {
fn from(value: String) -> Self {
if value == "image" {
BackgroundKind::Image
} else {
BackgroundKind::Video
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Slide {
id: i32,
background: Background,
text: String,
font: String,
font_size: i32,
text_alignment: TextAlignment,
video_loop: bool,
video_start_time: f32,
video_end_time: f32,
}
impl Slide {
pub fn background(&self) -> &PathBuf {
&self.background.path
}
pub fn text(&self) -> String {
self.text.clone()
}
pub fn font_size(&self) -> i32 {
self.font_size
}
pub fn font(&self) -> String {
self.font.clone()
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct SlideBuilder {
background: Option<Background>,
text: Option<String>,
font: Option<String>,
font_size: Option<i32>,
text_alignment: Option<TextAlignment>,
video_loop: Option<bool>,
video_start_time: Option<f32>,
video_end_time: Option<f32>,
}
impl SlideBuilder {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn background(mut self, background: PathBuf) -> Result<Self, ParseError> {
let background = Background::try_from(background)?;
let _ = self.background.insert(background);
Ok(self)
}
pub(crate) fn text(mut self, text: impl Into<String>) -> Self {
let _ = self.text.insert(text.into());
self
}
pub(crate) fn font(mut self, font: impl Into<String>) -> Self {
let _ = self.font.insert(font.into());
self
}
pub(crate) fn font_size(mut self, font_size: i32) -> Self {
let _ = self.font_size.insert(font_size);
self
}
pub(crate) fn text_alignment(mut self, text_alignment: TextAlignment) -> Self {
let _ = self.text_alignment.insert(text_alignment);
self
}
pub(crate) fn video_loop(mut self, video_loop: bool) -> Self {
let _ = self.video_loop.insert(video_loop);
self
}
pub(crate) fn video_start_time(mut self, video_start_time: f32) -> Self {
let _ = self.video_start_time.insert(video_start_time);
self
}
pub(crate) fn video_end_time(mut self, video_end_time: f32) -> Self {
let _ = self.video_end_time.insert(video_end_time);
self
}
pub(crate) fn build(self) -> Result<Slide> {
let Some(background) = self.background else {
return Err(miette!("No background"));
};
let Some(text) = self.text else {
return Err(miette!("No text"));
};
let Some(font) = self.font else {
return Err(miette!("No font"));
};
let Some(font_size) = self.font_size else {
return Err(miette!("No font_size"));
};
let Some(text_alignment) = self.text_alignment else {
return Err(miette!("No text_alignment"));
};
let Some(video_loop) = self.video_loop else {
return Err(miette!("No video_loop"));
};
let Some(video_start_time) = self.video_start_time else {
return Err(miette!("No video_start_time"));
};
let Some(video_end_time) = self.video_end_time else {
return Err(miette!("No video_end_time"));
};
Ok(Slide {
background,
text,
font,
font_size,
text_alignment,
video_loop,
video_start_time,
video_end_time,
..Default::default()
})
}
}
struct Image {
source: String,
fit: String,
}
fn build_image_bg(atom: &Value, image_map: &mut HashMap<String, String>, map_index: usize) {
// This needs to be the cons that contains (image . ...)
// the image is a symbol and the rest are keywords and other maps
if atom.is_symbol() {
// We shouldn't get a symbol
return;
}
for atom in atom.list_iter().unwrap().map(|a| a.as_cons().unwrap()) {
if atom.car() == &Value::Symbol("image".into()) {
build_image_bg(atom.cdr(), image_map, map_index);
} else {
let atom = atom.car();
match atom {
Value::Keyword(keyword) => {
image_map.insert(keyword.to_string(), "".into());
build_image_bg(atom, image_map, map_index);
}
Value::Symbol(symbol) => {
// let mut key;
// let image_map = image_map
// .iter_mut()
// .enumerate()
// .filter(|(i, e)| i == &map_index)
// .map(|(i, (k, v))| v.push_str(symbol))
// .collect();
build_image_bg(atom, image_map, map_index);
}
Value::String(string) => {}
_ => {}
}
}
}
}
#[cfg(test)]
mod test {
use lexpr::{parse::Options, Parser};
use pretty_assertions::assert_eq;
use serde_lexpr::from_str;
use std::fs::read_to_string;
use tracing::debug;
use super::*;
fn test_slide() -> Slide {
Slide::default()
}
fn print_list(atom: &Value) {
match atom {
Value::Nil => {
dbg!(Value::Nil);
}
Value::Bool(boolean) => {
dbg!(boolean);
}
Value::Number(number) => {
dbg!(number);
}
Value::String(string) => {
dbg!(string);
}
Value::Symbol(symbol) => {
dbg!(symbol);
match symbol.as_ref() {
"image" => {
dbg!("This is an image");
}
"slide" => {
dbg!("This is a slide");
}
"text" => {
dbg!("This is a text");
}
_ => {}
}
}
Value::Keyword(keyword) => {
dbg!(keyword);
}
Value::Cons(v) => {
print_list(v.car());
print_list(v.cdr());
}
Value::Null => {
dbg!("null");
}
Value::Char(c) => {
dbg!(c);
}
Value::Bytes(b) => {
dbg!(b);
}
Value::Vector(v) => {
dbg!(v);
}
}
// if atom.is_list() {
// for atom in atom.list_iter().unwrap() {
// dbg!(atom);
// print_list(atom);
// }
// } else {
// dbg!(atom);
// }
}
#[test]
fn test_lexp_serialize() {
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) => {
print_list(&atom);
}
Err(e) => {
dbg!(e);
}
}
}
// parser.map(|atom| match atom {
// Ok(atom) => dbg!(atom),
// Err(e) => dbg!(e),
// });
// let lispy = from_str_elisp(&lisp).expect("oops");
// if lispy.is_list() {
// for atom in lispy.list_iter().unwrap() {
// print_list(atom);
// }
// }
let slide: Slide = from_str(&lisp).expect("oops");
let test_slide = test_slide();
assert_eq!(slide, test_slide)
}
}