From b98e0bdcead96cc02297671cf2edc7252af4c5c3 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 15 Mar 2024 10:49:25 -0400 Subject: [PATCH] Strip kifu down even further and revamp the app_window structure --- kifu/core/src/api.rs | 15 +- kifu/core/src/lib.rs | 8 +- kifu/core/src/types.rs | 10 +- kifu/core/src/ui/configuration.rs | 22 --- kifu/gtk/Cargo.toml | 2 +- ...om.luminescent-dreams.kifu.dev.gschema.xml | 4 +- .../com.luminescent-dreams.kifu.gschema.xml | 4 +- kifu/gtk/src/app_window.rs | 175 ++++++++++++++++++ kifu/gtk/src/lib.rs | 23 ++- kifu/gtk/src/main.rs | 41 +++- kifu/gtk/src/ui/mod.rs | 105 ++--------- .../src/view_models/settings_view_model.rs | 15 +- kifu/gtk/src/views/mod.rs | 2 + kifu/gtk/src/views/settings.rs | 46 +++++ 14 files changed, 327 insertions(+), 145 deletions(-) delete mode 100644 kifu/core/src/ui/configuration.rs create mode 100644 kifu/gtk/src/app_window.rs create mode 100644 kifu/gtk/src/views/settings.rs diff --git a/kifu/core/src/api.rs b/kifu/core/src/api.rs index e656707..101c6fc 100644 --- a/kifu/core/src/api.rs +++ b/kifu/core/src/api.rs @@ -1,13 +1,12 @@ use crate::{ database::Database, - types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank}, - ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView}, + types::{AppState, Config, ConfigOption, LibraryPath, GameState, Player, Rank}, }; use async_std::channel::{Receiver, Sender}; use serde::{Deserialize, Serialize}; use std::{ path::PathBuf, - sync::{Arc, RwLock}, + sync::{Arc, RwLock, RwLockReadGuard}, }; pub trait Observable { @@ -62,6 +61,7 @@ impl From for Player { } } +/* #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CoreResponse { ConfigurationView(ConfigurationView), @@ -69,6 +69,7 @@ pub enum CoreResponse { PlayingFieldView(PlayingFieldView), UpdatedConfigurationView(ConfigurationView), } +*/ #[derive(Clone, Debug, Serialize, Deserialize)] pub enum CoreNotification { @@ -79,7 +80,7 @@ pub enum CoreNotification { pub struct Core { // config: Arc>, // state: Arc>, - database: Arc>>, + library: Arc>>, subscribers: Arc>>>, } @@ -92,11 +93,15 @@ impl Core { Self { // config: Arc::new(RwLock::new(config)), // state, - database: Arc::new(RwLock::new(None)), + library: Arc::new(RwLock::new(None)), subscribers: Arc::new(RwLock::new(vec![])), } } + pub fn library<'a>(&'a self) -> RwLockReadGuard<'_, Option> { + self.library.read().unwrap() + } + /* pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse { match request { diff --git a/kifu/core/src/lib.rs b/kifu/core/src/lib.rs index 855fe82..8a7f40f 100644 --- a/kifu/core/src/lib.rs +++ b/kifu/core/src/lib.rs @@ -1,10 +1,7 @@ extern crate config_derive; mod api; -pub use api::{ - ChangeSettingRequest, Core, CoreNotification, CoreRequest, CoreResponse, CreateGameRequest, - HotseatPlayerRequest, Observable, PlayerInfoRequest, -}; +pub use api::{Core, CoreNotification, Observable}; mod board; pub use board::*; @@ -12,6 +9,5 @@ pub use board::*; mod database; mod types; -pub use types::{BoardError, Color, Config, ConfigOption, DatabasePath, Player, Rank, Size}; +pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size}; -pub mod ui; diff --git a/kifu/core/src/types.rs b/kifu/core/src/types.rs index a443b99..7f39a8b 100644 --- a/kifu/core/src/types.rs +++ b/kifu/core/src/types.rs @@ -10,21 +10,21 @@ use std::{path::PathBuf, time::Duration}; use thiserror::Error; define_config! { - DatabasePath(DatabasePath), + LibraryPath(LibraryPath), Me(Me), } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)] -pub struct DatabasePath(pub PathBuf); +pub struct LibraryPath(pub PathBuf); -impl std::ops::Deref for DatabasePath { +impl std::ops::Deref for LibraryPath { type Target = PathBuf; fn deref(&self) -> &Self::Target { &self.0 } } -impl From for DatabasePath { +impl From for LibraryPath { fn from(s: String) -> Self { Self(PathBuf::from(s)) } @@ -78,7 +78,7 @@ pub struct AppState { } impl AppState { - pub fn new(database_path: DatabasePath) -> Self { + pub fn new(database_path: LibraryPath) -> Self { Self { game: Some(GameState::default()), database: Database::open_path(database_path.to_path_buf()).unwrap(), diff --git a/kifu/core/src/ui/configuration.rs b/kifu/core/src/ui/configuration.rs deleted file mode 100644 index 4596374..0000000 --- a/kifu/core/src/ui/configuration.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::{ - types::{Config, DatabasePath}, - ui::Field, -}; -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ConfigurationView { - pub library: Field<()>, -} - -pub fn configuration(config: &Config) -> ConfigurationView { - let path: Option = config.get(); - ConfigurationView { - library: Field { - id: "library-path-field".to_owned(), - label: "Library".to_owned(), - value: path.map(|path| path.to_string_lossy().into_owned()), - action: (), - }, - } -} diff --git a/kifu/gtk/Cargo.toml b/kifu/gtk/Cargo.toml index 4167479..f3facad 100644 --- a/kifu/gtk/Cargo.toml +++ b/kifu/gtk/Cargo.toml @@ -9,7 +9,7 @@ screenplay = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] } +adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] } async-channel = { version = "2" } async-std = { version = "1" } cairo-rs = { version = "0.18" } diff --git a/kifu/gtk/resources/com.luminescent-dreams.kifu.dev.gschema.xml b/kifu/gtk/resources/com.luminescent-dreams.kifu.dev.gschema.xml index 9918738..bb93e16 100644 --- a/kifu/gtk/resources/com.luminescent-dreams.kifu.dev.gschema.xml +++ b/kifu/gtk/resources/com.luminescent-dreams.kifu.dev.gschema.xml @@ -1,7 +1,7 @@ - + "" Path to the directory of games @@ -10,4 +10,4 @@ Language override, use system settings if empty - \ No newline at end of file + diff --git a/kifu/gtk/resources/com.luminescent-dreams.kifu.gschema.xml b/kifu/gtk/resources/com.luminescent-dreams.kifu.gschema.xml index bed47e9..0569b5b 100644 --- a/kifu/gtk/resources/com.luminescent-dreams.kifu.gschema.xml +++ b/kifu/gtk/resources/com.luminescent-dreams.kifu.gschema.xml @@ -1,7 +1,7 @@ - + "" Path to the directory of games @@ -10,4 +10,4 @@ Language override, use system settings if empty - \ No newline at end of file + diff --git a/kifu/gtk/src/app_window.rs b/kifu/gtk/src/app_window.rs new file mode 100644 index 0000000..788e3ee --- /dev/null +++ b/kifu/gtk/src/app_window.rs @@ -0,0 +1,175 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of Kifu. + +Kifu is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +Kifu is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License along with Kifu. If not, see . +*/ + +use adw::prelude::*; +use gio::resources_lookup_data; +use glib::IsA; +use gtk::STYLE_PROVIDER_PRIORITY_USER; +use kifu_core::{Config, Core}; + +use crate::view_models::SettingsViewModel; + +enum AppView { + Settings(SettingsViewModel), + Home, +} + +// An application window should generally contain +// - an overlay widget +// - the main content in a stack on the bottom panel of the overlay +// - the settings and the about page in bins atop the overlay +pub struct AppWindow { + pub window: adw::ApplicationWindow, + header: adw::HeaderBar, + + // content is a stack which contains the view models for the application. These are the main + // elements that users want to interact with: the home page, the game library, a review, a game + // itself, perhaps also chat rooms and player lists on other networks. stack contains the + // widgets that need to be rendered. The two of these work together in order to ensure that + // we can maintain the state of previous views. Since the two of these work together, they are + // a candidate for extraction into a new widget or a new struct. + stack: adw::NavigationView, + content: Vec, + + // Overlays are for transient content, such as about and settings, which can be accessed from + // anywhere but shouldn't be part of the main application flow. + panel_overlay: gtk::Overlay, + core: Core, +} + +impl AppWindow { + pub fn new(app: &adw::Application, core: Core) -> Self { + /* + + let stylesheet = String::from_utf8( + resources_lookup_data( + "/com/luminescent-dreams/kifu-gtk/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 header = setup_header(); + + let current_view = match config.get::() { + Some(_) => AppView::Home, + None => AppView::Config(SettingsViewModel::new(core.clone())), + }; + + let content = adw::Bin::builder().css_classes(vec!["content"]).build(); + content.set_child(Some( + )); + + + window.set_content(Some(&layout)); + */ + + let window = Self::setup_window(app); + let header = Self::setup_header(); + let panel_overlay = Self::setup_panel_overlay(); + let (stack, content) = Self::setup_content(core.clone()); + + let layout = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .build(); + layout.append(&header); + layout.append(&panel_overlay); + panel_overlay.set_child(Some(&stack)); + + window.set_content(Some(&layout)); + + Self { + window, + header, + stack, + content, + panel_overlay, + core, + } + } + + fn setup_window(app: &adw::Application) -> adw::ApplicationWindow { + let window = adw::ApplicationWindow::builder() + .application(app) + .width_request(800) + .height_request(500) + .build(); + + window + } + + fn setup_header() -> adw::HeaderBar { + let header = adw::HeaderBar::builder() + .title_widget(>k::Label::new(Some("Kifu"))) + .build(); + + let app_menu = gio::Menu::new(); + let menu_item = gio::MenuItem::new(Some("Configuration"), Some("app.show_config")); + app_menu.append_item(&menu_item); + + let hamburger = gtk::MenuButton::builder() + .icon_name("open-menu-symbolic") + .build(); + hamburger.set_menu_model(Some(&app_menu)); + + header.pack_end(&hamburger); + header + } + + fn setup_panel_overlay() -> gtk::Overlay { + gtk::Overlay::new() + } + + fn setup_content(core: Core) -> (adw::NavigationView, Vec) { + let stack = adw::NavigationView::new(); + let mut content = Vec::new(); + + let nothing_page = adw::StatusPage::builder().title("Nothing here").build(); + let _ = stack.push( + &adw::NavigationPage::builder() + .can_pop(false) + .title("Kifu") + .child(¬hing_page) + .build(), + ); + content.push(AppView::Home); + + /* + match *core.library() { + Some(_) => { + } + None => { + let settings_vm = SettingsViewModel::new(core.clone()); + let _ = stack.push(&adw::NavigationPage::new(&settings_vm.widget, "Settings")); + content.push(AppView::Settings(settings_vm)); + } + } + */ + + (stack, content) + } + + // pub fn set_content(content: &impl IsA) -> adw::ViewStack { + // self.content.set_child(Some(content)); + // } +} diff --git a/kifu/gtk/src/lib.rs b/kifu/gtk/src/lib.rs index ef7508c..ca2ab98 100644 --- a/kifu/gtk/src/lib.rs +++ b/kifu/gtk/src/lib.rs @@ -1,10 +1,29 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of Kifu. + +Kifu is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +Kifu is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License along with Kifu. If not, see . +*/ + pub mod ui; +mod app_window; +pub use app_window::AppWindow; + mod view_models; mod views; use async_std::task::yield_now; -use kifu_core::{Core, CoreRequest, CoreResponse, Observable}; +use kifu_core::{Core, Observable}; use std::{rc::Rc, sync::Arc}; use tokio::runtime::Runtime; @@ -15,6 +34,7 @@ pub struct CoreApi { } impl CoreApi { + /* pub fn dispatch(&self, request: CoreRequest) { /* spawn({ @@ -26,6 +46,7 @@ impl CoreApi { }); */ } +*/ } pub fn perftrace(trace_name: &str, f: F) -> A diff --git a/kifu/gtk/src/main.rs b/kifu/gtk/src/main.rs index 4601569..f8c2e5c 100644 --- a/kifu/gtk/src/main.rs +++ b/kifu/gtk/src/main.rs @@ -1,8 +1,10 @@ use adw::prelude::*; -use kifu_core::{Config, ConfigOption, Core, CoreRequest, CoreResponse, DatabasePath}; +use gio::ActionEntry; +use kifu_core::{Config, ConfigOption, Core}; use kifu_gtk::{ perftrace, - ui::{AppWindow, ConfigurationPage, Home, PlayingField}, + // ui::{ConfigurationPage, Home, PlayingField}, + AppWindow, CoreApi, }; use std::sync::{Arc, RwLock}; @@ -12,6 +14,7 @@ const APP_ID_PROD: &str = "com.luminescent-dreams.kifu-gtk"; const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/kifu-gtk/"; +/* fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) { let playing_field = Arc::new(RwLock::new(None)); match message { @@ -48,6 +51,26 @@ fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) }), } } +*/ + +fn load_config(app_id: &str) -> Config { + let settings = gio::Settings::new(app_id); + let db_path: String = settings.string("database-path").into(); + let mut config = Config::new(); + config.set(ConfigOption::LibraryPath(db_path.into())); + config +} + +fn setup_app_configuration_action(app: &adw::Application) { + println!("setup_app_configuration_action"); + let action = ActionEntry::builder("show_config") + .activate(|_app: &adw::Application, _, _| { + println!("show configuration window"); + }) + .build(); + app.add_action_entries([action]); + println!("setup_app_configuration_action complete"); +} fn main() { gio::resources_register_include!("com.luminescent-dreams.kifu-gtk.gresource") @@ -59,10 +82,7 @@ fn main() { APP_ID_PROD }; - let settings = gio::Settings::new(app_id); - let db_path: String = settings.string("database-path").into(); - let mut config = Config::new(); - config.set(ConfigOption::DatabasePath(db_path.into())); + let config = load_config(&app_id); let runtime = Arc::new( tokio::runtime::Builder::new_multi_thread() @@ -101,16 +121,20 @@ fn main() { .resource_base_path("/com/luminescent-dreams/kifu-gtk") .build(); + app.connect_activate({ let runtime = runtime.clone(); move |app| { - let app_window = AppWindow::new(app); + let app_window = AppWindow::new(app, core.clone()); + + setup_app_configuration_action(app); let api = CoreApi { rt: runtime.clone(), core: core.clone(), }; + /* let action_config = gio::SimpleAction::new("show-config", None); action_config.connect_activate({ let api = api.clone(); @@ -119,6 +143,7 @@ fn main() { } }); app.add_action(&action_config); + */ app_window.window.present(); @@ -134,7 +159,7 @@ fn main() { }); */ - api.dispatch(CoreRequest::Home); + // api.dispatch(CoreRequest::Home); } }); diff --git a/kifu/gtk/src/ui/mod.rs b/kifu/gtk/src/ui/mod.rs index 00b5cbd..43718f0 100644 --- a/kifu/gtk/src/ui/mod.rs +++ b/kifu/gtk/src/ui/mod.rs @@ -1,100 +1,27 @@ -use adw::prelude::*; -use gio::resources_lookup_data; -use glib::IsA; -use gtk::STYLE_PROVIDER_PRIORITY_USER; +// mod chat; +// pub use chat::Chat; -mod chat; -pub use chat::Chat; +// mod config; +// pub use config::ConfigurationPage; -mod config; -pub use config::ConfigurationPage; +// mod game_preview; +// pub use game_preview::GamePreview; -mod game_preview; -pub use game_preview::GamePreview; +// mod library; +// pub use library::Library; -mod library; -pub use library::Library; +// mod player_card; +// pub use player_card::PlayerCard; -mod player_card; -pub use player_card::PlayerCard; +// mod playing_field; +// pub use playing_field::PlayingField; -mod playing_field; -pub use playing_field::PlayingField; +// mod home; +// pub use home::Home; -mod home; -pub use home::Home; - -mod board; -pub use board::Board; +// mod board; +// pub use board::Board; #[cfg(feature = "screenplay")] pub use playing_field::playing_field_view; -pub struct AppWindow { - pub window: adw::ApplicationWindow, - pub header: adw::HeaderBar, - pub content: adw::Bin, -} - -impl AppWindow { - pub fn new(app: &adw::Application) -> Self { - let window = adw::ApplicationWindow::builder() - .application(app) - .width_request(800) - .height_request(500) - .build(); - - let stylesheet = String::from_utf8( - resources_lookup_data( - "/com/luminescent-dreams/kifu-gtk/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 header = adw::HeaderBar::builder() - .title_widget(>k::Label::new(Some("Kifu"))) - .build(); - - let app_menu = gio::Menu::new(); - let menu_item = gio::MenuItem::new(Some("Configuration"), Some("app.show-config")); - app_menu.append_item(&menu_item); - - let hamburger = gtk::MenuButton::builder() - .icon_name("open-menu-symbolic") - .build(); - hamburger.set_menu_model(Some(&app_menu)); - - header.pack_end(&hamburger); - - let content = adw::Bin::builder().css_classes(vec!["content"]).build(); - content.set_child(Some( - &adw::StatusPage::builder().title("Nothing here").build(), - )); - - let layout = gtk::Box::builder() - .orientation(gtk::Orientation::Vertical) - .build(); - layout.append(&header); - layout.append(&content); - - window.set_content(Some(&layout)); - - Self { - window, - header, - content, - } - } - - pub fn set_content(&self, content: &impl IsA) { - self.content.set_child(Some(content)); - } -} diff --git a/kifu/gtk/src/view_models/settings_view_model.rs b/kifu/gtk/src/view_models/settings_view_model.rs index 2f13cd8..1e2bfcb 100644 --- a/kifu/gtk/src/view_models/settings_view_model.rs +++ b/kifu/gtk/src/view_models/settings_view_model.rs @@ -14,17 +14,24 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with Kifu. If not, see . */ -use crate::LocalObserver; +use crate::{views, LocalObserver, views::SettingsView}; use kifu_core::{Core, CoreNotification}; +/// SettingsViewModel +/// +/// Listens for messages from the core, and serves as intermediary between the Settings UI and the +/// core. Because it needs to respond to events from the core, it owns the widget, which allows it +/// to tell the widget to update after certain events. pub struct SettingsViewModel { core: Core, + // Technically, Settings doesn't care about any events from Core. We will keep this around for + // now as reference, until something which does care shows up. notification_observer: LocalObserver, - widget: gtk::Box, + pub widget: views::SettingsView, } impl SettingsViewModel { - fn new(core: Core) -> Self { + pub fn new(core: Core) -> Self { let notification_observer = LocalObserver::new(&core, |msg| { println!("SettingsViewModel called with message: {:?}", msg) }); @@ -32,7 +39,7 @@ impl SettingsViewModel { Self { core, notification_observer, - widget: gtk::Box::new(gtk::Orientation::Horizontal, 0), + widget: SettingsView::new(), } } } diff --git a/kifu/gtk/src/views/mod.rs b/kifu/gtk/src/views/mod.rs index e69de29..e95b387 100644 --- a/kifu/gtk/src/views/mod.rs +++ b/kifu/gtk/src/views/mod.rs @@ -0,0 +1,2 @@ +mod settings; +pub use settings::SettingsView; diff --git a/kifu/gtk/src/views/settings.rs b/kifu/gtk/src/views/settings.rs new file mode 100644 index 0000000..f80bccd --- /dev/null +++ b/kifu/gtk/src/views/settings.rs @@ -0,0 +1,46 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of Kifu. + +Kifu is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +Kifu is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License along with Kifu. If not, see . +*/ + +use glib::Object; +use gtk::{prelude::*, subclass::prelude::*}; + +pub struct SettingsPrivate {} + +#[glib::object_subclass] +impl ObjectSubclass for SettingsPrivate { + const NAME: &'static str = "Settings"; + type Type = SettingsView; + type ParentType = gtk::Box; + + fn new() -> Self { + Self {} + } +} + +impl ObjectImpl for SettingsPrivate {} +impl WidgetImpl for SettingsPrivate {} +impl BoxImpl for SettingsPrivate {} + +glib::wrapper! { + pub struct SettingsView(ObjectSubclass) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; +} + +impl SettingsView { + pub fn new() -> Self { + let s: Self = Object::builder().build(); + s + } +}