diff --git a/apps/magpie/src/service/gl.rs b/apps/magpie/src/service/gl.rs index 689b340..e46e11e 100644 --- a/apps/magpie/src/service/gl.rs +++ b/apps/magpie/src/service/gl.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use canary::{DrawCommand, Vec2, PX_PER_MM}; -use glium::Surface; +use glium::{program::ProgramCreationInput, Surface}; #[derive(Copy, Clone)] pub struct Vertex { @@ -59,9 +59,21 @@ pub struct Graphics { impl Graphics { pub fn new(display: glium::Display) -> Self { - let program = - glium::Program::from_source(&display, VERTEX_SHADER_SRC, FRAGMENT_SHADER_SRC, None) - .unwrap(); + let program = glium::Program::new( + &display, + ProgramCreationInput::SourceCode { + vertex_shader: VERTEX_SHADER_SRC, + tessellation_control_shader: None, + tessellation_evaluation_shader: None, + geometry_shader: None, + fragment_shader: FRAGMENT_SHADER_SRC, + transform_feedback_varyings: None, + outputs_srgb: true, // don't automatically apply gamma correction + uses_point_size: false, + }, + ) + .unwrap(); + Self { display, program } } @@ -105,7 +117,7 @@ impl Graphics { }; let mut target = self.display.draw(); - target.clear_color(0.0, 0.0, 0.0, 1.0); + target.clear_color(0.0, 0.0, 0.0, 0.0); target .draw( &vertex_buffer, diff --git a/apps/magpie/src/service/window.rs b/apps/magpie/src/service/window.rs index 01cea07..7ea34e2 100644 --- a/apps/magpie/src/service/window.rs +++ b/apps/magpie/src/service/window.rs @@ -51,7 +51,9 @@ impl Window { panel: Panel, event_loop: &EventLoopWindowTarget, ) -> Result { - let wb = glutin::window::WindowBuilder::new(); + let wb = glutin::window::WindowBuilder::new() + .with_transparent(true) + .with_decorations(false); let cb = glutin::ContextBuilder::new(); let display = glium::Display::new(wb, cb, &event_loop)?; let graphics = Graphics::new(display); diff --git a/apps/music-player/Cargo.toml b/apps/music-player/Cargo.toml index 2aed4fe..107df85 100644 --- a/apps/music-player/Cargo.toml +++ b/apps/music-player/Cargo.toml @@ -11,9 +11,11 @@ required-features = ["bin"] [dependencies] canary-magpie = { path = "../magpie", optional = true } -mpris = { version = "2.0.0-rc3", optional = true } +futures-util = { version = "0.3", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" +smol = { version = "1.2", optional = true } +zbus = { version = "3.5", optional = true } [features] -bin = ["dep:canary-magpie", "dep:mpris"] +bin = ["dep:canary-magpie", "dep:futures-util", "dep:smol", "dep:zbus"] diff --git a/apps/music-player/src/lib.rs b/apps/music-player/src/lib.rs index d3ecabb..e7cdd2b 100644 --- a/apps/music-player/src/lib.rs +++ b/apps/music-player/src/lib.rs @@ -34,9 +34,6 @@ pub enum LoopStatus { pub struct ProgressChanged { /// Current position into the track in seconds. pub position: f32, - - /// Length of the current track in seconds. - pub length: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -48,7 +45,7 @@ pub struct AlbumInfo { pub artists: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct TrackInfo { /// The title of the current track. pub title: Option, @@ -58,6 +55,9 @@ pub struct TrackInfo { /// The optional track number on the disc the album the track appears on. pub track_number: Option, + + /// Length of the track in seconds. + pub length: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/apps/music-player/src/main.rs b/apps/music-player/src/main.rs index c6f17b0..6909c3e 100644 --- a/apps/music-player/src/main.rs +++ b/apps/music-player/src/main.rs @@ -5,58 +5,63 @@ use std::{os::unix::net::UnixStream, path::Path}; use canary_magpie::protocol::{ClientMessenger, CreatePanel, MagpieServerMsg, MAGPIE_SOCK}; use canary_music_player::*; -use mpris::PlayerFinder; pub type MagpieClient<'a> = ClientMessenger<&'a UnixStream>; -pub struct MetadataTracker { +pub mod mpris; + +use mpris::*; + +#[derive(Debug)] +pub struct Metadata { pub album: AlbumInfo, pub track: TrackInfo, } -impl From<&mpris::Metadata> for MetadataTracker { - fn from(metadata: &mpris::Metadata) -> Self { +impl<'a> From> for Metadata { + fn from(map: MetadataMap<'a>) -> Self { let album = AlbumInfo { - title: metadata.album_name().map(ToString::to_string), - artists: metadata - .album_artists() - .unwrap_or(Vec::new()) - .iter() - .map(ToString::to_string) - .collect(), + title: map + .get("xesam:album") + .and_then(|v| TryFrom::try_from(v).ok()), + artists: map + .get("xesam:albumArtist") + .cloned() + .and_then(|v| TryFrom::try_from(v).ok()) + .unwrap_or(Vec::new()), }; let track = TrackInfo { - title: metadata.title().map(ToString::to_string), - artists: metadata - .artists() - .unwrap_or(Vec::new()) - .iter() - .map(ToString::to_string) - .collect(), - track_number: metadata.track_number(), + title: map + .get("xesam:title") + .and_then(|v| TryFrom::try_from(v).ok()), + artists: map + .get("xesam:artist") + .cloned() + .and_then(|v| TryFrom::try_from(v).ok()) + .unwrap_or(Vec::new()), + track_number: map + .get("xesam:trackNumber") + .and_then(|v| TryFrom::try_from(v).ok()), + length: map + .get("xesam:length") + .and_then(|v| i64::try_from(v).ok()) + .map(|us| us as f32 / 1_000_000.0), // 1,000,000 microseconds in a second }; Self { album, track } } } -impl MetadataTracker { - pub fn new(magpie: &mut MagpieClient, metadata: &mpris::Metadata) -> Self { +impl Metadata { + pub fn new(magpie: &mut MagpieClient, metadata: MetadataMap) -> Self { let new: Self = metadata.into(); magpie.send_panel_json(0, &InMsg::AlbumChanged(new.album.clone())); magpie.send_panel_json(0, &InMsg::TrackChanged(new.track.clone())); - magpie.send_panel_json( - 0, - &InMsg::ProgressChanged(ProgressChanged { - position: 0.0, - length: metadata.length().map(|l| l.as_secs_f32()), - }), - ); new } - pub fn update(&mut self, messenger: &mut MagpieClient, metadata: &mpris::Metadata) { + pub fn update(&mut self, messenger: &mut MagpieClient, metadata: MetadataMap) { let new: Self = metadata.into(); if self.album != new.album { @@ -65,19 +70,58 @@ impl MetadataTracker { if self.track != new.track { messenger.send_panel_json(0, &InMsg::TrackChanged(new.track.clone())); - messenger.send_panel_json( - 0, - &InMsg::ProgressChanged(ProgressChanged { - position: 0.0, - length: metadata.length().map(|l| l.as_secs_f32()), - }), - ); } *self = new; } } +async fn player_main( + player: &PlayerProxy<'_>, + magpie: &mut MagpieClient<'_>, +) -> Result<(), Box> { + use futures_util::StreamExt; + let mut playback_status = player.receive_playback_status_changed().await.fuse(); + let mut metadata_tracker = player.receive_metadata_changed().await.fuse(); + + let mut metadata = Metadata::new(magpie, player.metadata().await?); + + loop { + futures_util::select! { + // TODO also update volume, shuffle status, and loop status + status = playback_status.next() => { + let status = match status { + Some(v) => v, + None => break, + }; + + let status = status.get().await?; + let status = match status.as_str() { + "Playing" => Some(PlaybackStatus::Playing), + "Paused" => Some(PlaybackStatus::Paused), + "Stopped" => Some(PlaybackStatus::Stopped), + _ => None, + }; + + if let Some(status) = status { + magpie.send_panel_json(0, &InMsg::PlaybackStatusChanged(status)); + } + } + new_metadata = metadata_tracker.next() => { + let new_metadata = match new_metadata { + Some(v) => v, + None => break, + }; + + let new_metadata = new_metadata.get().await?; + metadata.update(magpie, new_metadata); + } + }; + } + + Ok(()) +} + fn main() { let args: Vec = std::env::args().collect(); let module_path = args @@ -85,8 +129,6 @@ fn main() { .expect("Please pass a path to a Canary script!") .to_owned(); - let player_finder = PlayerFinder::new().expect("Could not connect to D-Bus"); - let sock_dir = std::env::var("XDG_RUNTIME_DIR").expect("XDG_RUNTIME_DIR not set"); let sock_dir = Path::new(&sock_dir); let sock_path = sock_dir.join(MAGPIE_SOCK); @@ -103,96 +145,55 @@ fn main() { let msg = MagpieServerMsg::CreatePanel(msg); magpie.send(&msg).unwrap(); - let mut first_loop = true; - let mut connected = false; + smol::block_on(async { + let dbus = zbus::Connection::session().await.unwrap(); - loop { - if !first_loop { - let wait = std::time::Duration::from_secs(1); - std::thread::sleep(wait); - } - - first_loop = false; - - if connected { - println!("Disconnected from MPRIS"); - let msg = InMsg::Disconnected; - magpie.send_panel_json(0, &msg); - connected = false; - } - - println!("Connecting to MPRIS..."); - - let player = match player_finder.find_active() { - Ok(player) => player, - Err(err) => { - eprintln!("Couldn't find player: {:?}", err); - continue; - } - }; - - println!( - "Connected to \"{}\" ({})", - player.identity(), - player.bus_name() - ); - connected = true; - magpie.send_panel_json(0, &InMsg::Connected); - - let metadata = player.get_metadata().unwrap(); - let mut metadata_tracker = MetadataTracker::new(&mut magpie, &metadata); - - let mut events = match player.events() { - Ok(events) => events, - Err(err) => { - eprintln!("Player events D-Bus error: {:?}", err); - continue; - } - }; + let mut first_loop = true; + let mut connected = false; loop { - let event = match events.next() { - None => break, - Some(Ok(e)) => e, - Some(Err(err)) => { - eprintln!("D-Bus error while reading player events: {:?}", err); + if !first_loop { + let wait = std::time::Duration::from_secs(1); + std::thread::sleep(wait); + } + + first_loop = false; + + if connected { + println!("Disconnected from MPRIS"); + let msg = InMsg::Disconnected; + magpie.send_panel_json(0, &msg); + connected = false; + } + + println!("Connecting to MPRIS..."); + + let player = match find_player(&dbus).await { + Ok(Some(player)) => player, + Ok(None) => { + eprintln!("Couldn't find player"); continue; } - }; - - use mpris::Event::*; - let in_msg = match event { - Playing => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Playing)), - Paused => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Paused)), - Stopped => Some(InMsg::PlaybackStatusChanged(PlaybackStatus::Stopped)), - LoopingChanged(status) => { - use mpris::LoopStatus::*; - let status = match status { - None => LoopStatus::None, - Track => LoopStatus::Track, - Playlist => LoopStatus::Playlist, - }; - - Some(InMsg::LoopingChanged(status)) - } - ShuffleToggled(shuffle) => Some(InMsg::ShuffleChanged { shuffle }), - VolumeChanged(volume) => Some(InMsg::VolumeChanged { - volume: volume as f32, - }), - PlayerShutDown => None, - TrackChanged(ref metadata) => { - metadata_tracker.update(&mut magpie, metadata); - None - } - _ => { - eprintln!("Unhandled MPRIS message: {:?}", event); - None + Err(err) => { + eprintln!("D-Bus error while finding player: {:?}", err); + return; } }; - if let Some(msg) = in_msg { - magpie.send_panel_json(0, &msg); + println!( + "Connected to \"{}\" ({})", + player.path().as_str(), + player.destination().as_str() + ); + connected = true; + magpie.send_panel_json(0, &InMsg::Connected); + + match player_main(&player, &mut magpie).await { + Ok(()) => {} + Err(err) => { + eprintln!("D-Bus error while connected to player: {:?}", err); + } } } - } + }); } diff --git a/apps/music-player/src/mpris.rs b/apps/music-player/src/mpris.rs new file mode 100644 index 0000000..74e47c2 --- /dev/null +++ b/apps/music-player/src/mpris.rs @@ -0,0 +1,47 @@ +use std::collections::HashMap; + +use zbus::fdo::DBusProxy; +use zbus::zvariant::Value; +use zbus::{dbus_proxy, Connection, Result}; + +pub type MetadataMap<'a> = HashMap>; + +#[dbus_proxy( + interface = "org.mpris.MediaPlayer2.Player", + default_path = "/org/mpris/MediaPlayer2" +)] +trait Player { + fn next(&self) -> Result<()>; + fn previous(&self) -> Result<()>; + fn pause(&self) -> Result<()>; + fn play_pause(&self) -> Result<()>; + fn stop(&self) -> Result<()>; + fn play(&self) -> Result<()>; + + #[dbus_proxy(property)] + fn playback_status(&self) -> Result; + + #[dbus_proxy(property)] + fn position(&self) -> Result; + + #[dbus_proxy(property)] + fn metadata(&self) -> Result; +} + +pub async fn find_player(connection: &Connection) -> Result> { + let dbus = DBusProxy::new(connection).await?; + let names = dbus.list_names().await?; + + for name in names { + let name = name.as_str().to_string(); + if name.starts_with("org.mpris.MediaPlayer2") { + let player = PlayerProxy::builder(connection) + .destination(name)? + .build() + .await?; + return Ok(Some(player)); + } + } + + Ok(None) +} diff --git a/apps/sandbox/src/main.rs b/apps/sandbox/src/main.rs index 9ec1f57..5b54f7b 100644 --- a/apps/sandbox/src/main.rs +++ b/apps/sandbox/src/main.rs @@ -33,6 +33,7 @@ struct App { last_update: Instant, protocol_buf: String, bind_message_buf: String, + panel_bg: egui::Color32, } impl App { @@ -49,6 +50,7 @@ impl App { last_update: Instant::now(), protocol_buf: String::new(), bind_message_buf: String::new(), + panel_bg: egui::Color32::TRANSPARENT, } } } @@ -58,6 +60,8 @@ impl eframe::App for App { ctx.request_repaint(); egui::SidePanel::left("left_panel").show(ctx, |ui| { + ui.heading("New Panel"); + ui.label("Protocol name:"); ui.text_edit_singleline(&mut self.protocol_buf); @@ -81,6 +85,14 @@ impl eframe::App for App { self.panels.push(panel); } + + ui.separator(); + ui.heading("Global Settings"); + + ui.horizontal(|ui| { + ui.label("Panel background color: "); + ui.color_edit_button_srgba(&mut self.panel_bg); + }); }); let dt = self.last_update.elapsed().as_secs_f32(); @@ -88,7 +100,7 @@ impl eframe::App for App { for panel in self.panels.iter_mut() { panel.panel.update(dt); - panel.show(ctx); + panel.show(self.panel_bg, ctx); } } } @@ -102,83 +114,87 @@ pub struct PanelWindow { } impl PanelWindow { - pub fn show(&mut self, ctx: &egui::Context) { + pub fn show(&mut self, bg: egui::Color32, ctx: &egui::Context) { + let frame = egui::Frame::window(&ctx.style()).fill(bg); let window_id = egui::Id::new(format!("panel_{}", self.index)); - egui::Window::new("Panel").id(window_id).show(ctx, |ui| { - egui::menu::bar(ui, |ui| { - ui.checkbox(&mut self.show_msg, "Show Message Editor"); - }); + egui::Window::new("Panel") + .frame(frame) + .id(window_id) + .show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + ui.checkbox(&mut self.show_msg, "Show Message Editor"); + }); - let sense = egui::Sense { - click: true, - drag: true, - focusable: true, - }; - - let desired_size = ui.available_size(); - let response = ui.allocate_response(desired_size, sense); - let rect = response.rect; - - if rect.size() != self.current_size { - let size = rect.size(); - self.current_size = size; - - let size = canary::Vec2::new(size.x, size.y); - self.panel.on_resize(size * PX_PER_MM); - } - - if let Some(hover_pos) = response.hover_pos() { - let local = (hover_pos - rect.left_top()) * PX_PER_MM; - let pos = canary::Vec2::new(local.x, local.y); - - let kind = if response.drag_started() { - CursorEventKind::Select - } else if response.drag_released() { - CursorEventKind::Deselect - } else if response.dragged() { - CursorEventKind::Drag - } else { - CursorEventKind::Hover + let sense = egui::Sense { + click: true, + drag: true, + focusable: true, }; - self.panel.on_cursor_event(kind, pos); - } + let desired_size = ui.available_size(); + let response = ui.allocate_response(desired_size, sense); + let rect = response.rect; - let texture = egui::TextureId::Managed(0); - let uv = egui::pos2(0.0, 0.0); - let mut mesh = egui::Mesh::with_texture(texture); + if rect.size() != self.current_size { + let size = rect.size(); + self.current_size = size; - let commands = self.panel.draw(); - for command in commands.into_iter() { - let voff = mesh.vertices.len() as u32; - - match command { - canary::DrawCommand::Mesh { vertices, indices } => { - for v in vertices.iter() { - use egui::epaint::Vertex; - let pos = v.position / PX_PER_MM; - let pos = egui::pos2(pos.x, pos.y); - let pos = pos + rect.left_top().to_vec2(); - let (r, g, b, a) = v.color.to_rgba_unmultiplied(); - let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a); - let v = Vertex { pos, uv, color }; - mesh.vertices.push(v); - } - - for i in indices.iter() { - mesh.indices.push(i + voff); - } - } - _ => unimplemented!(), + let size = canary::Vec2::new(size.x, size.y); + self.panel.on_resize(size * PX_PER_MM); } - } - let painter = ui.painter_at(rect); - let shape = egui::Shape::mesh(mesh); - painter.add(shape); + if let Some(hover_pos) = response.hover_pos() { + let local = (hover_pos - rect.left_top()) * PX_PER_MM; + let pos = canary::Vec2::new(local.x, local.y); - response - }); + let kind = if response.drag_started() { + CursorEventKind::Select + } else if response.drag_released() { + CursorEventKind::Deselect + } else if response.dragged() { + CursorEventKind::Drag + } else { + CursorEventKind::Hover + }; + + self.panel.on_cursor_event(kind, pos); + } + + let texture = egui::TextureId::Managed(0); + let uv = egui::pos2(0.0, 0.0); + let mut mesh = egui::Mesh::with_texture(texture); + + let commands = self.panel.draw(); + for command in commands.into_iter() { + let voff = mesh.vertices.len() as u32; + + match command { + canary::DrawCommand::Mesh { vertices, indices } => { + for v in vertices.iter() { + use egui::epaint::Vertex; + let pos = v.position / PX_PER_MM; + let pos = egui::pos2(pos.x, pos.y); + let pos = pos + rect.left_top().to_vec2(); + let (r, g, b, a) = v.color.to_rgba_unmultiplied(); + let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a); + let v = Vertex { pos, uv, color }; + mesh.vertices.push(v); + } + + for i in indices.iter() { + mesh.indices.push(i + voff); + } + } + _ => unimplemented!(), + } + } + + let painter = ui.painter_at(rect); + let shape = egui::Shape::mesh(mesh); + painter.add(shape); + + response + }); let msg_edit_id = egui::Id::new(format!("msg_edit_{}", self.index)); egui::Window::new("Message Editor") diff --git a/check_licenses.sh b/check_licenses.sh index c02b162..aad8cd3 100755 --- a/check_licenses.sh +++ b/check_licenses.sh @@ -1,4 +1,38 @@ #!/bin/sh -# Depends on: `rg` (ripgrep) -! rg --multiline --files-without-match --glob '*.rs' --pcre2 '(?/dev/null 2>&1; then + printf "%s: Missing dependency: tomcat(1)\n" + exit 69 # sysexits(3) EX_UNAVAILABLE +fi + +dir="$(pwd | sed 's/\//\n/g' | tail -n 1)" + +for toml in $(find "$PWD" -name "Cargo.toml"); do + printf "Project: %s\n" "$(tomcat package.name "$toml")" + for file in $(find "$(printf "%s\n" "$toml" |\ + sed 's/Cargo\.toml/src/g')" -name "*.rs") + do + info="$(head -n 2 "$file")" + toml_lic="$(tomcat package.license "$toml")" + if ! test -n "$toml_lic"; then + printf "%s: Missing license information\n" "$(printf "%s\n" "$toml" |\ + sed "s/^.\+$dir\///g")" + continue 2 + fi + if ! [ "$toml_lic" = "$(printf "%s\n" "$info" | tail -n 1 |\ + sed -n 's/\/\/ SPDX-License-Identifier: //p')" ] + then + printf "%s: Missing or malformed license information\n" \ + "$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")" + fi + if ! test -n "$(printf "%s\n" "$info" | head -n 1 |\ + sed -n '/\/\/ Copyright (c) .\+/p')" + then + printf "%s: Missing or malformed copyright holder information\n" \ + "$(printf "%s\n" "$file" | sed "s/^.\+$dir\///g")" + fi + done +done diff --git a/crates/script/src/lib.rs b/crates/script/src/lib.rs index c2ada0e..58c0266 100644 --- a/crates/script/src/lib.rs +++ b/crates/script/src/lib.rs @@ -173,13 +173,13 @@ impl Color { ) } - pub fn alpha_multiply(&self, mul: u8) -> Self { + pub const fn alpha_multiply(&self, mul: u8) -> Self { let a = self.0 as u8 as u16; let multiplied = ((a * (mul as u16)) >> 8) as u8; self.with_alpha(multiplied) } - pub fn with_alpha(&self, alpha: u8) -> Self { + pub const fn with_alpha(&self, alpha: u8) -> Self { Self(self.0 & 0xffffff00 | alpha as u32) } diff --git a/scripts/sao-ui/src/lib.rs b/scripts/sao-ui/src/lib.rs index b558d3f..948c2e9 100644 --- a/scripts/sao-ui/src/lib.rs +++ b/scripts/sao-ui/src/lib.rs @@ -7,6 +7,7 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; pub mod anim; pub mod main_menu; pub mod music_player; +pub mod style; pub mod widgets; use api::*; @@ -23,6 +24,7 @@ fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box MusicPlayerPanel::bind(panel, msg), + "wip-dialog" => ConfirmationDialogPanel::bind(panel, msg), _ => MainMenuPanel::bind(panel, msg), } } @@ -50,15 +52,23 @@ impl PanelImpl for ConfirmationDialogPanel { self.dialog.on_cursor_event(kind, at.into()); } - fn on_resize(&mut self, _size: Vec2) {} + fn on_resize(&mut self, size: Vec2) { + self.dialog.resize(size); + } fn on_message(&mut self, _msg: Message) {} } impl ConfirmationDialogPanel { pub fn bind(panel: Panel, msg: Message) -> Box { - let msg = msg.to_vec(); - let info: DialogInfo = serde_json::from_slice(&msg).unwrap(); + // let msg = msg.to_vec(); + // let info: DialogInfo = serde_json::from_slice(&msg).unwrap(); + + let info = DialogInfo { + title: "Hello world!".to_string(), + content: "Testing, testing...".to_string(), + responses: vec![DialogResponse::Yes, DialogResponse::No], + }; use widgets::dialog::*; let style = DialogStyle::default(); diff --git a/scripts/sao-ui/src/main_menu.rs b/scripts/sao-ui/src/main_menu.rs index 87bbbb0..b921a46 100644 --- a/scripts/sao-ui/src/main_menu.rs +++ b/scripts/sao-ui/src/main_menu.rs @@ -7,6 +7,7 @@ use crate::{DrawContext, Rect}; use button::{RectButton, RoundButton, RoundButtonStyle}; use dialog::{Dialog, DialogInfo, DialogResponse, DialogStyle}; use menu::{SlotMenu, SlotMenuEvent, TabMenu}; +use palette::Palette; use shell::{Offset, OffsetAlignment, Popup, Reveal}; use text::LabelText; @@ -47,6 +48,7 @@ pub struct MainMenu { pub menu: Offset>, pub player_info: Reveal>, pub inventory: Reveal>, + pub palette: Reveal>, pub settings: Reveal>, } @@ -64,9 +66,9 @@ impl Default for MainMenu { radius: 7.5, spacing: 1.5, thickness: 0.4, - body_color: Color::WHITE, - ring_color: Color::WHITE, - icon_color: Color::BLACK, + body_color: THEME.palette.surface, + ring_color: THEME.palette.surface, + icon_color: THEME.palette.text, }; let mut buttons = Vec::new(); @@ -102,6 +104,15 @@ impl Default for MainMenu { let inventory = Offset::new(inventory, submenu_spacing_right); let inventory = Reveal::new(inventory, reveal_slide, reveal_duration); + let palette = Palette::new(Default::default()); + let palette = Offset::new_aligned( + palette, + submenu_spacing_left, + OffsetAlignment::End, + OffsetAlignment::Center, + ); + let palette = Reveal::new(palette, -reveal_slide, reveal_duration); + let settings = SettingsMenu::new(); let settings = Offset::new(settings, submenu_spacing_right); let settings = Reveal::new(settings, reveal_slide, reveal_duration); @@ -110,6 +121,7 @@ impl Default for MainMenu { menu, player_info, inventory, + palette, settings, } } @@ -120,6 +132,7 @@ impl Container for MainMenu { f(&mut self.menu); f(&mut self.player_info); f(&mut self.inventory); + f(&mut self.palette); f(&mut self.settings); } @@ -139,8 +152,14 @@ impl Container for MainMenu { self.player_info.hide(); self.inventory.hide(); } - SlotMenuEvent::SubmenuOpen(4) => self.settings.show(), - SlotMenuEvent::SubmenuClose(4) => self.settings.hide(), + SlotMenuEvent::SubmenuOpen(4) => { + self.palette.show(); + self.settings.show(); + } + SlotMenuEvent::SubmenuClose(4) => { + self.palette.hide(); + self.settings.hide(); + } _ => {} }; } @@ -150,6 +169,7 @@ pub struct PlayerInfo { width: f32, height: f32, rounding: f32, + color: Color, } impl PlayerInfo { @@ -158,6 +178,7 @@ impl PlayerInfo { width: 70.0, height: 120.0, rounding: 5.0, + color: THEME.palette.surface, } } } @@ -170,7 +191,7 @@ impl RectBounds for PlayerInfo { impl Widget for PlayerInfo { fn draw(&mut self, ctx: &DrawContext) { - ctx.draw_rounded_rect(self.get_bounds(), self.rounding, Color::WHITE); + ctx.draw_rounded_rect(self.get_bounds(), self.rounding, self.color); } } diff --git a/scripts/sao-ui/src/music_player.rs b/scripts/sao-ui/src/music_player.rs index 82222c3..19c0834 100644 --- a/scripts/sao-ui/src/music_player.rs +++ b/scripts/sao-ui/src/music_player.rs @@ -192,7 +192,8 @@ impl MusicPlayerWidget { text: content.to_string(), }; - let label = Label::new_centered(text, 10.0, Color::BLACK); + let color = style.body.text_color; + let label = Label::new_centered(text, 10.0, color); Offset::new(label, Vec2::ZERO) }; @@ -209,7 +210,7 @@ impl MusicPlayerWidget { text, HorizontalAlignment::Center, scale, - Color::BLACK, + THEME.palette.text, 0.0, 0.0, baseline, @@ -238,18 +239,18 @@ impl MusicPlayerWidget { radius: style.footer.height * 0.3, spacing: style.footer.height * 0.1, thickness: style.footer.height * 0.025, - body_color: Color(0xf1b841ff), - ring_color: Color(0xf1b841ff), - icon_color: Color::BLACK, + body_color: THEME.palette.yellow, + ring_color: THEME.palette.yellow, + icon_color: THEME.palette.black, }; let secondary_button = RoundButtonStyle { radius: style.footer.height * 0.25, spacing: style.footer.height * 0.05, thickness: style.footer.height * 0.025, - body_color: Color::WHITE, - ring_color: Color::BLACK, - icon_color: Color::BLACK, + body_color: style.footer.color, + ring_color: THEME.palette.black, + icon_color: THEME.palette.black, }; let prev = RoundButton::new(secondary_button.clone(), Some(prev_text)); @@ -354,17 +355,17 @@ impl MusicPlayerWidget { .map(|s| s.as_str()) .unwrap_or(""), ); + + if let Some(length) = info.length { + self.duration.set_text(&Self::format_time(length)); + } else { + self.duration.set_text("--:--"); + } } pub fn update_progress(&mut self, progress: ProgressChanged) { self.position_secs = progress.position; self.position_dirty = true; - - if let Some(length) = progress.length { - self.duration.set_text(&Self::format_time(length)); - } else { - self.duration.set_text("--:--"); - } } pub fn update_playback_status(&mut self, status: PlaybackStatus) { diff --git a/scripts/sao-ui/src/style.rs b/scripts/sao-ui/src/style.rs new file mode 100644 index 0000000..afc002f --- /dev/null +++ b/scripts/sao-ui/src/style.rs @@ -0,0 +1,170 @@ +use canary_script::Color; + +/// A reusable set of colors. Used by default widget styles. +pub struct Palette { + pub base: Color, + pub base_hover: Color, + pub base_active: Color, + pub surface: Color, + pub overlay: Color, + pub text: Color, + pub black: Color, + pub red: Color, + pub green: Color, + pub yellow: Color, + pub blue: Color, + pub magenta: Color, + pub cyan: Color, + pub white: Color, +} + +impl Palette { + pub fn make_label_pairs(&self) -> Vec<(&'static str, Color)> { + vec![ + ("Base", self.base), + ("Base Hover", self.base_hover), + ("Base Active", self.base_active), + ("Surface", self.surface), + ("Overlay", self.overlay), + ("Text", self.text), + ("Black", self.black), + ("Red", self.red), + ("Green", self.green), + ("Yellow", self.yellow), + ("Blue", self.blue), + ("Magenta", self.magenta), + ("Cyan", self.cyan), + ("White", self.white), + ] + } +} + +/// The common base color alpha shared between all themes. +pub const BASE_ALPHA: u8 = 0xc0; + +/// The common base_hover color alpha shared between all themes. +pub const BASE_HOVER_ALPHA: u8 = 0xe0; + +/// Converts 0xrrggbb hex to an opaque [Color]. +pub const fn hex(rgb: u32) -> Color { + Color((rgb << 8) | 0xff) +} + +/// Sword Art Online color palette. +pub const SAO_PALETTE: Palette = Palette { + base: Color::WHITE.with_alpha(BASE_ALPHA), + base_hover: Color::WHITE.with_alpha(BASE_HOVER_ALPHA), + base_active: Color::YELLOW, + surface: Color::WHITE, + overlay: Color::WHITE, + text: Color::BLACK, + black: Color::BLACK, + red: Color::RED, + green: Color::GREEN, + yellow: Color::YELLOW, + blue: Color::BLUE, + magenta: Color::MAGENTA, + cyan: Color::CYAN, + white: Color::WHITE, +}; + +/// Rose Pine color palette. +pub const ROSE_PINE_PALETTE: Palette = Palette { + base: hex(0x191724).with_alpha(BASE_ALPHA), + base_hover: hex(0x21202ee0).with_alpha(BASE_HOVER_ALPHA), // Highlight Low + base_active: hex(0x403d52), // Highlight Med + surface: hex(0x1f1d2e), + overlay: hex(0x26233a), + text: hex(0xe0def4), + black: hex(0x6e6a86), // Muted + red: hex(0xeb6f92), // Love + green: hex(0x7fb59f), // ??? (not in Rose Pine?) + yellow: hex(0xf6c177), // Gold + blue: hex(0x31748f), // Pine + magenta: hex(0xc4a7e7), // Iris + cyan: hex(0x9ccfd8), // Foam + white: hex(0xe0def4), // Text +}; + +/// Rose Pine Moon color palette. +pub const ROSE_PINE_MOON_PALETTE: Palette = Palette { + base: hex(0x232136).with_alpha(BASE_ALPHA), + base_hover: hex(0x2a283e).with_alpha(BASE_HOVER_ALPHA), // Highlight Low + base_active: hex(0x44415a), // Highlight Med + surface: hex(0x2a273f), + overlay: hex(0x393552), + text: hex(0xe0def4), + black: hex(0x6e6a86), // Muted + red: hex(0xeb6f92), // Love + green: hex(0x7fb59f), // ??? (not in Rose Pine?) + yellow: hex(0xf6c177), // Gold + blue: hex(0x3e8fb0), // Pine + magenta: hex(0xc4a7e7), // Iris + cyan: hex(0x9ccfd8), // Foam + white: hex(0xe0def4), // Text +}; + +/// [Arctica](https://github.com/sashakoshka/arctica) indexable color theme. +pub const ARCTICA: [Color; 24] = [ + hex(0x242933), + hex(0x2e3440), + hex(0x3b4252), + hex(0x4c566a), + hex(0xeceff4), + hex(0xd8dee9), + hex(0xc2c9d6), + hex(0xaeb7c6), + hex(0xa8555d), + hex(0xb77763), + hex(0xcdb179), + hex(0x8ba277), + hex(0x769b9b), + hex(0x72a1ae), + hex(0x5e81ac), + hex(0x92738c), + hex(0xbf616a), + hex(0xd08770), + hex(0xebcb8b), + hex(0xa3be8c), + hex(0x8fbcbb), + hex(0x88c0d0), + hex(0x81a1c1), + hex(0xb48ead), +]; + +/// [Arctica](https://github.com/sashakoshka/arctica) color palette. +pub const ARCTICA_PALETTE: Palette = Palette { + base: ARCTICA[0].with_alpha(BASE_ALPHA), + base_hover: ARCTICA[1].with_alpha(BASE_HOVER_ALPHA), + base_active: ARCTICA[13], + surface: ARCTICA[2], + overlay: ARCTICA[3], + text: ARCTICA[5], + black: ARCTICA[3], + red: ARCTICA[8], + green: ARCTICA[11], + yellow: ARCTICA[10], + blue: ARCTICA[14], + magenta: ARCTICA[15], + cyan: ARCTICA[13], + white: ARCTICA[7], +}; + +/// Common measurements for widget shapes. +pub struct Metrics { + pub surface_rounding: f32, +} + +/// Common default parameters for widget styles. +pub struct Theme { + pub palette: Palette, + pub metrics: Metrics, +} + +/// The global theme. +pub const THEME: Theme = Theme { + palette: ARCTICA_PALETTE, + metrics: Metrics { + surface_rounding: 5.0, + }, +}; diff --git a/scripts/sao-ui/src/widgets/button.rs b/scripts/sao-ui/src/widgets/button.rs index d2b8859..846eae5 100644 --- a/scripts/sao-ui/src/widgets/button.rs +++ b/scripts/sao-ui/src/widgets/button.rs @@ -118,6 +118,8 @@ pub struct RectButtonStyle { pub inactive_color: Color, pub hover_color: Color, pub selected_color: Color, + pub icon_color: Color, + pub label_color: Color, } impl Default for RectButtonStyle { @@ -129,9 +131,11 @@ impl Default for RectButtonStyle { label_baseline: 0.25, icon_scale_factor: 0.8, icon_margin_factor: 1.1, - inactive_color: Color::WHITE.with_alpha(0x40), - hover_color: Color::WHITE.with_alpha(0xb0), - selected_color: Color::YELLOW, + inactive_color: THEME.palette.base, + hover_color: THEME.palette.base_hover, + selected_color: THEME.palette.base_active, + icon_color: THEME.palette.black, + label_color: THEME.palette.text, } } } @@ -168,7 +172,7 @@ impl RectButton { label_left += margin; alignment = HorizontalAlignment::Left; let scale = rect.height() * style.icon_scale_factor; - let color = Color::BLACK; + let color = style.icon_color; let cx = rect.tl.x + margin / 2.0; let cy = rect.tl.y + rect.height() / 2.0; let center = Vec2::new(cx, cy); @@ -182,7 +186,7 @@ impl RectButton { let right = rect.br.x; let baseline = rect.tl.y; let baseline = (rect.height() * (1.0 - style.label_baseline)) + baseline; - let color = Color::BLACK; + let color = style.label_color; Label::new(text, alignment, scale, color, left, right, baseline) }); diff --git a/scripts/sao-ui/src/widgets/dialog.rs b/scripts/sao-ui/src/widgets/dialog.rs index f8dea23..f954984 100644 --- a/scripts/sao-ui/src/widgets/dialog.rs +++ b/scripts/sao-ui/src/widgets/dialog.rs @@ -23,8 +23,8 @@ impl DialogResponse { pub fn get_color(&self) -> Color { match self { - DialogResponse::Yes => Color::BLUE, - DialogResponse::No => Color::RED, + DialogResponse::Yes => THEME.palette.blue, + DialogResponse::No => THEME.palette.red, } } } @@ -40,7 +40,7 @@ pub struct DialogStyle { impl Default for DialogStyle { fn default() -> Self { Self { - rounding: 5.0, + rounding: THEME.metrics.surface_rounding, header: Default::default(), body: Default::default(), footer: Default::default(), @@ -61,10 +61,10 @@ pub struct DialogHeaderStyle { impl Default for DialogHeaderStyle { fn default() -> Self { Self { - color: Color::WHITE, + color: THEME.palette.surface, height: 20.0, text_font: Font::new(crate::DISPLAY_FONT), - text_color: Color::BLACK, + text_color: THEME.palette.text, text_scale_factor: 0.65, text_baseline: 0.25, } @@ -82,9 +82,9 @@ pub struct DialogBodyStyle { impl Default for DialogBodyStyle { fn default() -> Self { Self { - color: Color::WHITE.with_alpha(0xb0), + color: THEME.palette.base, text_font: Font::new(crate::CONTENT_FONT), - text_color: Color::BLACK, + text_color: THEME.palette.text, text_size: 5.0, } } @@ -95,6 +95,7 @@ pub struct DialogFooterStyle { pub icon_font: Font, pub button_radius: f32, pub color: Color, + pub button_fg: Color, pub height: f32, } @@ -103,7 +104,8 @@ impl Default for DialogFooterStyle { Self { icon_font: Font::new(crate::ICON_FONT), button_radius: 7.5, - color: Color::WHITE, + color: THEME.palette.surface, + button_fg: THEME.palette.white, height: 15.0, } } @@ -136,7 +138,7 @@ impl Dialog { thickness: radius * 0.05, body_color: color, ring_color: color, - icon_color: Color::WHITE, + icon_color: style.footer.button_fg, }; let text = LabelText { diff --git a/scripts/sao-ui/src/widgets/menu.rs b/scripts/sao-ui/src/widgets/menu.rs index 5c0a022..36f0ba7 100644 --- a/scripts/sao-ui/src/widgets/menu.rs +++ b/scripts/sao-ui/src/widgets/menu.rs @@ -224,6 +224,7 @@ pub struct TabMenu { impl TabMenu { const HEAD_RADIUS: f32 = 5.0; const HEAD_HEIGHT: f32 = 15.0; + const HEAD_COLOR: Color = THEME.palette.surface; const TAB_WIDTH: f32 = 15.0; const TAB_HEIGHT: f32 = 25.0; const TAB_NUM: usize = 6; @@ -235,9 +236,9 @@ impl TabMenu { radius: Self::HEAD_HEIGHT * 0.25, spacing: Self::HEAD_HEIGHT * 0.1, thickness: Self::HEAD_HEIGHT * 0.05, - body_color: Color::WHITE, - ring_color: Color::BLACK, - icon_color: Color::BLACK, + body_color: Self::HEAD_COLOR, + ring_color: THEME.palette.black, + icon_color: THEME.palette.black, }; const HEAD_BUTTON_MARGIN: f32 = Self::HEAD_HEIGHT / 2.0; @@ -348,20 +349,18 @@ impl Container for TabMenu { } fn draw(&mut self, ctx: &DrawContext) { - let head_color = Color::WHITE; - ctx.draw_partially_rounded_rect( CornerFlags::BOTTOM_RIGHT, self.separator_rect, Self::INNER_RADIUS, - head_color, + Self::HEAD_COLOR, ); ctx.draw_partially_rounded_rect( CornerFlags::TOP, self.head_rect, Self::HEAD_RADIUS, - head_color, + Self::HEAD_COLOR, ); } } diff --git a/scripts/sao-ui/src/widgets/mod.rs b/scripts/sao-ui/src/widgets/mod.rs index a9968df..310bf1a 100644 --- a/scripts/sao-ui/src/widgets/mod.rs +++ b/scripts/sao-ui/src/widgets/mod.rs @@ -8,6 +8,7 @@ pub mod button; pub mod dialog; pub mod flex; pub mod menu; +pub mod palette; pub mod scroll; pub mod shell; pub mod text; @@ -75,6 +76,7 @@ impl Widget for T { pub mod prelude { pub use super::*; pub use crate::anim::Animation; + pub use crate::style::{self, THEME}; pub use canary_script::{*, api::*}; pub use keyframe::functions::*; } diff --git a/scripts/sao-ui/src/widgets/palette.rs b/scripts/sao-ui/src/widgets/palette.rs new file mode 100644 index 0000000..d045feb --- /dev/null +++ b/scripts/sao-ui/src/widgets/palette.rs @@ -0,0 +1,106 @@ +use super::prelude::*; +use shell::Offset; +use text::{HorizontalAlignment, Label, LabelText}; + +pub struct PaletteStyle { + pub bg: Color, + pub text: Color, + pub rounding: f32, + pub text_size: f32, + pub line_spacing: f32, + pub color_radius: f32, + pub margin: Rect, +} + +impl Default for PaletteStyle { + fn default() -> Self { + Self { + bg: THEME.palette.surface, + text: THEME.palette.text, + rounding: THEME.metrics.surface_rounding, + text_size: 5.0, + line_spacing: 8.0, + color_radius: 3.0, + margin: Rect::from_xy_size(Vec2::splat(10.0), Vec2::ZERO), + } + } +} + +/// A widget that displays all the colors in the global palette. +pub struct Palette { + body: Rect, + style: PaletteStyle, + labels: Vec>, + colors: Vec<(Vec2, Color)>, +} + +impl Palette { + pub fn new(style: PaletteStyle) -> Self { + let width = 70.0; + let pairs = THEME.palette.make_label_pairs(); + let label_font = Font::new(crate::CONTENT_FONT); + + let mut label_cursor = Vec2::new(0.0, style.line_spacing) + style.margin.tl; + let mut color_cursor = Vec2::new( + width - style.margin.br.x, + style.line_spacing / 2.0 + style.margin.tl.y, + ); + let mut labels = Vec::new(); + let mut colors = Vec::new(); + + for (text, color) in pairs { + let text = LabelText { + font: label_font, + text: text.to_string(), + }; + + let label = Label::new( + text, + HorizontalAlignment::Left, + style.text_size, + style.text, + 0.0, + 0.0, + 0.0, + ); + + let label = Offset::new(label, label_cursor); + + labels.push(label); + + colors.push((color_cursor, color)); + + label_cursor.y += style.line_spacing; + color_cursor.y += style.line_spacing; + } + + let height = label_cursor.y + style.margin.br.y; + + Self { + body: Rect::from_xy_size(Vec2::ZERO, Vec2::new(width, height)), + style, + labels, + colors, + } + } +} + +impl RectBounds for Palette { + fn get_bounds(&self) -> Rect { + self.body + } +} + +impl Widget for Palette { + fn draw(&mut self, ctx: &DrawContext) { + ctx.draw_rounded_rect(self.body, self.style.rounding, self.style.bg); + + for label in self.labels.iter_mut() { + label.draw(ctx); + } + + for (center, color) in self.colors.iter() { + ctx.draw_circle(*center, self.style.color_radius, *color); + } + } +} diff --git a/scripts/sao-ui/src/widgets/scroll.rs b/scripts/sao-ui/src/widgets/scroll.rs index 130cbc5..0925313 100644 --- a/scripts/sao-ui/src/widgets/scroll.rs +++ b/scripts/sao-ui/src/widgets/scroll.rs @@ -21,11 +21,11 @@ impl Default for ScrollBarStyle { margin: Vec2::splat(2.0), body_radius: 1.0, body_width: 3.0, - body_idle_color: Color(0x7f7f7fff), - body_hover_color: Color(0xb0b0b0ff), - body_selected_color: Color::WHITE, + body_idle_color: THEME.palette.base, + body_hover_color: THEME.palette.base_hover, + body_selected_color: THEME.palette.base_active, rail_width: 1.0, - rail_color: Color(0xa0a0a07f), + rail_color: THEME.palette.base, } } }