Working to actually create an iced lumina
This commit is contained in:
parent
0121dfa8f3
commit
084ce2fc32
3671
Cargo.lock
generated
3671
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
@ -1,11 +1,20 @@
|
|||
[package]
|
||||
name = "lumina-iced-test"
|
||||
name = "lumina"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "A cli presentation system"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
iced = { version = "0.12.1", features = ["tokio", "image", "debug", "system"] }
|
||||
iced_aw = "0.9.2"
|
||||
# iced_winit = "0.10.0"
|
||||
clap = { version = "4.5.20", features = ["debug", "derive"] }
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic", features = ["debug", "winit", "tokio", "xdg-portal", "dbus-config", "a11y", "wayland", "wgpu", "multi-window", "single-instance"] }
|
||||
# iced = { version = "0.13.1", features = ["tokio", "image", "debug", "system"] }
|
||||
# iced_aw = "0.11.0"
|
||||
lexpr = "0.2.7"
|
||||
miette = { version = "7.2.0", features = ["fancy"] }
|
||||
pretty_assertions = "1.4.1"
|
||||
serde = { version = "1.0.213", features = ["derive"] }
|
||||
serde-lexpr = "0.1.3"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt", "std", "chrono", "time", "local-time", "env-filter"] }
|
||||
|
|
46
flake.lock
46
flake.lock
|
@ -6,11 +6,11 @@
|
|||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1725949977,
|
||||
"narHash": "sha256-wyprFbiEQfc3iopAXEtyxrQpxKTv3bByEZQno8GiN5I=",
|
||||
"lastModified": 1729375822,
|
||||
"narHash": "sha256-bRo4xVwUhvJ4Gz+OhWMREFMdBOYSw4Yi1Apj01ebbug=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "930afdff0d11e60fa5956947513feb57463284af",
|
||||
"rev": "2853e7d9b5c52a148a9fb824bfe4f9f433f557ab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -24,11 +24,11 @@
|
|||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694529238,
|
||||
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -42,11 +42,11 @@
|
|||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1694081375,
|
||||
"narHash": "sha256-vzJXOUnmkMCm3xw8yfPP5m8kypQ3BhAIRe4RRCWpzy8=",
|
||||
"lastModified": 1721727458,
|
||||
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "3f976d822b7b37fc6fb8e6f157c2dd05e7e94e89",
|
||||
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -57,11 +57,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1725634671,
|
||||
"narHash": "sha256-v3rIhsJBOMLR8e/RNWxr828tB+WywYIoajrZKFM+0Gg=",
|
||||
"lastModified": 1729070438,
|
||||
"narHash": "sha256-KOTTUfPkugH52avUvXGxvWy8ibKKj4genodIYUED+Kc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "574d1eac1c200690e27b8eb4e24887f8df7ac27c",
|
||||
"rev": "5785b6bb5eaae44e627d541023034e1601455827",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -73,12 +73,10 @@
|
|||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1696725822,
|
||||
"narHash": "sha256-B7uAOS7TkLlOg1aX01rQlYbydcyB6ZnLJSfaYbKVww8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5aabb5780a11c500981993d49ee93cfa6df9307b",
|
||||
"type": "github"
|
||||
"lastModified": 0,
|
||||
"narHash": "sha256-4b3A9zPpxAxLnkF9MawJNHDtOOl6ruL0r6Og1TEDGCE=",
|
||||
"path": "/nix/store/qpg5mwsind2hy35b9vpk6mx4jimnypw0-source",
|
||||
"type": "path"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
|
@ -87,11 +85,11 @@
|
|||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1696604326,
|
||||
"narHash": "sha256-YXUNI0kLEcI5g8lqGMb0nh67fY9f2YoJsILafh6zlMo=",
|
||||
"lastModified": 1729256560,
|
||||
"narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "87828a0e03d1418e848d3dd3f3014a632e4a4f64",
|
||||
"rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -112,11 +110,11 @@
|
|||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1725890120,
|
||||
"narHash": "sha256-7bsWAKG/otbHj7wmCBrJ9P6ve2MFcoOlIh6wcx6ffKg=",
|
||||
"lastModified": 1729255720,
|
||||
"narHash": "sha256-yODOuZxBkS0UfqMa6nmbqNbVfIbsu0tYLbV5vZzmsqI=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "e35227d186acd47d8e5f78cbd792d57ddf47d74b",
|
||||
"rev": "72b214fbfbe6f7b95a7877b962783bd42062cc0a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
11
flake.nix
11
flake.nix
|
@ -38,7 +38,7 @@
|
|||
pkg-config
|
||||
];
|
||||
|
||||
bi = [
|
||||
bi = with pkgs; [
|
||||
gcc
|
||||
stdenv
|
||||
gnumake
|
||||
|
@ -61,6 +61,15 @@
|
|||
devShell = pkgs.mkShell {
|
||||
nativeBuildInputs = nbi;
|
||||
buildInputs = bi;
|
||||
LD_LIBRARY_PATH = "$LD_LIBRARY_PATH:${
|
||||
with pkgs;
|
||||
pkgs.lib.makeLibraryPath [
|
||||
pkgs.vulkan-loader
|
||||
pkgs.wayland
|
||||
pkgs.wayland-protocols
|
||||
pkgs.libxkbcommon
|
||||
]
|
||||
}";
|
||||
};
|
||||
defaultPackage = naersk'.buildPackage {
|
||||
src = ./.;
|
||||
|
|
10
justfile
Normal file
10
justfile
Normal file
|
@ -0,0 +1,10 @@
|
|||
default:
|
||||
just --list
|
||||
build:
|
||||
RUST_LOG=debug cargo build
|
||||
run:
|
||||
RUST_LOG=debug cargo run -- ~/dev/lumina-iced/test_presentation.lisp
|
||||
clean:
|
||||
RUST_LOG=debug cargo clean
|
||||
test:
|
||||
RUST_LOG=debug cargo test --benches --tests --all-features -- --nocapture
|
430
src/main.rs
430
src/main.rs
|
@ -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
404
src/slide.rs
Normal 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)
|
||||
}
|
||||
}
|
13
test_presentation.lisp
Normal file
13
test_presentation.lisp
Normal file
|
@ -0,0 +1,13 @@
|
|||
(slide (image :source "~/pics/frodo.jpg" :fill crop
|
||||
(text "This is frodo" :font-size 50)))
|
||||
|
||||
;; (slide ((background . ((path . "cool.jpg")
|
||||
;; (kind . Image)))
|
||||
;; (text . "hi")
|
||||
;; (font . "quicksand")
|
||||
;; (id . 0)
|
||||
;; (font_size . 50)
|
||||
;; (text_alignment . MiddleCenter)
|
||||
;; (video_loop . #f)
|
||||
;; (video_start_time . 0.0)
|
||||
;; (video_end_time . 0.0)))
|
Loading…
Reference in a new issue