Merge branch 'main' into main
This commit is contained in:
commit
50410c7f4f
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use glium::glutin::event_loop::EventLoopBuilder;
|
use glium::glutin::event_loop::EventLoopBuilder;
|
||||||
|
|
||||||
use canary_magpie::service::*;
|
use canary_magpie::service::*;
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
@ -21,6 +24,7 @@ pub type PanelId = u32;
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct CreatePanel {
|
pub struct CreatePanel {
|
||||||
pub id: PanelId,
|
pub id: PanelId,
|
||||||
|
pub protocol: String,
|
||||||
pub script: PathBuf,
|
pub script: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use canary::{DrawCommand, Vec2, PX_PER_MM};
|
use canary::{DrawCommand, Vec2, PX_PER_MM};
|
||||||
use glium::Surface;
|
use glium::Surface;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
@ -96,7 +99,11 @@ impl Client {
|
||||||
while let Some(msg) = self.messenger.recv() {
|
while let Some(msg) = self.messenger.recv() {
|
||||||
println!("Client #{}: {:?}", self.token.0, msg);
|
println!("Client #{}: {:?}", self.token.0, msg);
|
||||||
match msg {
|
match msg {
|
||||||
MagpieServerMsg::CreatePanel(CreatePanel { id, script }) => {
|
MagpieServerMsg::CreatePanel(CreatePanel {
|
||||||
|
id,
|
||||||
|
protocol,
|
||||||
|
script,
|
||||||
|
}) => {
|
||||||
let window = self.data.write().new_window_id();
|
let window = self.data.write().new_window_id();
|
||||||
|
|
||||||
if let Some(old_id) = self.id_to_window.insert(id, window) {
|
if let Some(old_id) = self.id_to_window.insert(id, window) {
|
||||||
|
@ -104,7 +111,11 @@ impl Client {
|
||||||
let _ = self.window_sender.send_event(msg);
|
let _ = self.window_sender.send_event(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = WindowMessage::OpenWindow { id: window, script };
|
let msg = WindowMessage::OpenWindow {
|
||||||
|
id: window,
|
||||||
|
protocol,
|
||||||
|
script,
|
||||||
|
};
|
||||||
let _ = self.window_sender.send_event(msg);
|
let _ = self.window_sender.send_event(msg);
|
||||||
}
|
}
|
||||||
MagpieServerMsg::SendMessage(SendMessage { id, msg }) => {
|
MagpieServerMsg::SendMessage(SendMessage { id, msg }) => {
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
pub mod gl;
|
pub mod gl;
|
||||||
pub mod ipc;
|
pub mod ipc;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
@ -14,10 +17,19 @@ use crate::service::ipc::{IpcMessage, IpcMessageSender};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum WindowMessage {
|
pub enum WindowMessage {
|
||||||
OpenWindow { id: usize, script: PathBuf },
|
OpenWindow {
|
||||||
CloseWindow { id: usize },
|
id: usize,
|
||||||
|
protocol: String,
|
||||||
|
script: PathBuf,
|
||||||
|
},
|
||||||
|
CloseWindow {
|
||||||
|
id: usize,
|
||||||
|
},
|
||||||
Quit,
|
Quit,
|
||||||
SendMessage { id: usize, msg: Vec<u8> },
|
SendMessage {
|
||||||
|
id: usize,
|
||||||
|
msg: Vec<u8>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type WindowMessageSender = EventLoopProxy<WindowMessage>;
|
pub type WindowMessageSender = EventLoopProxy<WindowMessage>;
|
||||||
|
@ -152,11 +164,11 @@ impl WindowStore {
|
||||||
message: WindowMessage,
|
message: WindowMessage,
|
||||||
) -> anyhow::Result<bool> {
|
) -> anyhow::Result<bool> {
|
||||||
match message {
|
match message {
|
||||||
WindowMessage::OpenWindow { id, script } => {
|
WindowMessage::OpenWindow { id, protocol, script } => {
|
||||||
println!("Opening window {} with script {:?}", id, script);
|
println!("Opening window {} with script {:?}", id, script);
|
||||||
let module = std::fs::read(script)?;
|
let module = std::fs::read(script)?;
|
||||||
let mut script = self.runtime.load_module(&module)?;
|
let mut script = self.runtime.load_module(&module)?;
|
||||||
let panel = script.create_panel(vec![])?;
|
let panel = script.create_panel(&protocol, vec![])?;
|
||||||
let window = Window::new(panel, &event_loop)?;
|
let window = Window::new(panel, &event_loop)?;
|
||||||
let window_id = window.get_id();
|
let window_id = window.get_id();
|
||||||
self.windows.insert(window_id, window);
|
self.windows.insert(window_id, window);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
name = "canary-music-player"
|
name = "canary-music-player"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "canary-music-player"
|
name = "canary-music-player"
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub use serde;
|
pub use serde;
|
||||||
pub use serde_json;
|
pub use serde_json;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum PlaybackStatus {
|
pub enum PlaybackStatus {
|
||||||
/// A track is currently playing.
|
/// A track is currently playing.
|
||||||
Playing,
|
Playing,
|
||||||
|
@ -15,7 +18,7 @@ pub enum PlaybackStatus {
|
||||||
Stopped,
|
Stopped,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum LoopStatus {
|
pub enum LoopStatus {
|
||||||
/// The playback will stop when there are no more tracks to play.
|
/// The playback will stop when there are no more tracks to play.
|
||||||
None,
|
None,
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use canary_music_player::*;
|
use canary_music_player::*;
|
||||||
use canary_magpie::client::MagpieClient;
|
use canary_magpie::client::MagpieClient;
|
||||||
use canary_magpie::protocol::{CreatePanel, MagpieServerMsg};
|
use canary_magpie::protocol::{CreatePanel, MagpieServerMsg};
|
||||||
|
use canary_music_player::*;
|
||||||
use mpris::PlayerFinder;
|
use mpris::PlayerFinder;
|
||||||
|
|
||||||
pub struct MetadataTracker {
|
pub struct MetadataTracker {
|
||||||
|
@ -8,8 +12,8 @@ pub struct MetadataTracker {
|
||||||
pub track: TrackInfo,
|
pub track: TrackInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<mpris::Metadata> for MetadataTracker {
|
impl From<&mpris::Metadata> for MetadataTracker {
|
||||||
fn from(metadata: mpris::Metadata) -> Self {
|
fn from(metadata: &mpris::Metadata) -> Self {
|
||||||
let album = AlbumInfo {
|
let album = AlbumInfo {
|
||||||
title: metadata.album_name().map(ToString::to_string),
|
title: metadata.album_name().map(ToString::to_string),
|
||||||
artists: metadata
|
artists: metadata
|
||||||
|
@ -36,14 +40,21 @@ impl From<mpris::Metadata> for MetadataTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetadataTracker {
|
impl MetadataTracker {
|
||||||
pub fn new(magpie: &mut MagpieClient, metadata: mpris::Metadata) -> Self {
|
pub fn new(magpie: &mut MagpieClient, metadata: &mpris::Metadata) -> Self {
|
||||||
let new: Self = metadata.into();
|
let new: Self = metadata.into();
|
||||||
magpie.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
|
magpie.send_json_message(0, &InMsg::AlbumChanged(new.album.clone()));
|
||||||
magpie.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
magpie.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
||||||
|
magpie.send_json_message(
|
||||||
|
0,
|
||||||
|
&InMsg::ProgressChanged(ProgressChanged {
|
||||||
|
position: 0.0,
|
||||||
|
length: metadata.length().map(|l| l.as_secs_f32()),
|
||||||
|
}),
|
||||||
|
);
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, messenger: &mut MagpieClient, metadata: mpris::Metadata) {
|
pub fn update(&mut self, messenger: &mut MagpieClient, metadata: &mpris::Metadata) {
|
||||||
let new: Self = metadata.into();
|
let new: Self = metadata.into();
|
||||||
|
|
||||||
if self.album != new.album {
|
if self.album != new.album {
|
||||||
|
@ -52,6 +63,13 @@ impl MetadataTracker {
|
||||||
|
|
||||||
if self.track != new.track {
|
if self.track != new.track {
|
||||||
messenger.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
messenger.send_json_message(0, &InMsg::TrackChanged(new.track.clone()));
|
||||||
|
messenger.send_json_message(
|
||||||
|
0,
|
||||||
|
&InMsg::ProgressChanged(ProgressChanged {
|
||||||
|
position: 0.0,
|
||||||
|
length: metadata.length().map(|l| l.as_secs_f32()),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
*self = new;
|
*self = new;
|
||||||
|
@ -68,8 +86,9 @@ fn main() {
|
||||||
let player_finder = PlayerFinder::new().expect("Could not connect to D-Bus");
|
let player_finder = PlayerFinder::new().expect("Could not connect to D-Bus");
|
||||||
|
|
||||||
let mut magpie = MagpieClient::new().unwrap();
|
let mut magpie = MagpieClient::new().unwrap();
|
||||||
|
let protocol = "tebibyte-media.desktop.music-player-controller".to_string();
|
||||||
let script = std::path::PathBuf::from(&module_path);
|
let script = std::path::PathBuf::from(&module_path);
|
||||||
let msg = CreatePanel { id: 0, script };
|
let msg = CreatePanel { id: 0, protocol, script };
|
||||||
let msg = MagpieServerMsg::CreatePanel(msg);
|
let msg = MagpieServerMsg::CreatePanel(msg);
|
||||||
magpie.messenger.send(&msg).unwrap();
|
magpie.messenger.send(&msg).unwrap();
|
||||||
|
|
||||||
|
@ -110,7 +129,7 @@ fn main() {
|
||||||
magpie.send_json_message(0, &InMsg::Connected);
|
magpie.send_json_message(0, &InMsg::Connected);
|
||||||
|
|
||||||
let metadata = player.get_metadata().unwrap();
|
let metadata = player.get_metadata().unwrap();
|
||||||
let mut metadata_tracker = MetadataTracker::new(&mut magpie, metadata);
|
let mut metadata_tracker = MetadataTracker::new(&mut magpie, &metadata);
|
||||||
|
|
||||||
let mut events = match player.events() {
|
let mut events = match player.events() {
|
||||||
Ok(events) => events,
|
Ok(events) => events,
|
||||||
|
@ -150,7 +169,7 @@ fn main() {
|
||||||
volume: volume as f32,
|
volume: volume as f32,
|
||||||
}),
|
}),
|
||||||
PlayerShutDown => None,
|
PlayerShutDown => None,
|
||||||
TrackChanged(metadata) => {
|
TrackChanged(ref metadata) => {
|
||||||
metadata_tracker.update(&mut magpie, metadata);
|
metadata_tracker.update(&mut magpie, metadata);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright (c) 2022 Marceline Cramer
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use canary::{CursorEventKind, Panel, Runtime, Script};
|
use canary::{CursorEventKind, Panel, Runtime, Script, PX_PER_MM};
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ struct App {
|
||||||
panels: Vec<PanelWindow>,
|
panels: Vec<PanelWindow>,
|
||||||
next_idx: usize,
|
next_idx: usize,
|
||||||
last_update: Instant,
|
last_update: Instant,
|
||||||
|
protocol_buf: String,
|
||||||
bind_message_buf: String,
|
bind_message_buf: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +47,7 @@ impl App {
|
||||||
panels: vec![],
|
panels: vec![],
|
||||||
next_idx: 0,
|
next_idx: 0,
|
||||||
last_update: Instant::now(),
|
last_update: Instant::now(),
|
||||||
|
protocol_buf: String::new(),
|
||||||
bind_message_buf: String::new(),
|
bind_message_buf: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,12 +58,16 @@ impl eframe::App for App {
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
|
|
||||||
egui::SidePanel::left("left_panel").show(ctx, |ui| {
|
egui::SidePanel::left("left_panel").show(ctx, |ui| {
|
||||||
|
ui.label("Protocol name:");
|
||||||
|
ui.text_edit_singleline(&mut self.protocol_buf);
|
||||||
|
|
||||||
|
ui.label("Bind message:");
|
||||||
let text_edit = egui::TextEdit::multiline(&mut self.bind_message_buf).code_editor();
|
let text_edit = egui::TextEdit::multiline(&mut self.bind_message_buf).code_editor();
|
||||||
ui.add(text_edit);
|
ui.add(text_edit);
|
||||||
|
|
||||||
if ui.button("Bind Panel").clicked() {
|
if ui.button("Bind Panel").clicked() {
|
||||||
let msg = self.bind_message_buf.as_bytes().to_vec();
|
let msg = self.bind_message_buf.as_bytes().to_vec();
|
||||||
let panel = self.script.create_panel(msg).unwrap();
|
let panel = self.script.create_panel(&self.protocol_buf, msg).unwrap();
|
||||||
let index = self.next_idx;
|
let index = self.next_idx;
|
||||||
self.next_idx += 1;
|
self.next_idx += 1;
|
||||||
|
|
||||||
|
@ -70,6 +76,7 @@ impl eframe::App for App {
|
||||||
index,
|
index,
|
||||||
msg_buf: String::new(),
|
msg_buf: String::new(),
|
||||||
show_msg: false,
|
show_msg: false,
|
||||||
|
current_size: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.panels.push(panel);
|
self.panels.push(panel);
|
||||||
|
@ -91,6 +98,7 @@ pub struct PanelWindow {
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub msg_buf: String,
|
pub msg_buf: String,
|
||||||
pub show_msg: bool,
|
pub show_msg: bool,
|
||||||
|
pub current_size: egui::Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PanelWindow {
|
impl PanelWindow {
|
||||||
|
@ -101,21 +109,27 @@ impl PanelWindow {
|
||||||
ui.checkbox(&mut self.show_msg, "Show Message Editor");
|
ui.checkbox(&mut self.show_msg, "Show Message Editor");
|
||||||
});
|
});
|
||||||
|
|
||||||
let size = egui::vec2(800.0, 800.0);
|
|
||||||
let sense = egui::Sense {
|
let sense = egui::Sense {
|
||||||
click: true,
|
click: true,
|
||||||
drag: true,
|
drag: true,
|
||||||
focusable: true,
|
focusable: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_at_least(size, sense);
|
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() {
|
if let Some(hover_pos) = response.hover_pos() {
|
||||||
let local = (hover_pos - rect.left_top()) / rect.size();
|
let local = (hover_pos - rect.left_top()) * PX_PER_MM;
|
||||||
let norm = local * 2.0 - egui::vec2(1.0, 1.0);
|
let pos = canary::Vec2::new(local.x, local.y);
|
||||||
let x = norm.x;
|
|
||||||
let y = -norm.y;
|
|
||||||
let pos = canary::Vec2 { x, y };
|
|
||||||
|
|
||||||
let kind = if response.drag_started() {
|
let kind = if response.drag_started() {
|
||||||
CursorEventKind::Select
|
CursorEventKind::Select
|
||||||
|
@ -142,9 +156,9 @@ impl PanelWindow {
|
||||||
canary::DrawCommand::Mesh { vertices, indices } => {
|
canary::DrawCommand::Mesh { vertices, indices } => {
|
||||||
for v in vertices.iter() {
|
for v in vertices.iter() {
|
||||||
use egui::epaint::Vertex;
|
use egui::epaint::Vertex;
|
||||||
let pos = egui::pos2(v.position.x, -v.position.y);
|
let pos = v.position / PX_PER_MM;
|
||||||
let pos = pos.to_vec2() / 2.0 + egui::vec2(0.5, 0.5);
|
let pos = egui::pos2(pos.x, pos.y);
|
||||||
let pos = rect.left_top() + pos * rect.size();
|
let pos = pos + rect.left_top().to_vec2();
|
||||||
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
|
let (r, g, b, a) = v.color.to_rgba_unmultiplied();
|
||||||
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
|
let color = egui::Color32::from_rgba_unmultiplied(r, g, b, a);
|
||||||
let v = Vertex { pos, uv, color };
|
let v = Vertex { pos, uv, color };
|
||||||
|
|
|
@ -7,11 +7,17 @@ use super::*;
|
||||||
|
|
||||||
static mut PANEL_IMPLS: Vec<Box<dyn PanelImpl>> = Vec::new();
|
static mut PANEL_IMPLS: Vec<Box<dyn PanelImpl>> = Vec::new();
|
||||||
|
|
||||||
pub fn bind_panel<T: BindPanel>(panel: u32, msg: u32) -> u32 {
|
pub fn bind_panel(
|
||||||
|
cb: impl Fn(Panel, Message, Message) -> Box<dyn PanelImpl>,
|
||||||
|
panel: u32,
|
||||||
|
protocol: u32,
|
||||||
|
msg: u32,
|
||||||
|
) -> u32 {
|
||||||
unsafe {
|
unsafe {
|
||||||
let panel = Panel(panel);
|
let panel = Panel(panel);
|
||||||
|
let protocol = Message(protocol);
|
||||||
let msg = Message(msg);
|
let msg = Message(msg);
|
||||||
let panel_impl = T::bind(panel, msg);
|
let panel_impl = cb(panel, protocol, msg);
|
||||||
let id = PANEL_IMPLS.len() as u32;
|
let id = PANEL_IMPLS.len() as u32;
|
||||||
PANEL_IMPLS.push(panel_impl);
|
PANEL_IMPLS.push(panel_impl);
|
||||||
id
|
id
|
||||||
|
|
|
@ -9,10 +9,10 @@ pub mod abi;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! export_abi {
|
macro_rules! export_abi {
|
||||||
($panel_impl: ident) => {
|
($bind_panel: ident) => {
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn bind_panel(panel: u32, msg: u32) -> u32 {
|
pub extern "C" fn bind_panel(panel: u32, protocol: u32, msg: u32) -> u32 {
|
||||||
::canary_script::api::abi::bind_panel::<$panel_impl>(panel, msg)
|
::canary_script::api::abi::bind_panel($bind_panel, panel, protocol, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -42,10 +42,6 @@ macro_rules! export_abi {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BindPanel {
|
|
||||||
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PanelImpl {
|
pub trait PanelImpl {
|
||||||
fn update(&mut self, dt: f32);
|
fn update(&mut self, dt: f32);
|
||||||
fn draw(&mut self);
|
fn draw(&mut self);
|
||||||
|
@ -375,7 +371,6 @@ impl DrawContext {
|
||||||
self.draw_rect(bottom_edge, color);
|
self.draw_rect(bottom_edge, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
self.draw_rect(inner_rect, color);
|
self.draw_rect(inner_rect, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
name = "canary-music-player-script"
|
name = "canary-music-player-script"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
use canary_script::*;
|
|
||||||
use api::*;
|
use api::*;
|
||||||
|
use canary_script::*;
|
||||||
|
|
||||||
canary_script::export_abi!(MusicPlayerPanel);
|
canary_script::export_abi!(bind_panel_impl);
|
||||||
|
|
||||||
|
pub fn bind_panel_impl(panel: Panel, _protocol: Message, message: Message) -> Box<dyn PanelImpl> {
|
||||||
|
MusicPlayerPanel::bind(panel, message)
|
||||||
|
}
|
||||||
|
|
||||||
const DISPLAY_FONT: &str = "Liberation Sans";
|
const DISPLAY_FONT: &str = "Liberation Sans";
|
||||||
|
|
||||||
|
@ -14,19 +21,6 @@ pub struct MusicPlayerPanel {
|
||||||
label: Label,
|
label: Label,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BindPanel for MusicPlayerPanel {
|
|
||||||
fn bind(panel: Panel, message: Message) -> Box<dyn PanelImpl> {
|
|
||||||
let display_font = Font::new(DISPLAY_FONT);
|
|
||||||
let label = Label::new(display_font, "Hello, world!".into(), 1.2);
|
|
||||||
let panel = Self {
|
|
||||||
panel,
|
|
||||||
display_font,
|
|
||||||
label,
|
|
||||||
};
|
|
||||||
Box::new(panel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PanelImpl for MusicPlayerPanel {
|
impl PanelImpl for MusicPlayerPanel {
|
||||||
fn update(&mut self, dt: f32) {}
|
fn update(&mut self, dt: f32) {}
|
||||||
|
|
||||||
|
@ -39,20 +33,31 @@ impl PanelImpl for MusicPlayerPanel {
|
||||||
self.label.draw(&ctx, offset, size, color);
|
self.label.draw(&ctx, offset, size, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_resize(&mut self, new_size: Vec2) {
|
fn on_resize(&mut self, new_size: Vec2) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {}
|
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {}
|
||||||
|
|
||||||
fn on_message(&mut self, msg: Message) {
|
fn on_message(&mut self, msg: Message) {
|
||||||
use canary_music_player::{InMsg, serde_json};
|
use canary_music_player::{serde_json, InMsg};
|
||||||
let msg = msg.to_vec();
|
let msg = msg.to_vec();
|
||||||
let msg = serde_json::from_slice::<InMsg>(&msg);
|
let msg = serde_json::from_slice::<InMsg>(&msg);
|
||||||
self.label.set_text(format!("{:#?}", msg));
|
self.label.set_text(format!("{:#?}", msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MusicPlayerPanel {
|
||||||
|
pub fn bind(panel: Panel, _message: Message) -> Box<dyn PanelImpl> {
|
||||||
|
let display_font = Font::new(DISPLAY_FONT);
|
||||||
|
let label = Label::new(display_font, "Hello, world!".into(), 1.2);
|
||||||
|
let panel = Self {
|
||||||
|
panel,
|
||||||
|
display_font,
|
||||||
|
label,
|
||||||
|
};
|
||||||
|
Box::new(panel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Label {
|
pub struct Label {
|
||||||
font: Font,
|
font: Font,
|
||||||
text: String,
|
text: String,
|
||||||
|
|
|
@ -10,6 +10,7 @@ crate-type = ["cdylib"]
|
||||||
[dependencies]
|
[dependencies]
|
||||||
glam = "^0.21"
|
glam = "^0.21"
|
||||||
keyframe = "1"
|
keyframe = "1"
|
||||||
|
canary-music-player = { path = "../../apps/music-player" }
|
||||||
canary-script = { path = "../../crates/script" }
|
canary-script = { path = "../../crates/script" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use crate::Color;
|
use crate::Color;
|
||||||
use keyframe::EasingFunction;
|
use keyframe::EasingFunction;
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -1,18 +1,31 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
pub mod anim;
|
pub mod anim;
|
||||||
pub mod draw;
|
|
||||||
pub mod main_menu;
|
pub mod main_menu;
|
||||||
pub mod panel;
|
pub mod music_player;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
use canary_script::*;
|
|
||||||
use api::*;
|
use api::*;
|
||||||
use widgets::Widget;
|
use canary_script::*;
|
||||||
use main_menu::MainMenuPanel;
|
use main_menu::MainMenuPanel;
|
||||||
|
use music_player::MusicPlayerPanel;
|
||||||
|
use widgets::Widget;
|
||||||
|
|
||||||
export_abi!(MainMenuPanel);
|
export_abi!(bind_panel_impl);
|
||||||
|
|
||||||
|
fn bind_panel_impl(panel: Panel, protocol: Message, msg: Message) -> Box<dyn PanelImpl> {
|
||||||
|
let protocol = protocol.to_vec();
|
||||||
|
let protocol = String::from_utf8(protocol).unwrap();
|
||||||
|
|
||||||
|
match protocol.as_str() {
|
||||||
|
"tebibyte-media.desktop.music-player-controller" => MusicPlayerPanel::bind(panel, msg),
|
||||||
|
_ => MainMenuPanel::bind(panel, msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const ICON_FONT: &str = "Iosevka Nerd Font";
|
pub const ICON_FONT: &str = "Iosevka Nerd Font";
|
||||||
pub const DISPLAY_FONT: &str = "Homenaje";
|
pub const DISPLAY_FONT: &str = "Homenaje";
|
||||||
|
@ -23,18 +36,6 @@ pub struct ConfirmationDialogPanel {
|
||||||
dialog: widgets::dialog::Dialog,
|
dialog: widgets::dialog::Dialog,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BindPanel for ConfirmationDialogPanel {
|
|
||||||
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
|
||||||
let msg = msg.to_vec();
|
|
||||||
let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
|
|
||||||
|
|
||||||
use widgets::dialog::*;
|
|
||||||
let style = DialogStyle::default();
|
|
||||||
let dialog = Dialog::new(style, &info);
|
|
||||||
Box::new(Self { panel, dialog })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PanelImpl for ConfirmationDialogPanel {
|
impl PanelImpl for ConfirmationDialogPanel {
|
||||||
fn update(&mut self, dt: f32) {
|
fn update(&mut self, dt: f32) {
|
||||||
self.dialog.update(dt);
|
self.dialog.update(dt);
|
||||||
|
@ -53,3 +54,15 @@ impl PanelImpl for ConfirmationDialogPanel {
|
||||||
|
|
||||||
fn on_message(&mut self, _msg: Message) {}
|
fn on_message(&mut self, _msg: Message) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ConfirmationDialogPanel {
|
||||||
|
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||||
|
let msg = msg.to_vec();
|
||||||
|
let info: DialogInfo = serde_json::from_slice(&msg).unwrap();
|
||||||
|
|
||||||
|
use widgets::dialog::*;
|
||||||
|
let style = DialogStyle::default();
|
||||||
|
let dialog = Dialog::new(style, &info);
|
||||||
|
Box::new(Self { panel, dialog })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use crate::widgets::prelude::*;
|
use crate::widgets::prelude::*;
|
||||||
use crate::{DrawContext, Rect};
|
use crate::{DrawContext, Rect};
|
||||||
|
|
||||||
|
@ -12,15 +15,6 @@ pub struct MainMenuPanel {
|
||||||
menu: MainMenu,
|
menu: MainMenu,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BindPanel for MainMenuPanel {
|
|
||||||
fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
|
||||||
Box::new(Self {
|
|
||||||
panel,
|
|
||||||
menu: MainMenu::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PanelImpl for MainMenuPanel {
|
impl PanelImpl for MainMenuPanel {
|
||||||
fn update(&mut self, dt: f32) {
|
fn update(&mut self, dt: f32) {
|
||||||
Widget::update(&mut self.menu, dt);
|
Widget::update(&mut self.menu, dt);
|
||||||
|
@ -40,6 +34,15 @@ impl PanelImpl for MainMenuPanel {
|
||||||
fn on_message(&mut self, msg: Message) {}
|
fn on_message(&mut self, msg: Message) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MainMenuPanel {
|
||||||
|
pub fn bind(panel: Panel, msg: Message) -> Box<dyn PanelImpl> {
|
||||||
|
Box::new(Self {
|
||||||
|
panel,
|
||||||
|
menu: MainMenu::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct MainMenu {
|
pub struct MainMenu {
|
||||||
pub menu: Offset<SlotMenu<RoundButton>>,
|
pub menu: Offset<SlotMenu<RoundButton>>,
|
||||||
pub player_info: Reveal<Offset<PlayerInfo>>,
|
pub player_info: Reveal<Offset<PlayerInfo>>,
|
||||||
|
|
|
@ -0,0 +1,362 @@
|
||||||
|
use api::*;
|
||||||
|
use canary_script::*;
|
||||||
|
|
||||||
|
use canary_music_player::{AlbumInfo, PlaybackStatus, ProgressChanged, TrackInfo};
|
||||||
|
|
||||||
|
use crate::widgets::prelude::*;
|
||||||
|
use button::{RoundButton, RoundButtonStyle};
|
||||||
|
use dialog::{DialogBodyStyle, DialogFooterStyle};
|
||||||
|
use shell::Offset;
|
||||||
|
use text::{HorizontalAlignment, Label, LabelText};
|
||||||
|
|
||||||
|
pub struct MusicPlayerPanel {
|
||||||
|
panel: Panel,
|
||||||
|
widget: Option<MusicPlayerWidget>,
|
||||||
|
disconnected: Offset<Label>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PanelImpl for MusicPlayerPanel {
|
||||||
|
fn update(&mut self, dt: f32) {
|
||||||
|
if let Some(widget) = self.widget.as_mut() {
|
||||||
|
Widget::update(widget, dt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self) {
|
||||||
|
let ctx = DrawContext::new(self.panel);
|
||||||
|
|
||||||
|
if let Some(widget) = self.widget.as_mut() {
|
||||||
|
Widget::draw(widget, &ctx);
|
||||||
|
} else {
|
||||||
|
self.disconnected.draw(&ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_resize(&mut self, new_size: Vec2) {
|
||||||
|
self.disconnected.set_offset(new_size / 2.0);
|
||||||
|
|
||||||
|
if let Some(widget) = self.widget.as_mut() {
|
||||||
|
widget.resize(new_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_cursor_event(&mut self, kind: CursorEventKind, at: Vec2) {
|
||||||
|
if let Some(widget) = self.widget.as_mut() {
|
||||||
|
Widget::on_cursor_event(widget, kind, at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_message(&mut self, msg: Message) {
|
||||||
|
use canary_music_player::{serde_json::from_slice, InMsg};
|
||||||
|
let msg = msg.to_vec();
|
||||||
|
let msg: InMsg = match from_slice(&msg) {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
use InMsg::*;
|
||||||
|
match (self.widget.as_mut(), msg) {
|
||||||
|
(Some(_), Disconnected) => self.widget = None,
|
||||||
|
(None, Connected) => self.widget = Some(MusicPlayerWidget::new()),
|
||||||
|
(Some(widget), AlbumChanged(info)) => widget.update_album(info),
|
||||||
|
(Some(widget), TrackChanged(info)) => widget.update_track(info),
|
||||||
|
(Some(widget), PlaybackStatusChanged(status)) => widget.update_playback_status(status),
|
||||||
|
(Some(widget), ProgressChanged(progress)) => widget.update_progress(progress),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicPlayerPanel {
|
||||||
|
pub fn bind(panel: Panel, _msg: Message) -> Box<dyn PanelImpl> {
|
||||||
|
let dc_text = LabelText {
|
||||||
|
font: Font::new(crate::DISPLAY_FONT),
|
||||||
|
text: "Disconnected".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let disconnected = Label::new_centered(dc_text, 10.0, Color::WHITE);
|
||||||
|
let disconnected = Offset::new(disconnected, Vec2::ZERO);
|
||||||
|
|
||||||
|
Box::new(Self {
|
||||||
|
panel,
|
||||||
|
widget: None,
|
||||||
|
disconnected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MusicPlayerStyle {
|
||||||
|
pub body: DialogBodyStyle,
|
||||||
|
pub footer: DialogFooterStyle,
|
||||||
|
pub rounding: f32,
|
||||||
|
pub art_margin: f32,
|
||||||
|
pub button_spacing: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MusicPlayerStyle {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
body: Default::default(),
|
||||||
|
footer: Default::default(),
|
||||||
|
rounding: 5.0,
|
||||||
|
art_margin: 5.0,
|
||||||
|
button_spacing: 15.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MusicPlayerWidget {
|
||||||
|
artist: Offset<Label>,
|
||||||
|
album: Offset<Label>,
|
||||||
|
track: Offset<Label>,
|
||||||
|
previous: Offset<RoundButton>,
|
||||||
|
play: Offset<RoundButton>,
|
||||||
|
next: Offset<RoundButton>,
|
||||||
|
position: Offset<Label>,
|
||||||
|
duration: Offset<Label>,
|
||||||
|
style: MusicPlayerStyle,
|
||||||
|
art_rect: Rect,
|
||||||
|
body_rect: Rect,
|
||||||
|
footer_rect: Rect,
|
||||||
|
position_secs: f32,
|
||||||
|
position_dirty: bool,
|
||||||
|
status: PlaybackStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Container for MusicPlayerWidget {
|
||||||
|
fn with_children(&mut self, mut f: impl FnMut(&mut dyn Widget)) {
|
||||||
|
f(&mut self.artist);
|
||||||
|
f(&mut self.album);
|
||||||
|
f(&mut self.track);
|
||||||
|
f(&mut self.previous);
|
||||||
|
f(&mut self.play);
|
||||||
|
f(&mut self.next);
|
||||||
|
f(&mut self.position);
|
||||||
|
f(&mut self.duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, dt: f32) {
|
||||||
|
if let PlaybackStatus::Playing = self.status {
|
||||||
|
self.position_secs += dt;
|
||||||
|
self.position_dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.position_dirty {
|
||||||
|
self.position_dirty = false;
|
||||||
|
self.position
|
||||||
|
.set_text(&Self::format_time(self.position_secs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, ctx: &DrawContext) {
|
||||||
|
ctx.draw_partially_rounded_rect(
|
||||||
|
CornerFlags::TOP,
|
||||||
|
self.body_rect,
|
||||||
|
self.style.rounding,
|
||||||
|
self.style.body.color,
|
||||||
|
);
|
||||||
|
|
||||||
|
ctx.draw_rounded_rect(self.art_rect, self.style.rounding, Color::MAGENTA);
|
||||||
|
|
||||||
|
ctx.draw_partially_rounded_rect(
|
||||||
|
CornerFlags::BOTTOM,
|
||||||
|
self.footer_rect,
|
||||||
|
self.style.rounding,
|
||||||
|
self.style.footer.color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicPlayerWidget {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let style = MusicPlayerStyle::default();
|
||||||
|
let display_font = Font::new(crate::DISPLAY_FONT);
|
||||||
|
let content_font = Font::new(crate::CONTENT_FONT);
|
||||||
|
|
||||||
|
let make_body_label = |content: &str| {
|
||||||
|
let text = LabelText {
|
||||||
|
font: display_font,
|
||||||
|
text: content.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let label = Label::new_centered(text, 10.0, Color::BLACK);
|
||||||
|
Offset::new(label, Vec2::ZERO)
|
||||||
|
};
|
||||||
|
|
||||||
|
let make_footer_label = |content: &str| {
|
||||||
|
let text = LabelText {
|
||||||
|
font: content_font,
|
||||||
|
text: content.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = style.footer.height;
|
||||||
|
let scale = size * 0.4;
|
||||||
|
let baseline = size * 0.125;
|
||||||
|
let label = Label::new(
|
||||||
|
text,
|
||||||
|
HorizontalAlignment::Center,
|
||||||
|
scale,
|
||||||
|
Color::BLACK,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
baseline,
|
||||||
|
);
|
||||||
|
Offset::new(label, Vec2::ZERO)
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon_font = Font::new(crate::ICON_FONT);
|
||||||
|
|
||||||
|
let prev_text = LabelText {
|
||||||
|
font: icon_font,
|
||||||
|
text: "玲".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let play_text = LabelText {
|
||||||
|
font: icon_font,
|
||||||
|
text: "契".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let next_text = LabelText {
|
||||||
|
font: icon_font,
|
||||||
|
text: "怜".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let primary_button = RoundButtonStyle {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev = RoundButton::new(secondary_button.clone(), Some(prev_text));
|
||||||
|
let play = RoundButton::new(primary_button, Some(play_text));
|
||||||
|
let next = RoundButton::new(secondary_button, Some(next_text));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
artist: make_body_label("Artist"),
|
||||||
|
album: make_body_label("Album"),
|
||||||
|
track: make_body_label("Track"),
|
||||||
|
previous: Offset::new(prev, Vec2::ZERO),
|
||||||
|
play: Offset::new(play, Vec2::ZERO),
|
||||||
|
next: Offset::new(next, Vec2::ZERO),
|
||||||
|
position: make_footer_label("--:--"),
|
||||||
|
duration: make_footer_label("--:--"),
|
||||||
|
style,
|
||||||
|
art_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
|
body_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
|
footer_rect: Rect::from_xy_size(Vec2::ZERO, Vec2::ZERO),
|
||||||
|
position_secs: 0.0,
|
||||||
|
position_dirty: false,
|
||||||
|
status: PlaybackStatus::Paused,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_time(secs: f32) -> String {
|
||||||
|
let duration = secs.floor() as usize;
|
||||||
|
let seconds = duration % 60;
|
||||||
|
let minutes = (duration / 60) % 60;
|
||||||
|
let hours = (duration / 60) / 60;
|
||||||
|
|
||||||
|
if hours > 0 {
|
||||||
|
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||||
|
} else {
|
||||||
|
format!("{:02}:{:02}", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&mut self, new_size: Vec2) {
|
||||||
|
let style = &self.style;
|
||||||
|
let width = new_size.x;
|
||||||
|
let body_height = new_size.y - style.footer.height;
|
||||||
|
let body_height = body_height.max(0.0);
|
||||||
|
let body_size = Vec2::new(width, body_height);
|
||||||
|
let footer_size = Vec2::new(width, style.footer.height);
|
||||||
|
|
||||||
|
let art_size = body_height - style.art_margin * 2.0;
|
||||||
|
self.art_rect = Rect::from_xy_size(Vec2::splat(style.art_margin), Vec2::splat(art_size));
|
||||||
|
|
||||||
|
let label_x = (width + self.art_rect.br.x) / 2.0;
|
||||||
|
let artist_baseline = body_height * 0.25;
|
||||||
|
let album_baseline = body_height * 0.55;
|
||||||
|
let track_baseline = body_height * 0.85;
|
||||||
|
|
||||||
|
let button_y = body_height + style.footer.height / 2.0;
|
||||||
|
let previous_x = style.button_spacing * 0.5;
|
||||||
|
let play_x = style.button_spacing * 1.5;
|
||||||
|
let next_x = style.button_spacing * 2.5;
|
||||||
|
let position_x = style.button_spacing * 3.5;
|
||||||
|
let duration_x = width - style.button_spacing * 0.75;
|
||||||
|
|
||||||
|
self.artist.set_offset(Vec2::new(label_x, artist_baseline));
|
||||||
|
self.album.set_offset(Vec2::new(label_x, album_baseline));
|
||||||
|
self.track.set_offset(Vec2::new(label_x, track_baseline));
|
||||||
|
self.position.set_offset(Vec2::new(position_x, button_y));
|
||||||
|
self.duration.set_offset(Vec2::new(duration_x, button_y));
|
||||||
|
|
||||||
|
self.body_rect = Rect::from_xy_size(Vec2::ZERO, body_size);
|
||||||
|
self.footer_rect = Rect::from_xy_size(self.body_rect.bl(), footer_size);
|
||||||
|
|
||||||
|
self.previous.set_offset(Vec2::new(previous_x, button_y));
|
||||||
|
self.play.set_offset(Vec2::new(play_x, button_y));
|
||||||
|
self.next.set_offset(Vec2::new(next_x, button_y));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_album(&mut self, info: AlbumInfo) {
|
||||||
|
self.album.set_text(
|
||||||
|
info.title
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("<album here>"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_track(&mut self, info: TrackInfo) {
|
||||||
|
self.artist.set_text(
|
||||||
|
info.artists
|
||||||
|
.first()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("<artist here>"),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.track.set_text(
|
||||||
|
info.title
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("<album here>"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
self.status = status;
|
||||||
|
|
||||||
|
let icon = match status {
|
||||||
|
PlaybackStatus::Playing => "契",
|
||||||
|
PlaybackStatus::Paused => "",
|
||||||
|
PlaybackStatus::Stopped => "栗",
|
||||||
|
};
|
||||||
|
|
||||||
|
self.play.set_text(icon);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
|
|
|
@ -48,6 +48,12 @@ impl RoundButton {
|
||||||
icon,
|
icon,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, text: &str) {
|
||||||
|
if let Some(icon) = self.icon.as_mut() {
|
||||||
|
icon.set_text(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Button for RoundButton {
|
impl Button for RoundButton {
|
||||||
|
|
|
@ -104,7 +104,7 @@ impl Default for DialogFooterStyle {
|
||||||
icon_font: Font::new(crate::ICON_FONT),
|
icon_font: Font::new(crate::ICON_FONT),
|
||||||
button_radius: 7.5,
|
button_radius: 7.5,
|
||||||
color: Color::WHITE,
|
color: Color::WHITE,
|
||||||
height: 20.0,
|
height: 15.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,26 @@ impl Label {
|
||||||
offset: Vec2::ZERO,
|
offset: Vec2::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_centered(text: LabelText, scale: f32, color: Color) -> Self {
|
||||||
|
Self::new(
|
||||||
|
text,
|
||||||
|
HorizontalAlignment::Center,
|
||||||
|
scale,
|
||||||
|
color,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, text: &str) {
|
||||||
|
if self.text.text != text {
|
||||||
|
self.text.text = text.to_string();
|
||||||
|
self.layout = None;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Label {
|
impl Widget for Label {
|
||||||
|
@ -108,6 +128,14 @@ impl Icon {
|
||||||
offset: Vec2::ZERO,
|
offset: Vec2::ZERO,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_text(&mut self, text: &str) {
|
||||||
|
if self.text.text != text {
|
||||||
|
self.text.text = text.to_string();
|
||||||
|
self.layout = None;
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Icon {
|
impl Widget for Icon {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Copyright (c) 2022 Marceline Cramer
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
// SDPX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
//! This module defines backends for WebAssembly execution.
|
//! This module defines backends for WebAssembly execution.
|
||||||
//!
|
//!
|
||||||
|
@ -36,10 +36,11 @@ pub trait Instance {
|
||||||
/// Binds script data to a Canary panel.
|
/// Binds script data to a Canary panel.
|
||||||
///
|
///
|
||||||
/// To "bind" a Canary panel to a Canary script, this function must be
|
/// To "bind" a Canary panel to a Canary script, this function must be
|
||||||
/// called. It passes the ID of a panel to the script, plus an
|
/// called. It passes the ID of a panel to the script, the name of the
|
||||||
/// initialization message, and the script returns an integer as
|
/// protocol that this panel will be using, plus an initialization
|
||||||
/// userdata. All panel events will be identified to the script with this
|
/// message, and the script returns an integer as userdata. All panel
|
||||||
/// userdata as the first argument.
|
/// events will be identified to the script with this userdata as the first
|
||||||
|
/// argument.
|
||||||
///
|
///
|
||||||
/// The intended usecase for this userdata is to contain a pointer. A
|
/// The intended usecase for this userdata is to contain a pointer. A
|
||||||
/// Canary script can allocate some high-level object in memory, and when
|
/// Canary script can allocate some high-level object in memory, and when
|
||||||
|
@ -47,7 +48,7 @@ pub trait Instance {
|
||||||
/// userdata. Then, when the runtime calls back into the script, the
|
/// userdata. Then, when the runtime calls back into the script, the
|
||||||
/// userdata will be reinterpreted as a pointer and a method can be called
|
/// userdata will be reinterpreted as a pointer and a method can be called
|
||||||
/// on that object in memory.
|
/// on that object in memory.
|
||||||
fn bind_panel(&self, panel: PanelId, msg: Vec<u8>) -> u32;
|
fn bind_panel(&self, panel: PanelId, protocol: &str, msg: Vec<u8>) -> u32;
|
||||||
|
|
||||||
fn update(&self, panel_ud: u32, dt: f32);
|
fn update(&self, panel_ud: u32, dt: f32);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Copyright (c) 2022 Marceline Cramer
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
// SDPX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ impl Backend for WasmtimeBackend {
|
||||||
|
|
||||||
pub struct WasmtimeInstance {
|
pub struct WasmtimeInstance {
|
||||||
store: Mutex<Store>,
|
store: Mutex<Store>,
|
||||||
bind_panel: wasmtime::TypedFunc<(u32, u32), u32>,
|
bind_panel: wasmtime::TypedFunc<(u32, u32, u32), u32>,
|
||||||
update: wasmtime::TypedFunc<(u32, f32), ()>,
|
update: wasmtime::TypedFunc<(u32, f32), ()>,
|
||||||
draw: wasmtime::TypedFunc<u32, ()>,
|
draw: wasmtime::TypedFunc<u32, ()>,
|
||||||
on_resize: wasmtime::TypedFunc<(u32, f32, f32), ()>,
|
on_resize: wasmtime::TypedFunc<(u32, f32, f32), ()>,
|
||||||
|
@ -189,11 +189,13 @@ impl WasmtimeInstance {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Instance for WasmtimeInstance {
|
impl Instance for WasmtimeInstance {
|
||||||
fn bind_panel(&self, panel: PanelId, msg: Vec<u8>) -> u32 {
|
fn bind_panel(&self, panel: PanelId, protocol: &str, msg: Vec<u8>) -> u32 {
|
||||||
let mut store = self.store.lock();
|
let mut store = self.store.lock();
|
||||||
|
let protocol = store.data().message_new(protocol.as_bytes().to_vec());
|
||||||
let msg = store.data().message_new(msg);
|
let msg = store.data().message_new(msg);
|
||||||
let args = (panel.0 as u32, msg);
|
let args = (panel.0 as u32, protocol, msg);
|
||||||
let data = self.bind_panel.call(store.deref_mut(), args).unwrap();
|
let data = self.bind_panel.call(store.deref_mut(), args).unwrap();
|
||||||
|
store.data().message_free(protocol);
|
||||||
store.data().message_free(msg);
|
store.data().message_free(msg);
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Copyright (c) 2022 Marceline Cramer
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
// SDPX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
pub use canary_script::*;
|
pub use canary_script::*;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
@ -45,10 +45,10 @@ pub struct Script {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Script {
|
impl Script {
|
||||||
pub fn create_panel(&mut self, msg: Vec<u8>) -> anyhow::Result<Panel> {
|
pub fn create_panel(&mut self, protocol: &str, msg: Vec<u8>) -> anyhow::Result<Panel> {
|
||||||
let id = PanelId(self.next_panel);
|
let id = PanelId(self.next_panel);
|
||||||
self.next_panel += 1;
|
self.next_panel += 1;
|
||||||
let userdata = self.instance.bind_panel(id, msg);
|
let userdata = self.instance.bind_panel(id, protocol, msg);
|
||||||
Ok(Panel {
|
Ok(Panel {
|
||||||
instance: self.instance.clone(),
|
instance: self.instance.clone(),
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Copyright (c) 2022 Marceline Cramer
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
// SDPX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
use super::{AllsortsFont, Rect, Vec2};
|
use super::{AllsortsFont, Rect, Vec2};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Copyright (c) 2022 Marceline Cramer
|
// Copyright (c) 2022 Marceline Cramer
|
||||||
// SDPX-License-Identifier: LGPL-3.0-or-later
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
use super::{Color, DrawCommand, MeshIndex, MeshVertex, Rect, Vec2};
|
use super::{Color, DrawCommand, MeshIndex, MeshVertex, Rect, Vec2};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue