diff --git a/Cargo.lock b/Cargo.lock index 76e05d6..abd507e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -900,6 +900,24 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gm-control-panel" +version = "0.1.0" +dependencies = [ + "config", + "config-derive", + "futures", + "gdk4", + "gio", + "glib", + "glib-build-tools", + "gtk4", + "libadwaita", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "gobject-sys" version = "0.17.10" diff --git a/Cargo.toml b/Cargo.toml index 2d81067..fc88da2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "flow", "fluent-ergonomics", "geo-types", + "gm-control-panel", "hex-grid", "ifc", "kifu/core", diff --git a/build.sh b/build.sh index ebfc926..1d443b2 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -set -x RUST_ALL_TARGETS=( "changeset" @@ -14,6 +13,7 @@ RUST_ALL_TARGETS=( "flow" "fluent-ergonomics" "geo-types" + "gm-control-panel" "hex-grid" "ifc" "kifu-core" diff --git a/gm-control-panel/Cargo.toml b/gm-control-panel/Cargo.toml new file mode 100644 index 0000000..81029d6 --- /dev/null +++ b/gm-control-panel/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "gm-control-panel" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +adw = { version = "0.4", package = "libadwaita", features = [ "v1_2", "gtk_v4_6" ] } +config = { path = "../config" } +config-derive = { path = "../config-derive" } +futures = { version = "0.3" } +gio = { version = "0.17" } +glib = { version = "0.17" } +gdk = { version = "0.6", package = "gdk4" } +gtk = { version = "0.6", package = "gtk4", features = [ "v4_6" ] } +serde = { version = "1" } +serde_json = { version = "*" } +tokio = { version = "1", features = ["full"] } + +[build-dependencies] +glib-build-tools = "0.16" + diff --git a/gm-control-panel/build.rs b/gm-control-panel/build.rs new file mode 100644 index 0000000..c83cc1e --- /dev/null +++ b/gm-control-panel/build.rs @@ -0,0 +1,7 @@ +fn main() { + glib_build_tools::compile_resources( + "resources", + "resources/gresources.xml", + "com.luminescent-dreams.gm-control-panel.gresource", + ); +} diff --git a/gm-control-panel/resources/gresources.xml b/gm-control-panel/resources/gresources.xml new file mode 100644 index 0000000..1caf7fc --- /dev/null +++ b/gm-control-panel/resources/gresources.xml @@ -0,0 +1,6 @@ + + + + style.css + + diff --git a/gm-control-panel/resources/style.css b/gm-control-panel/resources/style.css new file mode 100644 index 0000000..32b6d6a --- /dev/null +++ b/gm-control-panel/resources/style.css @@ -0,0 +1,6 @@ +.playlist-card { + margin: 8px; + padding: 8px; + min-width: 100px; + min-height: 100px; +} diff --git a/gm-control-panel/src/app_window.rs b/gm-control-panel/src/app_window.rs new file mode 100644 index 0000000..26574f2 --- /dev/null +++ b/gm-control-panel/src/app_window.rs @@ -0,0 +1,64 @@ +use crate::PlaylistCard; +use adw::prelude::AdwApplicationWindowExt; +use gio::resources_lookup_data; +use gtk::{prelude::*, STYLE_PROVIDER_PRIORITY_USER}; +use std::iter::Iterator; + +#[derive(Clone)] +pub struct ApplicationWindow { + pub window: adw::ApplicationWindow, + pub layout: gtk::FlowBox, + pub playlists: Vec, +} + +impl ApplicationWindow { + pub fn new(app: &adw::Application) -> Self { + let window = adw::ApplicationWindow::builder() + .application(app) + .title("GM-control-panel") + .width_request(500) + .build(); + + let stylesheet = String::from_utf8( + resources_lookup_data( + "/com/luminescent-dreams/gm-control-panel/style.css", + gio::ResourceLookupFlags::NONE, + ) + .expect("stylesheet should just be available") + .to_vec(), + ) + .expect("to parse stylesheet"); + + let provider = gtk::CssProvider::new(); + provider.load_from_data(&stylesheet); + let context = window.style_context(); + context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER); + + let layout = gtk::FlowBox::new(); + + let playlists: Vec = vec![ + "Creepy Cathedral", + "Joyful Tavern", + "Exploring", + "Out on the streets", + "The North Abbey", + ] + .into_iter() + .map(|name| { + let playlist = PlaylistCard::new(); + playlist.set_name(name); + playlist + }) + .collect(); + + playlists.iter().for_each(|card| layout.append(card)); + + window.set_content(Some(&layout)); + + Self { + window, + layout, + playlists, + } + } +} diff --git a/gm-control-panel/src/config.rs b/gm-control-panel/src/config.rs new file mode 100644 index 0000000..e8ea768 --- /dev/null +++ b/gm-control-panel/src/config.rs @@ -0,0 +1,40 @@ +use config::define_config; +use config_derive::ConfigOption; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +define_config! { + Language(Language), + MusicPath(MusicPath), + PlaylistDatabasePath(PlaylistDatabasePath), +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)] +pub struct Language(String); + +impl std::ops::Deref for Language { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)] +pub struct MusicPath(PathBuf); + +impl std::ops::Deref for MusicPath { + type Target = PathBuf; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)] +pub struct PlaylistDatabasePath(PathBuf); + +impl std::ops::Deref for PlaylistDatabasePath { + type Target = PathBuf; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/gm-control-panel/src/main.rs b/gm-control-panel/src/main.rs new file mode 100644 index 0000000..da86db8 --- /dev/null +++ b/gm-control-panel/src/main.rs @@ -0,0 +1,59 @@ +use glib::{Continue, Sender}; +use gtk::prelude::*; +use std::{ + env, + sync::{Arc, RwLock}, +}; + +mod app_window; +use app_window::ApplicationWindow; + +mod config; + +mod playlist_card; +use playlist_card::PlaylistCard; + +mod types; +use types::PlaybackState; + +#[derive(Clone, Debug)] +pub enum Message {} + +#[derive(Clone)] +pub struct Core { + tx: Arc>>>, +} + +pub fn main() { + gio::resources_register_include!("com.luminescent-dreams.gm-control-panel.gresource") + .expect("Failed to register resource"); + + let app = adw::Application::builder() + .application_id("com.luminescent-dreams.gm-control-panel") + .build(); + + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + + let core = Core { + tx: Arc::new(RwLock::new(None)), + }; + + app.connect_activate(move |app| { + let (gtk_tx, gtk_rx) = + gtk::glib::MainContext::channel::(gtk::glib::PRIORITY_DEFAULT); + + *core.tx.write().unwrap() = Some(gtk_tx); + + let window = ApplicationWindow::new(app); + window.window.present(); + + gtk_rx.attach(None, move |_msg| Continue(true)); + }); + + let args: Vec = env::args().collect(); + ApplicationExtManual::run_with_args(&app, &args); + runtime.shutdown_background(); +} diff --git a/gm-control-panel/src/playlist_card.rs b/gm-control-panel/src/playlist_card.rs new file mode 100644 index 0000000..d1d0007 --- /dev/null +++ b/gm-control-panel/src/playlist_card.rs @@ -0,0 +1,54 @@ +use crate::PlaybackState; +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +pub struct PlaylistCardPrivate { + name: gtk::Label, + playing: gtk::Label, +} + +impl Default for PlaylistCardPrivate { + fn default() -> Self { + Self { + name: gtk::Label::new(None), + playing: gtk::Label::new(Some("Stopped")), + } + } +} + +#[glib::object_subclass] +impl ObjectSubclass for PlaylistCardPrivate { + const NAME: &'static str = "PlaylistCard"; + type Type = PlaylistCard; + type ParentType = gtk::Box; +} + +impl ObjectImpl for PlaylistCardPrivate {} +impl WidgetImpl for PlaylistCardPrivate {} +impl BoxImpl for PlaylistCardPrivate {} + +glib::wrapper! { + pub struct PlaylistCard(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl PlaylistCard { + pub fn new() -> Self { + let s: Self = Object::builder().build(); + s.set_orientation(gtk::Orientation::Vertical); + s.add_css_class("playlist-card"); + s.add_css_class("card"); + + s.append(&s.imp().name); + s.append(&s.imp().playing); + + s + } + + pub fn set_name(&self, s: &str) { + self.imp().name.set_text(s); + } + + pub fn set_playback(&self, s: PlaybackState) { + self.imp().playing.set_text(&format!("{}", s)) + } +} diff --git a/gm-control-panel/src/types.rs b/gm-control-panel/src/types.rs new file mode 100644 index 0000000..f0a6349 --- /dev/null +++ b/gm-control-panel/src/types.rs @@ -0,0 +1,16 @@ +use std::fmt; + +#[derive(Clone, Debug)] +pub enum PlaybackState { + Stopped, + Playing, +} + +impl fmt::Display for PlaybackState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Stopped => write!(f, "Stopped"), + Self::Playing => write!(f, "Playing"), + } + } +}