diff --git a/src/app/file_select.rs b/src/app/file_select.rs new file mode 100644 index 0000000..5e9da53 --- /dev/null +++ b/src/app/file_select.rs @@ -0,0 +1,185 @@ +use iced::canvas::event::Status; +use iced::Font; +use iced::canvas::Text; +use iced::canvas::Stroke; +use iced::Size; +use iced::Point; +use iced::canvas::Path; +use iced::canvas::Frame; +use iced::canvas::Geometry; +use iced::canvas::Cursor; +use iced::Element; +use crate::app::Message; +use iced::canvas::Program; +use iced::mouse; +use iced::Color; +use iced::Rectangle; +use iced::Length; +use iced::HorizontalAlignment; +use iced::VerticalAlignment; +use iced::widget::canvas::{self, Canvas}; + +/* RYGCBM +const COLORS: [Color; 6] = [ + Color { r: 1., g: 0., b: 0., a: 1. }, + Color { r: 1., g: 1., b: 0., a: 1. }, + Color { r: 0., g: 1., b: 0., a: 1. }, + Color { r: 0., g: 1., b: 1., a: 1. }, + Color { r: 0., g: 0., b: 1., a: 1. }, + Color { r: 1., g: 0., b: 1., a: 1. }, +];*/ +const COLORS: [Color; 6] = [ + Color { r: 255./255., g: 129./255., b: 126./255., a: 1. }, + Color { r: 239./255., g: 190./255., b: 125./255., a: 1. }, + Color { r: 233./255., g: 236./255., b: 107./255., a: 1. }, + Color { r: 119./255., g: 221./255., b: 119./255., a: 1. }, + Color { r: 139./255., g: 211./255., b: 230./255., a: 1. }, + Color { r: 177./255., g: 162./255., b: 202./255., a: 1. }, +]; + +const MAX_TICKS: usize = 900; + +const FONT_MR_PIXEL: Font = Font::External { + name: "Mister Pixel", + bytes: include_bytes!("../../fonts/mister-pixel/mister-pixel.otf"), +}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct FileSelector { + tick: usize, +} + +impl FileSelector { + pub fn tick(&mut self) { + self.tick = (self.tick + 1) % MAX_TICKS; + } + + pub fn view(&mut self) -> Element { + Canvas::new(self) + .width(Length::Fill) + .height(Length::Fill) + .into() + } +} + +impl Default for FileSelector { + fn default() -> Self { + FileSelector { + tick: 0, + } + } +} + +impl Program for FileSelector { + fn draw(&self, bounds: Rectangle, _cursor: Cursor) -> Vec { + let offset_per_index = MAX_TICKS / &COLORS.len(); + + const TEXT_RECT_W: f32 = 350.; + const TEXT_RECT_H: f32 = 50.; + const RECT_RATIO: f32 = TEXT_RECT_W / TEXT_RECT_H; + let text_rect_scale = TEXT_RECT_W / bounds.width; + let text_rect: (Point, Size) = centered_rectangle(&bounds, RECT_RATIO, text_rect_scale); + let text_rect_far_point = Point { + x: text_rect.0.x + text_rect.1.width, + y: text_rect.0.y + text_rect.1.height, + }; + let bound_far_point: Point = Point { + x: bounds.x + bounds.width, + y: bounds.y + bounds.height, + }; + let screen_top_left = Point { x: bounds.x, y: bounds.y }; + + let mut frame = Frame::new(bounds.size()); + + frame.fill_rectangle( + Point::new(bounds.x, bounds.y), + Size::new(bounds.width, bounds.height), + Color::WHITE, + ); + + COLORS.iter() + .enumerate() + .map(|(index, color)| { + let size = + ((self.tick + offset_per_index * index) % MAX_TICKS) as f32 / + MAX_TICKS as f32; + + let top_left = interpolate(text_rect.0, screen_top_left, size); + let far_corner = interpolate(text_rect_far_point, bound_far_point, size); + let rect_size = Size::new(far_corner.x - top_left.x, far_corner.y - top_left.y); + + (Path::rectangle(top_left, rect_size), color, size) + }) + .for_each(|(rect, color, size)| + frame.stroke( + &rect, + Stroke::default() + .with_color(*color) + .with_width(5.) + ) + ); + + frame.fill_text( + Text { + content: String::from("select a song to start"), + horizontal_alignment: HorizontalAlignment::Center, + vertical_alignment: VerticalAlignment::Center, + size: 32., + color: Color::WHITE, + font: FONT_MR_PIXEL, + position: Point::new( + bounds.x + bounds.width * 0.5, + bounds.y + bounds.height * 0.5, + ), + ..Default::default() + } + ); + + frame.fill( + &Path::rectangle(text_rect.0, text_rect.1), + Color::BLACK, + ); + frame.stroke( + &Path::rectangle(text_rect.0, text_rect.1), + Stroke::default() + .with_color(Color::BLACK) + .with_width(2.) + ); + + vec![frame.into_geometry()] + } + + fn update( + &mut self, + event: canvas::Event, + _: Rectangle, + _: Cursor + ) -> (Status, Option) { + match event { + canvas::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + (Status::Captured, Some(Message::PromptForFile)) + }, + _ => (Status::Ignored, None), + } + } +} + +fn centered_rectangle(bounds: &Rectangle, ratio: f32, scale: f32) -> (Point, Size) { + let center_x = bounds.x + 0.5 * bounds.width; + let center_y = bounds.y + 0.5 * bounds.height; + let width = bounds.width * scale; + let height = width / ratio; + let top_x = center_x - width * 0.5; + let top_y = center_y - height * 0.5; + ( + Point::new(top_x, top_y), + Size::new(width, height), + ) +} + +fn interpolate(from: Point, to: Point, amount: f32) -> Point { + Point { + x: from.x + amount * (to.x - from.x), + y: from.y + amount * (to.y - from.y), + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 009886b..9316024 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,3 +1,5 @@ +use rfd::{AsyncFileDialog, FileHandle}; +use std::path::PathBuf; use core::time::Duration; use iced::Subscription; use iced::Clipboard; @@ -12,6 +14,7 @@ use iced::executor; use iced_futures::time; use iced_native::subscription; use iced_native::keyboard; +use iced_native::window; use iced_native::event::Event; mod lyrics; @@ -43,6 +46,9 @@ pub enum Message { LineAdvanced(usize), PasteSent, Tick, + PromptForFile, + FileOpened(PathBuf), + Null, } impl Application for DelyriumApp { @@ -67,6 +73,7 @@ impl Application for DelyriumApp { } fn update(&mut self, message: Message, clipboard: &mut Clipboard) -> Command{ + let mut command = None; match message { Message::LyricChanged { line_no, new_value } => { self.lyrics_component.update_line(new_value, line_no); @@ -90,9 +97,29 @@ impl Application for DelyriumApp { }, _ => { }, } - } + }, + Message::FileOpened(path) => { + println!("File opened! {}", path.display()); + }, + Message::PromptForFile => { + let task = async { + let handle = AsyncFileDialog::new() + .add_filter("Song Files", &["mp3", "flac", "ogg", "opus", "wav"]) + .set_title("Select a song") + .pick_file() + .await; + if let Some(h) = handle { + Message::FileOpened(h.path().to_owned()) + } else { + Message::Null + } + }; + command = Some(task.into()); + }, + Message::Null => { }, } - Command::none() + + command.unwrap_or_else(Command::none) } fn view(&mut self) -> Element { @@ -126,7 +153,10 @@ impl Application for DelyriumApp { } _ => { None } } - } + }, + Event::Window(window::Event::FileDropped(path)) => { + Some(Message::FileOpened(path)) + }, _ => { None } } });