Merge branch 'master' of git.tfcconnection.org:chris/lumina-iced
Some checks are pending
/ test (push) Waiting to run
Some checks are pending
/ test (push) Waiting to run
This commit is contained in:
commit
77daa03db6
19 changed files with 4325 additions and 748 deletions
157
src/core/lisp.rs
157
src/core/lisp.rs
|
|
@ -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}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -95,7 +95,10 @@ mod test {
|
|||
let screenshot = bg_path_from_video(video);
|
||||
let screenshot_string =
|
||||
screenshot.to_str().expect("Should be thing");
|
||||
assert_eq!(screenshot_string, "/home/chris/.local/share/lumina/thumbnails/moms-funeral.png");
|
||||
assert_eq!(
|
||||
screenshot_string,
|
||||
"/home/chris/.local/share/lumina/thumbnails/moms-funeral.png"
|
||||
);
|
||||
|
||||
// let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
let result = bg_from_video(video, &screenshot);
|
||||
|
|
@ -118,6 +121,9 @@ mod test {
|
|||
let screenshot = bg_path_from_video(video);
|
||||
let screenshot_string =
|
||||
screenshot.to_str().expect("Should be thing");
|
||||
assert_ne!(screenshot_string, "/home/chris/.local/share/lumina/thumbnails/All WebDev Sucks and you know it.webm");
|
||||
assert_ne!(
|
||||
screenshot_string,
|
||||
"/home/chris/.local/share/lumina/thumbnails/All WebDev Sucks and you know it.webm"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
688
src/main.rs
688
src/main.rs
|
|
@ -9,26 +9,34 @@ 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, Length, Point};
|
||||
use cosmic::iced::{
|
||||
self, event, window, Background as IcedBackground, Border, Color,
|
||||
Length,
|
||||
};
|
||||
use cosmic::iced_core::text::Wrapping;
|
||||
use cosmic::iced_futures::Subscription;
|
||||
use cosmic::iced_widget::{column, row, stack};
|
||||
use cosmic::theme;
|
||||
use cosmic::widget::dnd_destination::dnd_destination;
|
||||
use cosmic::widget::menu::key_bind::Modifier;
|
||||
use cosmic::widget::menu::{ItemWidth, KeyBind};
|
||||
use cosmic::widget::nav_bar::nav_bar_style;
|
||||
use cosmic::widget::tooltip::Position as TPosition;
|
||||
use cosmic::widget::Container;
|
||||
use cosmic::widget::{
|
||||
button, horizontal_space, mouse_area, nav_bar, search_input,
|
||||
tooltip, vertical_space, Space,
|
||||
button, context_menu, horizontal_space, mouse_area, nav_bar,
|
||||
nav_bar_toggle, responsive, scrollable, search_input, tooltip,
|
||||
vertical_space, Space,
|
||||
};
|
||||
use cosmic::widget::{container, text};
|
||||
use cosmic::widget::{icon, slider};
|
||||
use cosmic::widget::{menu, Container};
|
||||
use cosmic::{executor, Application, ApplicationExt, Element};
|
||||
use crisp::types::Value;
|
||||
use lisp::parse_lisp;
|
||||
use miette::{miette, Result};
|
||||
use rayon::prelude::*;
|
||||
use resvg::usvg::fontdb;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -42,6 +50,7 @@ use ui::EditorMode;
|
|||
|
||||
use crate::core::kinds::ServiceItemKind;
|
||||
use crate::ui::text_svg::{self};
|
||||
use crate::ui::widgets::draggable;
|
||||
|
||||
pub mod core;
|
||||
pub mod lisp;
|
||||
|
|
@ -54,7 +63,7 @@ struct Cli {
|
|||
watch: bool,
|
||||
#[arg(short = 'i', long)]
|
||||
ui: bool,
|
||||
file: PathBuf,
|
||||
file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
|
@ -106,7 +115,9 @@ struct App {
|
|||
presenter: Presenter,
|
||||
windows: Vec<window::Id>,
|
||||
service: Vec<ServiceItem>,
|
||||
selected_items: Vec<usize>,
|
||||
current_item: (usize, usize),
|
||||
hovered_item: Option<usize>,
|
||||
presentation_open: bool,
|
||||
cli_mode: bool,
|
||||
library: Option<Library>,
|
||||
|
|
@ -119,6 +130,8 @@ struct App {
|
|||
search_id: cosmic::widget::Id,
|
||||
library_dragged_item: Option<ServiceItem>,
|
||||
fontdb: Arc<fontdb::Database>,
|
||||
menu_keys: HashMap<KeyBind, MenuAction>,
|
||||
context_menu: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -129,7 +142,7 @@ enum Message {
|
|||
File(PathBuf),
|
||||
OpenWindow,
|
||||
CloseWindow(Option<window::Id>),
|
||||
WindowOpened(window::Id, Option<Point>),
|
||||
WindowOpened(window::Id),
|
||||
WindowClosed(window::Id),
|
||||
AddLibrary(Library),
|
||||
LibraryToggle,
|
||||
|
|
@ -138,15 +151,53 @@ enum Message {
|
|||
None,
|
||||
EditorToggle(bool),
|
||||
ChangeServiceItem(usize),
|
||||
SelectServiceItem(usize),
|
||||
AddSelectServiceItem(usize),
|
||||
HoveredServiceItem(Option<usize>),
|
||||
AddServiceItem(usize, ServiceItem),
|
||||
RemoveServiceItem(usize),
|
||||
AddServiceItemDrop(usize),
|
||||
AppendServiceItem(ServiceItem),
|
||||
AddService(Vec<ServiceItem>),
|
||||
ReorderService(usize, usize),
|
||||
ContextMenuItem(usize),
|
||||
SearchFocus,
|
||||
Search(String),
|
||||
CloseSearch,
|
||||
UpdateSearchResults(Vec<ServiceItem>),
|
||||
OpenEditor(ServiceItem),
|
||||
New,
|
||||
Open,
|
||||
OpenFile(PathBuf),
|
||||
Save(Option<PathBuf>),
|
||||
SaveAs,
|
||||
OpenSettings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum MenuAction {
|
||||
New,
|
||||
Save,
|
||||
SaveAs,
|
||||
Open,
|
||||
OpenSettings,
|
||||
DeleteItem(usize),
|
||||
}
|
||||
|
||||
impl menu::Action for MenuAction {
|
||||
type Message = Message;
|
||||
|
||||
fn message(&self) -> Self::Message {
|
||||
match self {
|
||||
MenuAction::New => Message::New,
|
||||
MenuAction::Save => Message::Save(None),
|
||||
MenuAction::SaveAs => Message::SaveAs,
|
||||
MenuAction::Open => Message::Open,
|
||||
MenuAction::OpenSettings => Message::OpenSettings,
|
||||
MenuAction::DeleteItem(index) => {
|
||||
Message::RemoveServiceItem(*index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HEADER_SPACE: u16 = 6;
|
||||
|
|
@ -179,30 +230,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
|
||||
|
|
@ -230,12 +287,35 @@ impl cosmic::Application for App {
|
|||
// nav_model.insert().text(item.title()).data(item.clone());
|
||||
// }
|
||||
|
||||
let mut menu_keys = HashMap::new();
|
||||
menu_keys.insert(
|
||||
KeyBind {
|
||||
modifiers: vec![Modifier::Ctrl],
|
||||
key: Key::Character("s".into()),
|
||||
},
|
||||
MenuAction::Save,
|
||||
);
|
||||
menu_keys.insert(
|
||||
KeyBind {
|
||||
modifiers: vec![Modifier::Ctrl],
|
||||
key: Key::Character("o".into()),
|
||||
},
|
||||
MenuAction::Open,
|
||||
);
|
||||
menu_keys.insert(
|
||||
KeyBind {
|
||||
modifiers: vec![Modifier::Ctrl],
|
||||
key: Key::Character(".".into()),
|
||||
},
|
||||
MenuAction::OpenSettings,
|
||||
);
|
||||
// nav_model.activate_position(0);
|
||||
let mut app = Self {
|
||||
presenter,
|
||||
core,
|
||||
nav_model,
|
||||
service: items,
|
||||
selected_items: vec![],
|
||||
file: PathBuf::default(),
|
||||
windows,
|
||||
presentation_open: false,
|
||||
|
|
@ -251,6 +331,9 @@ impl cosmic::Application for App {
|
|||
current_item: (0, 0),
|
||||
library_dragged_item: None,
|
||||
fontdb: Arc::clone(&fontdb),
|
||||
menu_keys,
|
||||
hovered_item: None,
|
||||
context_menu: None,
|
||||
};
|
||||
|
||||
let mut batch = vec![];
|
||||
|
|
@ -270,7 +353,82 @@ impl cosmic::Application for App {
|
|||
}
|
||||
|
||||
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
||||
vec![]
|
||||
let file_menu = menu::Tree::with_children(
|
||||
Into::<Element<Message>>::into(menu::root("File")),
|
||||
menu::items(
|
||||
&self.menu_keys,
|
||||
vec![
|
||||
menu::Item::Button(
|
||||
"New",
|
||||
Some(
|
||||
icon::from_name("document-new")
|
||||
.symbolic(true)
|
||||
.into(),
|
||||
),
|
||||
MenuAction::New,
|
||||
),
|
||||
menu::Item::Button(
|
||||
"Open",
|
||||
Some(
|
||||
icon::from_name("document-open")
|
||||
.symbolic(true)
|
||||
.into(),
|
||||
),
|
||||
MenuAction::Open,
|
||||
),
|
||||
menu::Item::Button(
|
||||
"Save",
|
||||
Some(
|
||||
icon::from_name("document-save")
|
||||
.symbolic(true)
|
||||
.into(),
|
||||
),
|
||||
MenuAction::Save,
|
||||
),
|
||||
menu::Item::Button(
|
||||
"Save As",
|
||||
Some(
|
||||
icon::from_name("document-save-as")
|
||||
.symbolic(true)
|
||||
.into(),
|
||||
),
|
||||
MenuAction::SaveAs,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
let settings_menu = menu::Tree::with_children(
|
||||
Into::<Element<Message>>::into(
|
||||
menu::root("Settings").on_press(Message::None),
|
||||
),
|
||||
menu::items(
|
||||
&self.menu_keys,
|
||||
vec![menu::Item::Button(
|
||||
"Open Settings",
|
||||
Some(
|
||||
icon::from_name("settings")
|
||||
.symbolic(true)
|
||||
.into(),
|
||||
),
|
||||
MenuAction::OpenSettings,
|
||||
)],
|
||||
),
|
||||
);
|
||||
let menu_bar =
|
||||
menu::bar::<Message>(vec![file_menu, settings_menu])
|
||||
.item_width(ItemWidth::Static(250))
|
||||
.main_offset(10);
|
||||
let library_button = tooltip(
|
||||
nav_bar_toggle().on_toggle(Message::LibraryToggle),
|
||||
if self.library_open {
|
||||
"Hide library"
|
||||
} else {
|
||||
"Show library"
|
||||
},
|
||||
TPosition::Bottom,
|
||||
)
|
||||
.gap(cosmic::theme::spacing().space_xs);
|
||||
vec![library_button.into(), menu_bar.into()]
|
||||
}
|
||||
|
||||
fn header_center(&self) -> Vec<Element<Self::Message>> {
|
||||
|
|
@ -309,7 +467,8 @@ impl cosmic::Application for App {
|
|||
.on_press(Message::SearchFocus),
|
||||
"Search Library",
|
||||
TPosition::Bottom,
|
||||
),
|
||||
)
|
||||
.gap(cosmic::theme::spacing().space_xs),
|
||||
tooltip(
|
||||
button::custom(
|
||||
row!(
|
||||
|
|
@ -332,7 +491,8 @@ impl cosmic::Application for App {
|
|||
)),
|
||||
"Enter Edit Mode",
|
||||
TPosition::Bottom,
|
||||
),
|
||||
)
|
||||
.gap(cosmic::theme::spacing().space_xs),
|
||||
tooltip(
|
||||
button::custom(
|
||||
row!(
|
||||
|
|
@ -363,7 +523,8 @@ impl cosmic::Application for App {
|
|||
}),
|
||||
"Start Presentation",
|
||||
TPosition::Bottom,
|
||||
),
|
||||
)
|
||||
.gap(cosmic::theme::spacing().space_xs),
|
||||
tooltip(
|
||||
button::custom(
|
||||
row!(
|
||||
|
|
@ -385,6 +546,7 @@ impl cosmic::Application for App {
|
|||
"Open Library",
|
||||
TPosition::Bottom,
|
||||
)
|
||||
.gap(cosmic::theme::spacing().space_xs),
|
||||
]
|
||||
.spacing(HEADER_SPACE)
|
||||
.into();
|
||||
|
|
@ -434,23 +596,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
|
||||
}
|
||||
}
|
||||
|
|
@ -580,7 +752,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),
|
||||
))
|
||||
|
|
@ -612,19 +784,11 @@ impl cosmic::Application for App {
|
|||
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 slide_index = slide_index + 1;
|
||||
let action = self.presenter.update(
|
||||
presenter::Message::SlideChange(
|
||||
slide,
|
||||
presenter::Message::ActivateSlide(
|
||||
item_index,
|
||||
slide_index,
|
||||
),
|
||||
);
|
||||
match action {
|
||||
|
|
@ -644,10 +808,12 @@ impl cosmic::Application for App {
|
|||
// debug!("Slides are not longer");
|
||||
self.current_item =
|
||||
(item_index + 1, 0);
|
||||
if let Some(item) =
|
||||
self.service.get(item_index + 1)
|
||||
if self
|
||||
.service
|
||||
.get(item_index + 1)
|
||||
.is_some()
|
||||
{
|
||||
let action = self.presenter.update(presenter::Message::SlideChange(item.slides[0].clone()));
|
||||
let action = self.presenter.update(presenter::Message::ActivateSlide(self.current_item.0, self.current_item.1));
|
||||
match action {
|
||||
presenter::Action::Task(
|
||||
task,
|
||||
|
|
@ -678,12 +844,11 @@ impl cosmic::Application for App {
|
|||
self.service.get(item_index)
|
||||
{
|
||||
if slide_index != 0 {
|
||||
let slide = item.slides
|
||||
[slide_index - 1]
|
||||
.clone();
|
||||
let slide_index = slide_index - 1;
|
||||
let action = self.presenter.update(
|
||||
presenter::Message::SlideChange(
|
||||
slide,
|
||||
presenter::Message::ActivateSlide(
|
||||
item_index,
|
||||
slide_index,
|
||||
),
|
||||
);
|
||||
match action {
|
||||
|
|
@ -718,10 +883,12 @@ impl cosmic::Application for App {
|
|||
item_index - 1,
|
||||
previous_item_slides_length - 1,
|
||||
);
|
||||
if let Some(item) =
|
||||
self.service.get(item_index - 1)
|
||||
if self
|
||||
.service
|
||||
.get(item_index - 1)
|
||||
.is_some()
|
||||
{
|
||||
let action = self.presenter.update(presenter::Message::SlideChange(item.slides[previous_item_slides_length - 1].clone()));
|
||||
let action = self.presenter.update(presenter::Message::ActivateSlide(self.current_item.0, self.current_item.1));
|
||||
match action {
|
||||
presenter::Action::Task(
|
||||
task,
|
||||
|
|
@ -818,9 +985,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) => {
|
||||
|
|
@ -830,7 +995,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?")
|
||||
|
|
@ -873,10 +1038,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 {
|
||||
|
|
@ -892,6 +1053,18 @@ impl cosmic::Application for App {
|
|||
self.search_id.clone(),
|
||||
)
|
||||
}
|
||||
Message::HoveredServiceItem(index) => {
|
||||
self.hovered_item = index;
|
||||
Task::none()
|
||||
}
|
||||
Message::SelectServiceItem(index) => {
|
||||
self.selected_items = vec![index];
|
||||
Task::none()
|
||||
}
|
||||
Message::AddSelectServiceItem(index) => {
|
||||
self.selected_items.push(index);
|
||||
Task::none()
|
||||
}
|
||||
Message::ChangeServiceItem(index) => {
|
||||
if let Some((index, item)) = self
|
||||
.service
|
||||
|
|
@ -902,8 +1075,9 @@ impl cosmic::Application for App {
|
|||
{
|
||||
self.current_item = (index, 0);
|
||||
self.presenter.update(
|
||||
presenter::Message::SlideChange(
|
||||
slide.clone(),
|
||||
presenter::Message::ActivateSlide(
|
||||
self.current_item.0,
|
||||
self.current_item.1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -925,6 +1099,15 @@ impl cosmic::Application for App {
|
|||
self.presenter.update_items(self.service.clone());
|
||||
Task::none()
|
||||
}
|
||||
Message::RemoveServiceItem(index) => {
|
||||
self.service.remove(index);
|
||||
self.presenter.update_items(self.service.clone());
|
||||
Task::none()
|
||||
}
|
||||
Message::ContextMenuItem(index) => {
|
||||
self.context_menu = Some(index);
|
||||
Task::none()
|
||||
}
|
||||
Message::AddServiceItemDrop(index) => {
|
||||
if let Some(item) = &self.library_dragged_item {
|
||||
self.service.insert(index, item.clone());
|
||||
|
|
@ -947,6 +1130,12 @@ impl cosmic::Application for App {
|
|||
self.presenter.update_items(self.service.clone());
|
||||
Task::none()
|
||||
}
|
||||
Message::ReorderService(index, target_index) => {
|
||||
let item = self.service.remove(index);
|
||||
self.service.insert(target_index, item);
|
||||
self.presenter.update_items(self.service.clone());
|
||||
Task::none()
|
||||
}
|
||||
Message::Search(query) => {
|
||||
self.search_query = query.clone();
|
||||
self.search(query)
|
||||
|
|
@ -978,6 +1167,34 @@ impl cosmic::Application for App {
|
|||
ServiceItemKind::Content(_slide) => todo!(),
|
||||
}
|
||||
}
|
||||
Message::New => {
|
||||
debug!("new file");
|
||||
Task::none()
|
||||
}
|
||||
Message::Open => {
|
||||
debug!("Open file");
|
||||
Task::none()
|
||||
}
|
||||
Message::OpenFile(file) => {
|
||||
debug!(?file, "opening file");
|
||||
Task::none()
|
||||
}
|
||||
Message::Save(file) => {
|
||||
let Some(file) = file else {
|
||||
debug!("saving current");
|
||||
return Task::none();
|
||||
};
|
||||
debug!(?file, "saving new file");
|
||||
Task::none()
|
||||
}
|
||||
Message::SaveAs => {
|
||||
debug!("saving as a file");
|
||||
Task::none()
|
||||
}
|
||||
Message::OpenSettings => {
|
||||
debug!("Opening settings");
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1069,8 +1286,7 @@ impl cosmic::Application for App {
|
|||
let song_editor =
|
||||
self.song_editor.view().map(Message::SongEditor);
|
||||
|
||||
let row = row![
|
||||
library,
|
||||
let service_row = row![
|
||||
service_list,
|
||||
Container::new(
|
||||
button::icon(icon_left)
|
||||
|
|
@ -1104,23 +1320,33 @@ impl cosmic::Application for App {
|
|||
.height(Length::Fill)
|
||||
.spacing(20);
|
||||
|
||||
let column = column![
|
||||
Container::new(row).center_y(Length::Fill),
|
||||
let preview_bar = if self.editor_mode.is_none() {
|
||||
Container::new(
|
||||
self.presenter.preview_bar().map(Message::Present)
|
||||
self.presenter.preview_bar().map(Message::Present),
|
||||
)
|
||||
.clip(true)
|
||||
.width(Length::Fill)
|
||||
.center_y(180)
|
||||
];
|
||||
} else {
|
||||
Container::new(horizontal_space())
|
||||
};
|
||||
|
||||
if let Some(_editor) = &self.editor_mode {
|
||||
let main_area = if let Some(editor) = &self.editor_mode {
|
||||
container(song_editor)
|
||||
.padding(cosmic::theme::spacing().space_xxl)
|
||||
.into()
|
||||
} else {
|
||||
Element::from(column)
|
||||
}
|
||||
Container::new(service_row).center_y(Length::Fill)
|
||||
};
|
||||
|
||||
let column = column![
|
||||
row![
|
||||
library.width(Length::FillPortion(1)),
|
||||
main_area.width(Length::FillPortion(4))
|
||||
],
|
||||
preview_bar
|
||||
];
|
||||
|
||||
column.into()
|
||||
}
|
||||
|
||||
// View for presentation
|
||||
|
|
@ -1159,9 +1385,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> {
|
||||
|
|
@ -1185,36 +1410,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,
|
||||
|
|
@ -1237,6 +1432,15 @@ where
|
|||
}
|
||||
}
|
||||
match (key, modifiers) {
|
||||
(Key::Character(k), Modifiers::CTRL) if k == *"s" => {
|
||||
self.update(Message::Save(None))
|
||||
}
|
||||
(Key::Character(k), Modifiers::CTRL) if k == *"o" => {
|
||||
self.update(Message::Open)
|
||||
}
|
||||
(Key::Character(k), Modifiers::CTRL) if k == *"." => {
|
||||
self.update(Message::OpenSettings)
|
||||
}
|
||||
(Key::Character(k), Modifiers::CTRL)
|
||||
if k == *"k" || k == *"f" =>
|
||||
{
|
||||
|
|
@ -1278,65 +1482,189 @@ 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)
|
||||
// .padding(cosmic::theme::spacing().space_m)
|
||||
// .height(cosmic::theme::spacing().space_xxxl)
|
||||
.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,
|
||||
))
|
||||
.align_y(Vertical::Center)
|
||||
.wrapping(Wrapping::None)
|
||||
.into()
|
||||
});
|
||||
let container = container(
|
||||
row![icon, title]
|
||||
.align_y(Vertical::Center)
|
||||
.spacing(cosmic::theme::spacing().space_xs),
|
||||
)
|
||||
.height(cosmic::theme::spacing().space_xl)
|
||||
.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
|
||||
},
|
||||
) || self
|
||||
.selected_items
|
||||
.contains(&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_double_press(Message::ChangeServiceItem(
|
||||
index,
|
||||
))
|
||||
.on_drag(Message::None)
|
||||
.on_right_press(Message::ContextMenuItem(index))
|
||||
.on_release(Message::SelectServiceItem(index));
|
||||
let single_item = if let Some(context_menu_item) =
|
||||
self.context_menu
|
||||
{
|
||||
if context_menu_item == index {
|
||||
let context_menu = context_menu(
|
||||
mouse_area,
|
||||
self.context_menu.map_or_else(
|
||||
|| None,
|
||||
|i| {
|
||||
if i == index {
|
||||
let menu =
|
||||
vec![menu::Item::Button(
|
||||
"Delete",
|
||||
None,
|
||||
MenuAction::DeleteItem(index),
|
||||
)];
|
||||
Some(menu::items(
|
||||
&HashMap::new(),
|
||||
menu,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.close_on_escape(true);
|
||||
Element::from(context_menu)
|
||||
} else {
|
||||
Element::from(mouse_area)
|
||||
}
|
||||
} else {
|
||||
Element::from(mouse_area)
|
||||
};
|
||||
let tooltip = tooltip(
|
||||
single_item,
|
||||
text::body(item.kind.to_string()),
|
||||
TPosition::Right,
|
||||
)
|
||||
.gap(cosmic::theme::spacing().space_xs);
|
||||
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)
|
||||
})
|
||||
.into()
|
||||
});
|
||||
|
||||
let scrollable = scrollable(
|
||||
draggable::column::column(list)
|
||||
.spacing(10)
|
||||
.on_drag(|event| match event {
|
||||
draggable::DragEvent::Picked { .. } => {
|
||||
Message::None
|
||||
}
|
||||
draggable::DragEvent::Dropped {
|
||||
index,
|
||||
target_index,
|
||||
..
|
||||
} => Message::ReorderService(index, target_index),
|
||||
draggable::DragEvent::Canceled { .. } => {
|
||||
Message::None
|
||||
}
|
||||
})
|
||||
.style(|t| draggable::column::Style {
|
||||
scale: 1.05,
|
||||
moved_item_overlay: Color::from(
|
||||
t.cosmic().primary.base,
|
||||
)
|
||||
.scale_alpha(0.2)
|
||||
.into(),
|
||||
ghost_border: Border {
|
||||
width: 1.0,
|
||||
color: t.cosmic().secondary.base.into(),
|
||||
radius: t.cosmic().radius_m().into(),
|
||||
},
|
||||
ghost_background: Color::from(
|
||||
t.cosmic().secondary.base,
|
||||
)
|
||||
.scale_alpha(0.2)
|
||||
.into(),
|
||||
})
|
||||
.height(Length::Shrink),
|
||||
)
|
||||
.anchor_top()
|
||||
.height(Length::Fill);
|
||||
|
||||
let column = column![
|
||||
text::heading("Service List")
|
||||
.center()
|
||||
.width(Length::Fill),
|
||||
iced::widget::horizontal_rule(1),
|
||||
column(list).spacing(10),
|
||||
scrollable
|
||||
]
|
||||
.padding(10)
|
||||
.spacing(10);
|
||||
let container = Container::new(stack![
|
||||
dnd_destination(
|
||||
vertical_space().width(Length::Fill),
|
||||
vec!["application/service-item".into()]
|
||||
|
|
@ -1358,33 +1686,11 @@ where
|
|||
debug!(?item);
|
||||
Message::AppendServiceItem(item)
|
||||
}
|
||||
)
|
||||
]
|
||||
.padding(10)
|
||||
.spacing(10);
|
||||
let container = Container::new(column)
|
||||
// .height(Length::Fill)
|
||||
.style(nav_bar_style);
|
||||
),
|
||||
column
|
||||
])
|
||||
.style(nav_bar_style);
|
||||
|
||||
container.center(Length::FillPortion(2)).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ pub struct Library {
|
|||
enum MenuMessage {
|
||||
Delete((LibraryKind, i32)),
|
||||
Open,
|
||||
None,
|
||||
}
|
||||
|
||||
impl MenuAction for MenuMessage {
|
||||
|
|
@ -64,7 +63,6 @@ impl MenuAction for MenuMessage {
|
|||
Message::DeleteItem((*kind, *index))
|
||||
}
|
||||
MenuMessage::Open => todo!(),
|
||||
MenuMessage::None => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -314,9 +312,44 @@ impl<'a> Library {
|
|||
})
|
||||
.ok()
|
||||
}
|
||||
LibraryKind::Video => todo!(),
|
||||
LibraryKind::Image => todo!(),
|
||||
LibraryKind::Presentation => todo!(),
|
||||
LibraryKind::Video => {
|
||||
let video = Video::default();
|
||||
self.video_library
|
||||
.add_item(video)
|
||||
.map(|_| {
|
||||
let index =
|
||||
self.video_library.items.len();
|
||||
(LibraryKind::Video, index as i32)
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
LibraryKind::Image => {
|
||||
let image = Image::default();
|
||||
self.image_library
|
||||
.add_item(image)
|
||||
.map(|_| {
|
||||
let index =
|
||||
self.image_library.items.len();
|
||||
(LibraryKind::Image, index as i32)
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
LibraryKind::Presentation => {
|
||||
let presentation = Presentation::default();
|
||||
self.presentation_library
|
||||
.add_item(presentation)
|
||||
.map(|_| {
|
||||
let index = self
|
||||
.presentation_library
|
||||
.items
|
||||
.len();
|
||||
(
|
||||
LibraryKind::Presentation,
|
||||
index as i32,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
};
|
||||
return self.update(Message::OpenItem(item));
|
||||
}
|
||||
|
|
@ -591,40 +624,43 @@ impl<'a> Library {
|
|||
column({
|
||||
model.items.iter().enumerate().map(
|
||||
|(index, item)| {
|
||||
|
||||
let service_item = item.to_service_item();
|
||||
let visual_item = self
|
||||
.single_item(index, item, model)
|
||||
.map(|()| Message::None);
|
||||
|
||||
DndSource::<Message, ServiceItem>::new({
|
||||
let mouse_area = Element::from(mouse_area(visual_item)
|
||||
.on_drag(Message::DragItem(service_item.clone()))
|
||||
.on_enter(Message::HoverItem(
|
||||
Some((
|
||||
model.kind,
|
||||
index as i32,
|
||||
)),
|
||||
))
|
||||
.on_double_click(
|
||||
Message::OpenItem(Some((
|
||||
model.kind,
|
||||
index as i32,
|
||||
))),
|
||||
)
|
||||
.on_right_press(Message::OpenContext(index as i32))
|
||||
.on_exit(Message::HoverItem(None))
|
||||
.on_press(Message::SelectItem(
|
||||
Some((
|
||||
model.kind,
|
||||
index as i32,
|
||||
)),
|
||||
)));
|
||||
let mouse_area = mouse_area(visual_item);
|
||||
let mouse_area = mouse_area.on_enter(Message::HoverItem(
|
||||
Some((
|
||||
model.kind,
|
||||
index as i32,
|
||||
)),
|
||||
))
|
||||
.on_double_click(
|
||||
Message::OpenItem(Some((
|
||||
model.kind,
|
||||
index as i32,
|
||||
))),
|
||||
)
|
||||
.on_right_press(Message::OpenContext(index as i32))
|
||||
.on_exit(Message::HoverItem(None))
|
||||
.on_press(Message::SelectItem(
|
||||
Some((
|
||||
model.kind,
|
||||
index as i32,
|
||||
)),
|
||||
));
|
||||
|
||||
if let Some(context_id) = self.context_menu {
|
||||
if index == context_id as usize {
|
||||
let menu_items = vec![menu::Item::Button("Delete", None, MenuMessage::Delete((model.kind, index as i32)))];
|
||||
let context_menu = context_menu(
|
||||
mouse_area,
|
||||
self.context_menu.map_or_else(|| None, |_| {
|
||||
Some(menu::items(&self.menu_keys,
|
||||
vec![menu::Item::Button("Delete", None, MenuMessage::Delete((model.kind, index as i32)))]))
|
||||
menu_items))
|
||||
})
|
||||
);
|
||||
Element::from(context_menu)
|
||||
|
|
@ -958,7 +994,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;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use crate::core::model::LibraryKind;
|
|||
pub mod double_ended_slider;
|
||||
pub mod library;
|
||||
pub mod presenter;
|
||||
pub mod service;
|
||||
pub mod slide_editor;
|
||||
pub mod song_editor;
|
||||
pub mod text_svg;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -62,6 +68,7 @@ pub(crate) enum Message {
|
|||
NextSlide,
|
||||
PrevSlide,
|
||||
SlideChange(Slide),
|
||||
ActivateSlide(usize, usize),
|
||||
EndVideo,
|
||||
StartVideo,
|
||||
StartAudio,
|
||||
|
|
@ -147,14 +154,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,
|
||||
|
|
@ -200,6 +215,19 @@ impl Presenter {
|
|||
// self.current_slide_index - 1,
|
||||
// ));
|
||||
}
|
||||
Message::ActivateSlide(item_index, slide_index) => {
|
||||
if let Some(slide) = self
|
||||
.service
|
||||
.get(item_index)
|
||||
.map(|item| item.slides.get(slide_index))
|
||||
.flatten()
|
||||
{
|
||||
self.current_item = item_index;
|
||||
self.current_slide_index = slide_index;
|
||||
return self
|
||||
.update(Message::SlideChange(slide.clone()));
|
||||
}
|
||||
}
|
||||
Message::SlideChange(slide) => {
|
||||
let slide_text = slide.text();
|
||||
debug!(slide_text, "slide changed");
|
||||
|
|
@ -221,10 +249,34 @@ impl Presenter {
|
|||
self.reset_video();
|
||||
}
|
||||
|
||||
let mut target_item = 0;
|
||||
|
||||
self.service.iter().enumerate().try_for_each(
|
||||
|(index, item)| {
|
||||
item.slides.iter().enumerate().try_for_each(
|
||||
|(slide_index, _)| {
|
||||
target_item += 1;
|
||||
if (index, slide_index)
|
||||
== (
|
||||
self.current_item,
|
||||
self.current_slide_index,
|
||||
)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(())
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
debug!(target_item);
|
||||
|
||||
let offset = AbsoluteOffset {
|
||||
x: {
|
||||
if self.current_slide_index > 2 {
|
||||
(self.current_slide_index as f32)
|
||||
if target_item > 2 {
|
||||
(target_item as f32)
|
||||
.mul_add(187.5, -187.5)
|
||||
} else {
|
||||
0.0
|
||||
|
|
@ -232,7 +284,7 @@ impl Presenter {
|
|||
},
|
||||
y: 0.0,
|
||||
};
|
||||
debug!(?offset);
|
||||
|
||||
let mut tasks = vec![];
|
||||
tasks.push(scroll_to(self.scroll_id.clone(), offset));
|
||||
|
||||
|
|
@ -399,21 +451,11 @@ impl Presenter {
|
|||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
slide_view(
|
||||
self.current_slide.clone(),
|
||||
&self.video,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
slide_view(&self.current_slide, &self.video, false, true)
|
||||
}
|
||||
|
||||
pub fn view_preview(&self) -> Element<Message> {
|
||||
slide_view(
|
||||
self.current_slide.clone(),
|
||||
&self.video,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
slide_view(&self.current_slide, &self.video, false, false)
|
||||
}
|
||||
|
||||
pub fn preview_bar(&self) -> Element<Message> {
|
||||
|
|
@ -423,19 +465,6 @@ impl Presenter {
|
|||
let mut slides = vec![];
|
||||
item.slides.iter().enumerate().for_each(
|
||||
|(slide_index, slide)| {
|
||||
let font_name = slide.font().into_boxed_str();
|
||||
let family =
|
||||
Family::Name(Box::leak(font_name));
|
||||
let weight = Weight::Normal;
|
||||
let stretch = Stretch::Normal;
|
||||
let style = Style::Normal;
|
||||
let font = Font {
|
||||
family,
|
||||
weight,
|
||||
stretch,
|
||||
style,
|
||||
};
|
||||
|
||||
let is_current_slide =
|
||||
(item_index, slide_index)
|
||||
== (
|
||||
|
|
@ -444,7 +473,7 @@ impl Presenter {
|
|||
);
|
||||
|
||||
let container = slide_view(
|
||||
slide.clone(),
|
||||
&slide,
|
||||
&self.video,
|
||||
true,
|
||||
false,
|
||||
|
|
@ -518,8 +547,9 @@ impl Presenter {
|
|||
)))
|
||||
})
|
||||
.on_exit(Message::HoveredSlide(None))
|
||||
.on_press(Message::SlideChange(
|
||||
slide.clone(),
|
||||
.on_press(Message::ActivateSlide(
|
||||
item_index,
|
||||
slide_index,
|
||||
));
|
||||
slides.push(delegate.into());
|
||||
},
|
||||
|
|
@ -705,7 +735,7 @@ fn scale_font(font_size: f32, width: f32) -> f32 {
|
|||
}
|
||||
|
||||
pub(crate) fn slide_view<'a>(
|
||||
slide: Slide,
|
||||
slide: &'a Slide,
|
||||
video: &'a Option<Video>,
|
||||
delegate: bool,
|
||||
hide_mouse: bool,
|
||||
|
|
@ -775,7 +805,7 @@ pub(crate) fn slide_view<'a>(
|
|||
})
|
||||
.content_fit(ContentFit::Cover),
|
||||
)
|
||||
.center(Length::Shrink)
|
||||
.center(Length::Fill)
|
||||
.clip(true)
|
||||
// Container::new(Space::new(0, 0))
|
||||
} else {
|
||||
|
|
|
|||
353
src/ui/service.rs
Normal file
353
src/ui/service.rs
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
use cosmic::iced::Size;
|
||||
|
||||
use cosmic::iced_core::widget::tree;
|
||||
use cosmic::{
|
||||
iced::{
|
||||
clipboard::dnd::{DndEvent, SourceEvent},
|
||||
event, mouse, Event, Length, Point, Rectangle, Vector,
|
||||
},
|
||||
iced_core::{
|
||||
self, image::Renderer, layout, renderer, widget::Tree,
|
||||
Clipboard, Shell,
|
||||
},
|
||||
widget::Widget,
|
||||
Element,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::core::service_items::ServiceItem;
|
||||
|
||||
pub fn service<'a, Message: Clone + 'static>(
|
||||
service: &'a Vec<ServiceItem>,
|
||||
) -> Service<'a, Message> {
|
||||
Service::new(service)
|
||||
}
|
||||
|
||||
pub struct Service<'a, Message> {
|
||||
service: &'a Vec<ServiceItem>,
|
||||
on_start: Option<Message>,
|
||||
on_cancelled: Option<Message>,
|
||||
on_finish: Option<Message>,
|
||||
drag_threshold: f32,
|
||||
width: Length,
|
||||
height: Length,
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'static> Service<'a, Message> {
|
||||
pub fn new(service: &'a Vec<ServiceItem>) -> Self {
|
||||
Self {
|
||||
service,
|
||||
drag_threshold: 8.0,
|
||||
on_start: None,
|
||||
on_cancelled: None,
|
||||
on_finish: None,
|
||||
width: Length::Fill,
|
||||
height: Length::Fill,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn drag_threshold(mut self, threshold: f32) -> Self {
|
||||
self.drag_threshold = threshold;
|
||||
self
|
||||
}
|
||||
|
||||
// pub fn start_dnd(
|
||||
// &self,
|
||||
// clipboard: &mut dyn Clipboard,
|
||||
// bounds: Rectangle,
|
||||
// offset: Vector,
|
||||
// ) {
|
||||
// let Some(content) = self.drag_content.as_ref().map(|f| f())
|
||||
// else {
|
||||
// return;
|
||||
// };
|
||||
|
||||
// iced_core::clipboard::start_dnd(
|
||||
// clipboard,
|
||||
// false,
|
||||
// if let Some(window) = self.window.as_ref() {
|
||||
// Some(iced_core::clipboard::DndSource::Surface(
|
||||
// *window,
|
||||
// ))
|
||||
// } else {
|
||||
// Some(iced_core::clipboard::DndSource::Widget(
|
||||
// self.id.clone(),
|
||||
// ))
|
||||
// },
|
||||
// self.drag_icon.as_ref().map(|f| {
|
||||
// let (icon, state, offset) = f(offset);
|
||||
// iced_core::clipboard::IconSurface::new(
|
||||
// container(icon)
|
||||
// .width(Length::Fixed(bounds.width))
|
||||
// .height(Length::Fixed(bounds.height))
|
||||
// .into(),
|
||||
// state,
|
||||
// offset,
|
||||
// )
|
||||
// }),
|
||||
// Box::new(content),
|
||||
// DndAction::Move,
|
||||
// );
|
||||
// }
|
||||
|
||||
pub fn on_start(mut self, on_start: Option<Message>) -> Self {
|
||||
self.on_start = on_start;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_cancel(
|
||||
mut self,
|
||||
on_cancelled: Option<Message>,
|
||||
) -> Self {
|
||||
self.on_cancelled = on_cancelled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_finish(mut self, on_finish: Option<Message>) -> Self {
|
||||
self.on_finish = on_finish;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message: Clone + 'static>
|
||||
Widget<Message, cosmic::Theme, cosmic::Renderer>
|
||||
for Service<'_, Message>
|
||||
{
|
||||
fn size(&self) -> iced_core::Size<Length> {
|
||||
Size {
|
||||
width: Length::Fill,
|
||||
height: Length::Fill,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_tree: &mut Tree,
|
||||
_renderer: &cosmic::Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
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,
|
||||
// layout: layout::Layout<'_>,
|
||||
// renderer: &cosmic::Renderer,
|
||||
// operation: &mut dyn iced_core::widget::Operation<()>,
|
||||
// ) {
|
||||
// operation.custom(
|
||||
// (&mut tree.state) as &mut dyn Any,
|
||||
// Some(&self.id),
|
||||
// );
|
||||
// operation.container(
|
||||
// Some(&self.id),
|
||||
// layout.bounds(),
|
||||
// &mut |operation| {
|
||||
// self.container.as_widget().operate(
|
||||
// &mut tree.children[0],
|
||||
// layout,
|
||||
// renderer,
|
||||
// operation,
|
||||
// )
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: layout::Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &cosmic::Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
let state = tree.state.downcast_mut::<State>();
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse_event) => match mouse_event {
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left) => {
|
||||
if let Some(position) = cursor.position() {
|
||||
if !state.hovered {
|
||||
return event::Status::Ignored;
|
||||
}
|
||||
|
||||
state.left_pressed_position = Some(position);
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
mouse::Event::ButtonReleased(mouse::Button::Left)
|
||||
if state.left_pressed_position.is_some() =>
|
||||
{
|
||||
state.left_pressed_position = None;
|
||||
return event::Status::Captured;
|
||||
}
|
||||
mouse::Event::CursorMoved { .. } => {
|
||||
if let Some(position) = cursor.position() {
|
||||
if state.hovered {
|
||||
// We ignore motion if we do not possess drag content by now.
|
||||
if let Some(left_pressed_position) =
|
||||
state.left_pressed_position
|
||||
{
|
||||
if position
|
||||
.distance(left_pressed_position)
|
||||
> self.drag_threshold
|
||||
{
|
||||
if let Some(on_start) =
|
||||
self.on_start.as_ref()
|
||||
{
|
||||
shell
|
||||
.publish(on_start.clone())
|
||||
}
|
||||
let offset = Vector::new(
|
||||
left_pressed_position.x
|
||||
- layout.bounds().x,
|
||||
left_pressed_position.y
|
||||
- layout.bounds().y,
|
||||
);
|
||||
state.is_dragging = true;
|
||||
state.left_pressed_position =
|
||||
None;
|
||||
}
|
||||
}
|
||||
if !cursor.is_over(layout.bounds()) {
|
||||
state.hovered = false;
|
||||
|
||||
return event::Status::Ignored;
|
||||
}
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
state.hovered = true;
|
||||
}
|
||||
return event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => 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());
|
||||
}
|
||||
state.is_dragging = false;
|
||||
return event::Status::Captured;
|
||||
}
|
||||
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());
|
||||
}
|
||||
state.is_dragging = false;
|
||||
return event::Status::Captured;
|
||||
}
|
||||
return event::Status::Ignored;
|
||||
}
|
||||
Event::Dnd(event) => debug!(?event),
|
||||
_ => return event::Status::Ignored,
|
||||
}
|
||||
event::Status::Ignored
|
||||
}
|
||||
|
||||
// fn mouse_interaction(
|
||||
// &self,
|
||||
// tree: &Tree,
|
||||
// layout: layout::Layout<'_>,
|
||||
// cursor_position: mouse::Cursor,
|
||||
// viewport: &Rectangle,
|
||||
// renderer: &cosmic::Renderer,
|
||||
// ) -> mouse::Interaction {
|
||||
// let state = tree.state.downcast_ref::<State>();
|
||||
// if state.is_dragging {
|
||||
// return mouse::Interaction::Grabbing;
|
||||
// }
|
||||
// self.container.as_widget().mouse_interaction(
|
||||
// &tree.children[0],
|
||||
// layout,
|
||||
// cursor_position,
|
||||
// viewport,
|
||||
// renderer,
|
||||
// )
|
||||
// }
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut cosmic::Renderer,
|
||||
theme: &cosmic::Theme,
|
||||
renderer_style: &renderer::Style,
|
||||
layout: layout::Layout<'_>,
|
||||
cursor_position: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
// let state = tree.state.downcast_mut::<State>();
|
||||
for item in self.service {}
|
||||
}
|
||||
|
||||
// fn overlay<'b>(
|
||||
// &'b mut self,
|
||||
// tree: &'b mut Tree,
|
||||
// layout: layout::Layout<'_>,
|
||||
// renderer: &cosmic::Renderer,
|
||||
// translation: Vector,
|
||||
// ) -> Option<
|
||||
// overlay::Element<
|
||||
// 'b,
|
||||
// Message,
|
||||
// cosmic::Theme,
|
||||
// cosmic::Renderer,
|
||||
// >,
|
||||
// > {
|
||||
// self.container.as_widget_mut().overlay(
|
||||
// &mut tree.children[0],
|
||||
// layout,
|
||||
// renderer,
|
||||
// translation,
|
||||
// )
|
||||
// }
|
||||
|
||||
// #[cfg(feature = "a11y")]
|
||||
// /// get the a11y nodes for the widget
|
||||
// fn a11y_nodes(
|
||||
// &self,
|
||||
// layout: iced_core::Layout<'_>,
|
||||
// state: &Tree,
|
||||
// p: mouse::Cursor,
|
||||
// ) -> iced_accessibility::A11yTree {
|
||||
// let c_state = &state.children[0];
|
||||
// self.container.as_widget().a11y_nodes(layout, c_state, p)
|
||||
// }
|
||||
}
|
||||
|
||||
impl<'a, Message: Clone + 'static> From<Service<'a, Message>>
|
||||
for Element<'a, Message>
|
||||
{
|
||||
fn from(e: Service<'a, Message>) -> Element<'a, Message> {
|
||||
Element::new(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Local state of the [`MouseListener`].
|
||||
#[derive(Debug, Default)]
|
||||
struct State {
|
||||
hovered: bool,
|
||||
left_pressed_position: Option<Point>,
|
||||
is_dragging: bool,
|
||||
cached_bounds: Rectangle,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +1,28 @@
|
|||
use std::{io, path::PathBuf, sync::Arc};
|
||||
|
||||
use cosmic::{
|
||||
dialog::file_chooser::open::Dialog,
|
||||
iced::{
|
||||
font::{Family, Stretch, Style, Weight},
|
||||
Font, Length,
|
||||
},
|
||||
dialog::file_chooser::{open::Dialog, FileFilter},
|
||||
iced::{alignment::Vertical, Length},
|
||||
iced_wgpu::graphics::text::cosmic_text::fontdb,
|
||||
iced_widget::row,
|
||||
iced_widget::{column, row},
|
||||
theme,
|
||||
widget::{
|
||||
button, column, combo_box, container, horizontal_space, icon,
|
||||
scrollable, text, text_editor, text_input,
|
||||
button, combo_box, container, horizontal_space, icon,
|
||||
progress_bar, scrollable, text, text_editor, text_input,
|
||||
vertical_space,
|
||||
},
|
||||
Element, Task,
|
||||
};
|
||||
use dirs::font_dir;
|
||||
use iced_video_player::Video;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::{
|
||||
core::{service_items::ServiceTrait, songs::Song},
|
||||
ui::{presenter::slide_view, slide_editor::SlideEditor},
|
||||
core::{service_items::ServiceTrait, slide::Slide, songs::Song},
|
||||
ui::{
|
||||
presenter::slide_view, slide_editor::SlideEditor, text_svg,
|
||||
},
|
||||
Background, BackgroundKind,
|
||||
};
|
||||
|
||||
|
|
@ -30,7 +31,6 @@ pub struct SongEditor {
|
|||
pub song: Option<Song>,
|
||||
title: String,
|
||||
font_db: Arc<fontdb::Database>,
|
||||
fonts: Vec<(fontdb::ID, String)>,
|
||||
fonts_combo: combo_box::State<String>,
|
||||
font_sizes: combo_box::State<String>,
|
||||
font: String,
|
||||
|
|
@ -42,8 +42,8 @@ pub struct SongEditor {
|
|||
editing: bool,
|
||||
background: Option<Background>,
|
||||
video: Option<Video>,
|
||||
current_font: Font,
|
||||
ccli: String,
|
||||
song_slides: Option<Vec<Slide>>,
|
||||
slide_state: SlideEditor,
|
||||
}
|
||||
|
||||
|
|
@ -67,29 +67,19 @@ pub enum Message {
|
|||
Edit(bool),
|
||||
None,
|
||||
ChangeAuthor(String),
|
||||
PauseVideo,
|
||||
}
|
||||
|
||||
impl SongEditor {
|
||||
pub fn new(font_db: Arc<fontdb::Database>) -> Self {
|
||||
let fonts = font_dir();
|
||||
debug!(?fonts);
|
||||
let mut fonts: Vec<(fontdb::ID, String)> = font_db
|
||||
let mut fonts: Vec<String> = font_db
|
||||
.faces()
|
||||
.map(|f| {
|
||||
let id = f.id;
|
||||
let font_base_name: String =
|
||||
f.families.iter().map(|f| f.0.clone()).collect();
|
||||
let font_weight = f.weight;
|
||||
let font_style = f.style;
|
||||
let font_stretch = f.stretch;
|
||||
(id, font_base_name)
|
||||
})
|
||||
.map(|f| f.families[0].0.clone())
|
||||
.collect();
|
||||
fonts.dedup();
|
||||
// let fonts = vec![
|
||||
// String::from("Quicksand"),
|
||||
// String::from("Noto Sans"),
|
||||
// ];
|
||||
fonts.sort();
|
||||
let font_sizes = vec![
|
||||
"5".to_string(),
|
||||
"6".to_string(),
|
||||
|
|
@ -111,33 +101,51 @@ impl SongEditor {
|
|||
"65".to_string(),
|
||||
"70".to_string(),
|
||||
"80".to_string(),
|
||||
"90".to_string(),
|
||||
"100".to_string(),
|
||||
"110".to_string(),
|
||||
"120".to_string(),
|
||||
"130".to_string(),
|
||||
"140".to_string(),
|
||||
"150".to_string(),
|
||||
"160".to_string(),
|
||||
"170".to_string(),
|
||||
];
|
||||
let font_texts = fonts.iter().map(|f| f.1.clone()).collect();
|
||||
Self {
|
||||
song: None,
|
||||
font_db,
|
||||
fonts,
|
||||
fonts_combo: combo_box::State::new(font_texts),
|
||||
title: "Death was Arrested".to_owned(),
|
||||
font: "Quicksand".to_owned(),
|
||||
fonts_combo: combo_box::State::new(fonts),
|
||||
title: "Death was Arrested".to_string(),
|
||||
font: "Quicksand".to_string(),
|
||||
font_size: 16,
|
||||
font_sizes: combo_box::State::new(font_sizes),
|
||||
verse_order: "Death was Arrested".to_owned(),
|
||||
verse_order: "Death was Arrested".to_string(),
|
||||
lyrics: text_editor::Content::new(),
|
||||
editing: false,
|
||||
author: "North Point Worship".into(),
|
||||
audio: PathBuf::new(),
|
||||
background: None,
|
||||
video: None,
|
||||
current_font: cosmic::font::default(),
|
||||
ccli: "8".to_owned(),
|
||||
ccli: "8".to_string(),
|
||||
slide_state: SlideEditor::default(),
|
||||
song_slides: None,
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self, message: Message) -> Action {
|
||||
match message {
|
||||
Message::ChangeSong(song) => {
|
||||
self.song = Some(song.clone());
|
||||
self.song_slides = song.to_slides().ok().map(|v| {
|
||||
v.into_par_iter()
|
||||
.map(|mut s| {
|
||||
text_svg::text_svg_generator(
|
||||
&mut s,
|
||||
Arc::clone(&self.font_db),
|
||||
);
|
||||
s
|
||||
})
|
||||
.collect::<Vec<Slide>>()
|
||||
});
|
||||
self.title = song.title;
|
||||
if let Some(font) = song.font {
|
||||
self.font = font;
|
||||
|
|
@ -171,35 +179,21 @@ impl SongEditor {
|
|||
self.background = song.background;
|
||||
}
|
||||
Message::ChangeFont(font) => {
|
||||
let font_id = self
|
||||
.fonts
|
||||
.iter()
|
||||
.filter(|f| f.1 == font)
|
||||
.map(|f| f.0)
|
||||
.next();
|
||||
if let Some(id) = font_id
|
||||
&& let Some(face) = self.font_db.face(id)
|
||||
{
|
||||
self.font = face.post_script_name.clone();
|
||||
// self.current_font = Font::from(face);
|
||||
}
|
||||
self.font = font.clone();
|
||||
|
||||
let font_name = font.into_boxed_str();
|
||||
let family = Family::Name(Box::leak(font_name));
|
||||
let weight = Weight::Bold;
|
||||
let stretch = Stretch::Normal;
|
||||
let style = Style::Normal;
|
||||
let font = Font {
|
||||
family,
|
||||
weight,
|
||||
stretch,
|
||||
style,
|
||||
};
|
||||
self.current_font = font;
|
||||
// return self.update_song(song);
|
||||
if let Some(song) = &mut self.song {
|
||||
song.font = Some(font);
|
||||
let song = song.to_owned();
|
||||
return self.update_song(song);
|
||||
}
|
||||
}
|
||||
Message::ChangeFontSize(size) => {
|
||||
self.font_size = size;
|
||||
if let Some(song) = &mut self.song {
|
||||
song.font_size = Some(size as i32);
|
||||
let song = song.to_owned();
|
||||
return self.update_song(song);
|
||||
}
|
||||
}
|
||||
Message::ChangeFontSize(size) => self.font_size = size,
|
||||
Message::ChangeTitle(title) => {
|
||||
self.title = title.clone();
|
||||
if let Some(song) = &mut self.song {
|
||||
|
|
@ -259,66 +253,92 @@ impl SongEditor {
|
|||
Message::ChangeBackground,
|
||||
));
|
||||
}
|
||||
Message::PauseVideo => {
|
||||
if let Some(video) = &mut self.video {
|
||||
let paused = video.paused();
|
||||
video.set_paused(!paused);
|
||||
};
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Action::None
|
||||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
let video_elements = if let Some(video) = &self.video {
|
||||
let play_button = button::icon(if video.paused() {
|
||||
icon::from_name("media-playback-start")
|
||||
} else {
|
||||
icon::from_name("media-playback-pause")
|
||||
})
|
||||
.on_press(Message::PauseVideo);
|
||||
let video_track = progress_bar(
|
||||
0.0..=video.duration().as_secs_f32(),
|
||||
video.position().as_secs_f32(),
|
||||
)
|
||||
.height(cosmic::theme::spacing().space_s)
|
||||
.width(Length::Fill);
|
||||
container(
|
||||
row![play_button, video_track]
|
||||
.align_y(Vertical::Center)
|
||||
.spacing(cosmic::theme::spacing().space_m),
|
||||
)
|
||||
.padding(cosmic::theme::spacing().space_s)
|
||||
.center_x(Length::FillPortion(2))
|
||||
} else {
|
||||
container(vertical_space())
|
||||
};
|
||||
let slide_preview = container(self.slide_preview())
|
||||
.width(Length::FillPortion(2));
|
||||
|
||||
let column = column::with_children(vec![
|
||||
let slide_section = column![video_elements, slide_preview]
|
||||
.spacing(cosmic::theme::spacing().space_s);
|
||||
let column = column![
|
||||
self.toolbar(),
|
||||
row![
|
||||
container(self.left_column())
|
||||
.center_x(Length::FillPortion(2)),
|
||||
container(slide_preview)
|
||||
.center_x(Length::FillPortion(3))
|
||||
]
|
||||
.into(),
|
||||
])
|
||||
container(slide_section)
|
||||
.center_x(Length::FillPortion(2))
|
||||
],
|
||||
]
|
||||
.spacing(theme::active().cosmic().space_l());
|
||||
column.into()
|
||||
}
|
||||
|
||||
fn slide_preview(&self) -> Element<Message> {
|
||||
if let Some(song) = &self.song {
|
||||
if let Ok(slides) = song.to_slides() {
|
||||
let slides: Vec<Element<Message>> = slides
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, slide)| {
|
||||
container(
|
||||
slide_view(
|
||||
slide,
|
||||
if index == 0 {
|
||||
&self.video
|
||||
} else {
|
||||
&None
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.map(|_| Message::None),
|
||||
if let Some(slides) = &self.song_slides {
|
||||
let slides: Vec<Element<Message>> = slides
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, slide)| {
|
||||
container(
|
||||
slide_view(
|
||||
&slide,
|
||||
if index == 0 {
|
||||
&self.video
|
||||
} else {
|
||||
&None
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.height(250)
|
||||
.center_x(Length::Fill)
|
||||
.padding([0, 20])
|
||||
.clip(true)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
scrollable(
|
||||
column::with_children(slides)
|
||||
.spacing(theme::active().cosmic().space_l()),
|
||||
)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
horizontal_space().into()
|
||||
}
|
||||
.map(|_| Message::None),
|
||||
)
|
||||
.height(250)
|
||||
.center_x(Length::Fill)
|
||||
.padding([0, 20])
|
||||
.clip(true)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
scrollable(
|
||||
cosmic::widget::column::with_children(slides)
|
||||
.spacing(theme::active().cosmic().space_l()),
|
||||
)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
} else {
|
||||
horizontal_space().into()
|
||||
}
|
||||
|
|
@ -345,37 +365,26 @@ order",
|
|||
.on_input(Message::ChangeVerseOrder);
|
||||
|
||||
let lyric_title = text("Lyrics");
|
||||
let lyric_input = column::with_children(vec![
|
||||
lyric_title.into(),
|
||||
let lyric_input = column![
|
||||
lyric_title,
|
||||
text_editor(&self.lyrics)
|
||||
.on_action(Message::ChangeLyrics)
|
||||
.height(Length::Fill)
|
||||
.into(),
|
||||
])
|
||||
]
|
||||
.spacing(5);
|
||||
|
||||
column::with_children(vec![
|
||||
title_input.into(),
|
||||
author_input.into(),
|
||||
verse_input.into(),
|
||||
lyric_input.into(),
|
||||
])
|
||||
.spacing(25)
|
||||
.width(Length::FillPortion(2))
|
||||
.into()
|
||||
column![title_input, author_input, verse_input, lyric_input,]
|
||||
.spacing(25)
|
||||
.width(Length::FillPortion(2))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn toolbar(&self) -> Element<Message> {
|
||||
let selected_font = &self.font;
|
||||
let selected_font_size = {
|
||||
let font_size_position = self
|
||||
.font_sizes
|
||||
.options()
|
||||
.iter()
|
||||
.position(|s| *s == self.font_size.to_string());
|
||||
self.font_sizes
|
||||
.options()
|
||||
.get(font_size_position.unwrap_or_default())
|
||||
let selected_font_size = if self.font_size > 0 {
|
||||
Some(&self.font_size.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let font_selector = combo_box(
|
||||
&self.fonts_combo,
|
||||
|
|
@ -383,7 +392,7 @@ order",
|
|||
Some(selected_font),
|
||||
Message::ChangeFont,
|
||||
)
|
||||
.width(200);
|
||||
.width(300);
|
||||
let font_size = combo_box(
|
||||
&self.font_sizes,
|
||||
"Font Size",
|
||||
|
|
@ -405,11 +414,14 @@ order",
|
|||
.padding(10);
|
||||
|
||||
row![
|
||||
text::body("Font:"),
|
||||
font_selector,
|
||||
text::body("Font Size:"),
|
||||
font_size,
|
||||
horizontal_space(),
|
||||
background_selector
|
||||
]
|
||||
.align_y(Vertical::Center)
|
||||
.spacing(10)
|
||||
.into()
|
||||
}
|
||||
|
|
@ -420,6 +432,17 @@ order",
|
|||
|
||||
fn update_song(&mut self, song: Song) -> Action {
|
||||
self.song = Some(song.clone());
|
||||
self.song_slides = song.to_slides().ok().map(|v| {
|
||||
v.into_par_iter()
|
||||
.map(|mut s| {
|
||||
text_svg::text_svg_generator(
|
||||
&mut s,
|
||||
Arc::clone(&self.font_db),
|
||||
);
|
||||
s
|
||||
})
|
||||
.collect::<Vec<Slide>>()
|
||||
});
|
||||
Action::UpdateSong(song)
|
||||
}
|
||||
|
||||
|
|
@ -448,21 +471,38 @@ impl Default for SongEditor {
|
|||
|
||||
async fn pick_background() -> Result<PathBuf, SongError> {
|
||||
let dialog = Dialog::new().title("Choose a background...");
|
||||
let bg_filter = FileFilter::new("Videos and Images")
|
||||
.extension("png")
|
||||
.extension("jpg")
|
||||
.extension("mp4")
|
||||
.extension("webm")
|
||||
.extension("mkv")
|
||||
.extension("jpeg");
|
||||
dialog
|
||||
.filter(bg_filter)
|
||||
.directory(dirs::home_dir().expect("oops"))
|
||||
.open_file()
|
||||
.await
|
||||
.map_err(|_| SongError::DialogClosed)
|
||||
.map_err(|e| {
|
||||
error!(?e);
|
||||
SongError::BackgroundDialogClosed
|
||||
})
|
||||
.map(|file| file.url().to_file_path().unwrap())
|
||||
// rfd::AsyncFileDialog::new()
|
||||
// .set_title("Choose a background...")
|
||||
// .add_filter(
|
||||
// "Images and Videos",
|
||||
// &["png", "jpeg", "mp4", "webm", "mkv", "jpg", "mpeg"],
|
||||
// )
|
||||
// .set_directory(dirs::home_dir().unwrap())
|
||||
// .pick_file()
|
||||
// .await
|
||||
// .ok_or(SongError::DialogClosed)
|
||||
// .ok_or(SongError::BackgroundDialogClosed)
|
||||
// .map(|file| file.path().to_owned())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SongError {
|
||||
DialogClosed,
|
||||
BackgroundDialogClosed,
|
||||
IOError(io::ErrorKind),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -249,18 +249,20 @@ impl TextSvg {
|
|||
}
|
||||
|
||||
pub fn build(mut self) -> Self {
|
||||
debug!("starting...");
|
||||
// debug!("starting...");
|
||||
|
||||
let mut path = dirs::data_local_dir().unwrap();
|
||||
path.push(PathBuf::from("lumina"));
|
||||
path.push(PathBuf::from("temp"));
|
||||
|
||||
let shadow = if let Some(shadow) = &self.shadow {
|
||||
format!("<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
||||
format!(
|
||||
"<filter id=\"shadow\"><feDropShadow dx=\"{}\" dy=\"{}\" stdDeviation=\"{}\" flood-color=\"{}\"/></filter>",
|
||||
shadow.offset_x,
|
||||
shadow.offset_y,
|
||||
shadow.spread,
|
||||
shadow.color)
|
||||
shadow.color
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
|
@ -299,20 +301,24 @@ impl TextSvg {
|
|||
.collect();
|
||||
let text: String = text_pieces.join("\n");
|
||||
|
||||
let final_svg = format!("<svg viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\"><defs>{}</defs><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-weight=\"bold\" font-family=\"{}\" font-size=\"{}\" fill=\"{}\" {} style=\"filter:url(#shadow);\">{}</text></svg>",
|
||||
size.width,
|
||||
size.height,
|
||||
shadow,
|
||||
self.font.name,
|
||||
font_size,
|
||||
self.fill, stroke, text);
|
||||
let final_svg = format!(
|
||||
"<svg viewBox=\"0 0 {} {}\" xmlns=\"http://www.w3.org/2000/svg\"><defs>{}</defs><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-weight=\"bold\" font-family=\"{}\" font-size=\"{}\" fill=\"{}\" {} style=\"filter:url(#shadow);\">{}</text></svg>",
|
||||
size.width,
|
||||
size.height,
|
||||
shadow,
|
||||
self.font.name,
|
||||
font_size,
|
||||
self.fill,
|
||||
stroke,
|
||||
text
|
||||
);
|
||||
|
||||
let hashed_title = rapidhash_v3(final_svg.as_bytes());
|
||||
path.push(PathBuf::from(hashed_title.to_string()));
|
||||
path.set_extension("png");
|
||||
|
||||
if path.exists() {
|
||||
debug!("cached");
|
||||
// debug!("cached");
|
||||
let handle = Handle::from_path(path);
|
||||
self.handle = Some(handle);
|
||||
return self;
|
||||
|
|
@ -354,14 +360,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 +406,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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
917
src/ui/widgets/draggable/column.rs
Normal file
917
src/ui/widgets/draggable/column.rs
Normal file
|
|
@ -0,0 +1,917 @@
|
|||
//! Distribute draggable content vertically.
|
||||
// This widget is a modification of the original `Column` widget from [`iced`]
|
||||
//
|
||||
// [`iced`]: https://github.com/iced-rs/iced
|
||||
//
|
||||
// Copyright 2019 Héctor Ramón, Iced contributors
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
use cosmic::iced::advanced::layout::{self, Layout};
|
||||
use cosmic::iced::advanced::widget::{tree, Operation, Tree, Widget};
|
||||
use cosmic::iced::advanced::{overlay, renderer, Clipboard, Shell};
|
||||
use cosmic::iced::alignment::{self, Alignment};
|
||||
use cosmic::iced::event::{self, Event};
|
||||
use cosmic::iced::{self, mouse, Transformation};
|
||||
use cosmic::iced::{
|
||||
Background, Border, Color, Element, Length, Padding, Pixels,
|
||||
Point, Rectangle, Size, Vector,
|
||||
};
|
||||
use cosmic::Theme;
|
||||
|
||||
use super::{Action, DragEvent, DropPosition};
|
||||
|
||||
pub fn column<'a, Message, Theme, Renderer>(
|
||||
children: impl IntoIterator<
|
||||
Item = Element<'a, Message, Theme, Renderer>,
|
||||
>,
|
||||
) -> Column<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
Column::with_children(children)
|
||||
}
|
||||
|
||||
const DRAG_DEADBAND_DISTANCE: f32 = 5.0;
|
||||
|
||||
/// A container that distributes its contents vertically.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// use iced::widget::{button, column};
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// column![
|
||||
/// "I am on top!",
|
||||
/// button("I am in the center!"),
|
||||
/// "I am below.",
|
||||
/// ].into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Column<
|
||||
'a,
|
||||
Message,
|
||||
Theme = cosmic::Theme,
|
||||
Renderer = iced::Renderer,
|
||||
> where
|
||||
Theme: Catalog,
|
||||
{
|
||||
spacing: f32,
|
||||
padding: Padding,
|
||||
width: Length,
|
||||
height: Length,
|
||||
max_width: f32,
|
||||
align: Alignment,
|
||||
clip: bool,
|
||||
deadband_zone: f32,
|
||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer>
|
||||
Column<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
/// Creates an empty [`Column`].
|
||||
pub fn new() -> Self {
|
||||
Self::from_vec(Vec::new())
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] with the given capacity.
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self::from_vec(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] with the given elements.
|
||||
pub fn with_children(
|
||||
children: impl IntoIterator<
|
||||
Item = Element<'a, Message, Theme, Renderer>,
|
||||
>,
|
||||
) -> Self {
|
||||
let iterator = children.into_iter();
|
||||
|
||||
Self::with_capacity(iterator.size_hint().0).extend(iterator)
|
||||
}
|
||||
|
||||
/// Creates a [`Column`] from an already allocated [`Vec`].
|
||||
///
|
||||
/// Keep in mind that the [`Column`] will not inspect the [`Vec`], which means
|
||||
/// it won't automatically adapt to the sizing strategy of its contents.
|
||||
///
|
||||
/// If any of the children have a [`Length::Fill`] strategy, you will need to
|
||||
/// call [`Column::width`] or [`Column::height`] accordingly.
|
||||
pub fn from_vec(
|
||||
children: Vec<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
spacing: 0.0,
|
||||
padding: Padding::ZERO,
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
max_width: f32::INFINITY,
|
||||
align: Alignment::Start,
|
||||
clip: false,
|
||||
deadband_zone: DRAG_DEADBAND_DISTANCE,
|
||||
children,
|
||||
class: Theme::default(),
|
||||
on_drag: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the vertical spacing _between_ elements.
|
||||
///
|
||||
/// Custom margins per element do not exist in iced. You should use this
|
||||
/// method instead! While less flexible, it helps you keep spacing between
|
||||
/// elements consistent.
|
||||
pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
|
||||
self.spacing = amount.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Column`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`Column`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the height of the [`Column`].
|
||||
pub fn height(mut self, height: impl Into<Length>) -> Self {
|
||||
self.height = height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum width of the [`Column`].
|
||||
pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self {
|
||||
self.max_width = max_width.into().0;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the horizontal alignment of the contents of the [`Column`] .
|
||||
pub fn align_x(
|
||||
mut self,
|
||||
align: impl Into<alignment::Horizontal>,
|
||||
) -> Self {
|
||||
self.align = Alignment::from(align.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the contents of the [`Column`] should be clipped on
|
||||
/// overflow.
|
||||
pub fn clip(mut self, clip: bool) -> Self {
|
||||
self.clip = clip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the drag deadband zone of the [`Column`].
|
||||
pub fn deadband_zone(mut self, deadband_zone: f32) -> Self {
|
||||
self.deadband_zone = deadband_zone;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an element to the [`Column`].
|
||||
pub fn push(
|
||||
mut self,
|
||||
child: impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
) -> Self {
|
||||
let child = child.into();
|
||||
let child_size = child.as_widget().size_hint();
|
||||
|
||||
self.width = self.width.enclose(child_size.width);
|
||||
self.height = self.height.enclose(child_size.height);
|
||||
|
||||
self.children.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an element to the [`Column`], if `Some`.
|
||||
pub fn push_maybe(
|
||||
self,
|
||||
child: Option<
|
||||
impl Into<Element<'a, Message, Theme, Renderer>>,
|
||||
>,
|
||||
) -> Self {
|
||||
if let Some(child) = child {
|
||||
self.push(child)
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Column`].
|
||||
#[must_use]
|
||||
pub fn style(
|
||||
mut self,
|
||||
style: impl Fn(&Theme) -> Style + 'a,
|
||||
) -> Self
|
||||
where
|
||||
Theme::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style class of the [`Column`].
|
||||
#[must_use]
|
||||
pub fn class(
|
||||
mut self,
|
||||
class: impl Into<Theme::Class<'a>>,
|
||||
) -> Self {
|
||||
self.class = class.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Extends the [`Column`] with the given children.
|
||||
pub fn extend(
|
||||
self,
|
||||
children: impl IntoIterator<
|
||||
Item = Element<'a, Message, Theme, Renderer>,
|
||||
>,
|
||||
) -> Self {
|
||||
children.into_iter().fold(self, Self::push)
|
||||
}
|
||||
|
||||
/// The message produced by the [`Column`] when a child is dragged.
|
||||
pub fn on_drag(
|
||||
mut self,
|
||||
on_reorder: impl Fn(DragEvent) -> Message + 'a,
|
||||
) -> Self {
|
||||
self.on_drag = Some(Box::new(on_reorder));
|
||||
self
|
||||
}
|
||||
|
||||
// Computes the index and position where a dragged item should be dropped.
|
||||
fn compute_target_index(
|
||||
&self,
|
||||
cursor_position: Point,
|
||||
layout: Layout<'_>,
|
||||
dragged_index: usize,
|
||||
) -> (usize, DropPosition) {
|
||||
let cursor_y = cursor_position.y;
|
||||
|
||||
for (i, child_layout) in layout.children().enumerate() {
|
||||
let bounds = child_layout.bounds();
|
||||
let y = bounds.y;
|
||||
let height = bounds.height;
|
||||
|
||||
if cursor_y >= y && cursor_y <= y + height {
|
||||
if i == dragged_index {
|
||||
// Cursor is over the dragged item itself
|
||||
return (i, DropPosition::Swap);
|
||||
}
|
||||
|
||||
let thickness = height / 4.0;
|
||||
let top_threshold = y + thickness;
|
||||
let bottom_threshold = y + height - thickness;
|
||||
|
||||
if cursor_y < top_threshold {
|
||||
// Near the top edge - insert above
|
||||
return (i, DropPosition::Before);
|
||||
} else if cursor_y > bottom_threshold {
|
||||
// Near the bottom edge - insert below
|
||||
return (i + 1, DropPosition::After);
|
||||
} else {
|
||||
// Middle area - swap
|
||||
return (i, DropPosition::Swap);
|
||||
}
|
||||
} else if cursor_y < y {
|
||||
// Cursor is above this child
|
||||
return (i, DropPosition::Before);
|
||||
}
|
||||
}
|
||||
|
||||
// Cursor is below all children
|
||||
(self.children.len(), DropPosition::After)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Renderer> Default
|
||||
for Column<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer: renderer::Renderer>
|
||||
FromIterator<Element<'a, Message, Theme, Renderer>>
|
||||
for Column<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn from_iter<
|
||||
T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
|
||||
>(
|
||||
iter: T,
|
||||
) -> Self {
|
||||
Self::with_children(iter)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Column<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: renderer::Renderer,
|
||||
Theme: Catalog,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<Action>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(Action::Idle)
|
||||
}
|
||||
|
||||
fn children(&self) -> Vec<Tree> {
|
||||
self.children.iter().map(Tree::new).collect()
|
||||
}
|
||||
|
||||
fn diff(&mut self, tree: &mut Tree) {
|
||||
tree.diff_children(self.children.as_mut_slice());
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let limits = limits.max_width(self.max_width);
|
||||
|
||||
layout::flex::resolve(
|
||||
layout::flex::Axis::Vertical,
|
||||
renderer,
|
||||
&limits,
|
||||
self.width,
|
||||
self.height,
|
||||
self.padding,
|
||||
self.spacing,
|
||||
self.align,
|
||||
&self.children,
|
||||
&mut tree.children,
|
||||
)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn Operation,
|
||||
) {
|
||||
operation.container(
|
||||
None,
|
||||
layout.bounds(),
|
||||
&mut |operation| {
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.for_each(|((child, state), c_layout)| {
|
||||
child.as_widget().operate(
|
||||
state,
|
||||
c_layout.with_virtual_offset(
|
||||
layout.virtual_offset(),
|
||||
),
|
||||
renderer,
|
||||
operation,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
let mut event_status = event::Status::Ignored;
|
||||
|
||||
let action = tree.state.downcast_mut::<Action>();
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(
|
||||
mouse::Button::Left,
|
||||
)) => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position_over(layout.bounds())
|
||||
{
|
||||
for (index, child_layout) in
|
||||
layout.children().enumerate()
|
||||
{
|
||||
if child_layout
|
||||
.bounds()
|
||||
.contains(cursor_position)
|
||||
{
|
||||
*action = Action::Picking {
|
||||
index,
|
||||
origin: cursor_position,
|
||||
};
|
||||
event_status = event::Status::Captured;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
|
||||
match *action {
|
||||
Action::Picking { index, origin } => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position()
|
||||
{
|
||||
if cursor_position.distance(origin)
|
||||
> self.deadband_zone
|
||||
{
|
||||
// Start dragging
|
||||
*action = Action::Dragging {
|
||||
index,
|
||||
origin,
|
||||
last_cursor: cursor_position,
|
||||
};
|
||||
if let Some(on_reorder) =
|
||||
&self.on_drag
|
||||
{
|
||||
shell.publish(on_reorder(
|
||||
DragEvent::Picked { index },
|
||||
));
|
||||
}
|
||||
event_status =
|
||||
event::Status::Captured;
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::Dragging { origin, index, .. } => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position()
|
||||
{
|
||||
*action = Action::Dragging {
|
||||
last_cursor: cursor_position,
|
||||
origin,
|
||||
index,
|
||||
};
|
||||
event_status = event::Status::Captured;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
)) => {
|
||||
match *action {
|
||||
Action::Dragging { index, .. } => {
|
||||
if let Some(cursor_position) =
|
||||
cursor.position()
|
||||
{
|
||||
let bounds = layout.bounds();
|
||||
if bounds.contains(cursor_position) {
|
||||
let (target_index, drop_position) =
|
||||
self.compute_target_index(
|
||||
cursor_position,
|
||||
layout,
|
||||
index,
|
||||
);
|
||||
|
||||
if let Some(on_reorder) =
|
||||
&self.on_drag
|
||||
{
|
||||
shell.publish(on_reorder(
|
||||
DragEvent::Dropped {
|
||||
index,
|
||||
target_index,
|
||||
drop_position,
|
||||
},
|
||||
));
|
||||
event_status =
|
||||
event::Status::Captured;
|
||||
}
|
||||
} else if let Some(on_reorder) =
|
||||
&self.on_drag
|
||||
{
|
||||
shell.publish(on_reorder(
|
||||
DragEvent::Canceled { index },
|
||||
));
|
||||
event_status =
|
||||
event::Status::Captured;
|
||||
}
|
||||
}
|
||||
*action = Action::Idle;
|
||||
}
|
||||
Action::Picking { .. } => {
|
||||
// Did not move enough to start dragging
|
||||
*action = Action::Idle;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let child_status = self
|
||||
.children
|
||||
.iter_mut()
|
||||
.zip(&mut tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), c_layout)| {
|
||||
child.as_widget_mut().on_event(
|
||||
state,
|
||||
event.clone(),
|
||||
c_layout
|
||||
.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge);
|
||||
|
||||
event::Status::merge(event_status, child_status)
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let action = tree.state.downcast_ref::<Action>();
|
||||
|
||||
if let Action::Dragging { .. } = *action {
|
||||
return mouse::Interaction::Grabbing;
|
||||
}
|
||||
|
||||
self.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.map(|((child, state), c_layout)| {
|
||||
child.as_widget().mouse_interaction(
|
||||
state,
|
||||
c_layout
|
||||
.with_virtual_offset(layout.virtual_offset()),
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
})
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
default_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let action = tree.state.downcast_ref::<Action>();
|
||||
let style = theme.style(&self.class);
|
||||
|
||||
match action {
|
||||
Action::Dragging {
|
||||
index,
|
||||
last_cursor,
|
||||
origin,
|
||||
..
|
||||
} => {
|
||||
let child_count = self.children.len();
|
||||
|
||||
// Determine the target index based on cursor position
|
||||
let target_index = if cursor.position().is_some() {
|
||||
let (target_index, _) = self
|
||||
.compute_target_index(
|
||||
*last_cursor,
|
||||
layout,
|
||||
*index,
|
||||
);
|
||||
target_index.min(child_count - 1)
|
||||
} else {
|
||||
*index
|
||||
};
|
||||
|
||||
// Store the width of the dragged item
|
||||
let drag_bounds =
|
||||
layout.children().nth(*index).unwrap().bounds();
|
||||
let drag_height = drag_bounds.height + self.spacing;
|
||||
|
||||
// Draw all children except the one being dragged
|
||||
let mut translations = 0.0;
|
||||
for i in 0..child_count {
|
||||
let child = &self.children[i];
|
||||
let state = &tree.children[i];
|
||||
let child_layout =
|
||||
layout.children().nth(i).unwrap();
|
||||
|
||||
// Draw the dragged item separately
|
||||
// TODO: Draw a shadow below the picked item to enhance the
|
||||
// floating effect
|
||||
if i == *index {
|
||||
let scaling =
|
||||
Transformation::scale(style.scale);
|
||||
let translation =
|
||||
*last_cursor - *origin * scaling;
|
||||
renderer.with_translation(
|
||||
translation,
|
||||
|renderer| {
|
||||
renderer.with_transformation(
|
||||
scaling,
|
||||
|renderer| {
|
||||
renderer.with_layer(
|
||||
child_layout.bounds(),
|
||||
|renderer| {
|
||||
child
|
||||
.as_widget()
|
||||
.draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
default_style,
|
||||
child_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let offset: i32 =
|
||||
match target_index.cmp(index) {
|
||||
std::cmp::Ordering::Less
|
||||
if i >= target_index
|
||||
&& i < *index =>
|
||||
{
|
||||
1
|
||||
}
|
||||
std::cmp::Ordering::Greater
|
||||
if i > *index
|
||||
&& i <= target_index =>
|
||||
{
|
||||
-1
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let translation = Vector::new(
|
||||
0.0,
|
||||
offset as f32 * drag_height,
|
||||
);
|
||||
renderer.with_translation(
|
||||
translation,
|
||||
|renderer| {
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
default_style,
|
||||
child_layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
// Draw an overlay if this item is being moved
|
||||
// TODO: instead of drawing an overlay, it would be nicer to
|
||||
// draw the item with a reduced opacity, but that's not possible today
|
||||
if offset != 0 {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: child_layout
|
||||
.bounds(),
|
||||
..renderer::Quad::default(
|
||||
)
|
||||
},
|
||||
style.moved_item_overlay,
|
||||
);
|
||||
|
||||
// Keep track of the total translation so we can
|
||||
// draw the "ghost" of the dragged item later
|
||||
translations -= (child_layout
|
||||
.bounds()
|
||||
.height
|
||||
+ self.spacing)
|
||||
* offset.signum() as f32;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
// Draw a ghost of the dragged item in its would-be position
|
||||
let ghost_translation =
|
||||
Vector::new(0.0, translations);
|
||||
renderer.with_translation(
|
||||
ghost_translation,
|
||||
|renderer| {
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds: drag_bounds,
|
||||
border: style.ghost_border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.ghost_background,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
// Draw all children normally when not dragging
|
||||
if let Some(clipped_viewport) =
|
||||
layout.bounds().intersection(viewport)
|
||||
{
|
||||
let viewport = if self.clip {
|
||||
&clipped_viewport
|
||||
} else {
|
||||
viewport
|
||||
};
|
||||
for ((child, state), c_layout) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(&tree.children)
|
||||
.zip(layout.children())
|
||||
.filter(|(_, layout)| {
|
||||
layout.bounds().intersects(viewport)
|
||||
})
|
||||
{
|
||||
child.as_widget().draw(
|
||||
state,
|
||||
renderer,
|
||||
theme,
|
||||
default_style,
|
||||
c_layout.with_virtual_offset(
|
||||
layout.virtual_offset(),
|
||||
),
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
overlay::from_children(
|
||||
&mut self.children,
|
||||
tree,
|
||||
layout,
|
||||
renderer,
|
||||
translation,
|
||||
)
|
||||
}
|
||||
|
||||
fn drag_destinations(
|
||||
&self,
|
||||
state: &Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
dnd_rectangles: &mut cosmic::iced_core::clipboard::DndDestinationRectangles,
|
||||
) {
|
||||
for ((e, c_layout), state) in self
|
||||
.children
|
||||
.iter()
|
||||
.zip(layout.children())
|
||||
.zip(state.children.iter())
|
||||
{
|
||||
e.as_widget().drag_destinations(
|
||||
state,
|
||||
c_layout.with_virtual_offset(layout.virtual_offset()),
|
||||
renderer,
|
||||
dnd_rectangles,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer>
|
||||
From<Column<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: renderer::Renderer + 'a,
|
||||
{
|
||||
fn from(column: Column<'a, Message, Theme, Renderer>) -> Self {
|
||||
Self::new(column)
|
||||
}
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`Column`].
|
||||
pub trait Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> Self::Class<'a>;
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style;
|
||||
}
|
||||
|
||||
/// The appearance of a [`Column`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The scaling to apply to a picked element while it's being dragged.
|
||||
pub scale: f32,
|
||||
/// The color of the overlay on items that are moved around
|
||||
pub moved_item_overlay: Color,
|
||||
/// The outline border of the dragged item's ghost
|
||||
pub ghost_border: Border,
|
||||
/// The background of the dragged item's ghost
|
||||
pub ghost_background: Background,
|
||||
}
|
||||
|
||||
/// A styling function for a [`Column`].
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
|
||||
|
||||
impl Catalog for cosmic::Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> Self::Class<'a> {
|
||||
Box::new(default)
|
||||
}
|
||||
|
||||
fn style(&self, class: &Self::Class<'_>) -> Style {
|
||||
class(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default(theme: &Theme) -> Style {
|
||||
Style {
|
||||
scale: 1.05,
|
||||
moved_item_overlay: Color::from(theme.cosmic().primary.base)
|
||||
.scale_alpha(0.2)
|
||||
.into(),
|
||||
ghost_border: Border {
|
||||
width: 1.0,
|
||||
color: theme.cosmic().secondary.base.into(),
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
ghost_background: Color::from(theme.cosmic().secondary.base)
|
||||
.scale_alpha(0.2)
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
42
src/ui/widgets/draggable/mod.rs
Normal file
42
src/ui/widgets/draggable/mod.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
use cosmic::iced::Point;
|
||||
|
||||
pub use self::column::column;
|
||||
pub use self::row::row;
|
||||
pub mod column;
|
||||
pub mod row;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Action {
|
||||
Idle,
|
||||
Picking {
|
||||
index: usize,
|
||||
origin: Point,
|
||||
},
|
||||
Dragging {
|
||||
index: usize,
|
||||
origin: Point,
|
||||
last_cursor: Point,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum DropPosition {
|
||||
Before,
|
||||
Swap,
|
||||
After,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DragEvent {
|
||||
Picked {
|
||||
index: usize,
|
||||
},
|
||||
Dropped {
|
||||
index: usize,
|
||||
target_index: usize,
|
||||
drop_position: DropPosition,
|
||||
},
|
||||
Canceled {
|
||||
index: usize,
|
||||
},
|
||||
}
|
||||
1071
src/ui/widgets/draggable/row.rs
Normal file
1071
src/ui/widgets/draggable/row.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1 +1,2 @@
|
|||
// pub mod slide_text;
|
||||
pub mod draggable;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue