Set up the settings user interface #225

Merged
savanni merged 5 commits from kifu/main-menu into main 2024-03-21 21:11:03 +00:00
14 changed files with 327 additions and 145 deletions
Showing only changes of commit b98e0bdcea - Show all commits

View File

@ -1,13 +1,12 @@
use crate::{ use crate::{
database::Database, database::Database,
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank}, types::{AppState, Config, ConfigOption, LibraryPath, GameState, Player, Rank},
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView},
}; };
use async_std::channel::{Receiver, Sender}; use async_std::channel::{Receiver, Sender};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
path::PathBuf, path::PathBuf,
sync::{Arc, RwLock}, sync::{Arc, RwLock, RwLockReadGuard},
}; };
pub trait Observable<T> { pub trait Observable<T> {
@ -62,6 +61,7 @@ impl From<HotseatPlayerRequest> for Player {
} }
} }
/*
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum CoreResponse { pub enum CoreResponse {
ConfigurationView(ConfigurationView), ConfigurationView(ConfigurationView),
@ -69,6 +69,7 @@ pub enum CoreResponse {
PlayingFieldView(PlayingFieldView), PlayingFieldView(PlayingFieldView),
UpdatedConfigurationView(ConfigurationView), UpdatedConfigurationView(ConfigurationView),
} }
*/
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum CoreNotification { pub enum CoreNotification {
@ -79,7 +80,7 @@ pub enum CoreNotification {
pub struct Core { pub struct Core {
// config: Arc<RwLock<Config>>, // config: Arc<RwLock<Config>>,
// state: Arc<RwLock<AppState>>, // state: Arc<RwLock<AppState>>,
database: Arc<RwLock<Option<Database>>>, library: Arc<RwLock<Option<Database>>>,
subscribers: Arc<RwLock<Vec<Sender<CoreNotification>>>>, subscribers: Arc<RwLock<Vec<Sender<CoreNotification>>>>,
} }
@ -92,11 +93,15 @@ impl Core {
Self { Self {
// config: Arc::new(RwLock::new(config)), // config: Arc::new(RwLock::new(config)),
// state, // state,
database: Arc::new(RwLock::new(None)), library: Arc::new(RwLock::new(None)),
subscribers: Arc::new(RwLock::new(vec![])), subscribers: Arc::new(RwLock::new(vec![])),
} }
} }
pub fn library<'a>(&'a self) -> RwLockReadGuard<'_, Option<Database>> {
self.library.read().unwrap()
}
/* /*
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse { pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
match request { match request {

View File

@ -1,10 +1,7 @@
extern crate config_derive; extern crate config_derive;
mod api; mod api;
pub use api::{ pub use api::{Core, CoreNotification, Observable};
ChangeSettingRequest, Core, CoreNotification, CoreRequest, CoreResponse, CreateGameRequest,
HotseatPlayerRequest, Observable, PlayerInfoRequest,
};
mod board; mod board;
pub use board::*; pub use board::*;
@ -12,6 +9,5 @@ pub use board::*;
mod database; mod database;
mod types; 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;

View File

@ -10,21 +10,21 @@ use std::{path::PathBuf, time::Duration};
use thiserror::Error; use thiserror::Error;
define_config! { define_config! {
DatabasePath(DatabasePath), LibraryPath(LibraryPath),
Me(Me), Me(Me),
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)] #[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; type Target = PathBuf;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
} }
} }
impl From<String> for DatabasePath { impl From<String> for LibraryPath {
fn from(s: String) -> Self { fn from(s: String) -> Self {
Self(PathBuf::from(s)) Self(PathBuf::from(s))
} }
@ -78,7 +78,7 @@ pub struct AppState {
} }
impl AppState { impl AppState {
pub fn new(database_path: DatabasePath) -> Self { pub fn new(database_path: LibraryPath) -> Self {
Self { Self {
game: Some(GameState::default()), game: Some(GameState::default()),
database: Database::open_path(database_path.to_path_buf()).unwrap(), database: Database::open_path(database_path.to_path_buf()).unwrap(),

View File

@ -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<DatabasePath> = 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: (),
},
}
}

View File

@ -9,7 +9,7 @@ screenplay = []
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] } adw = { version = "0.5", package = "libadwaita", features = [ "v1_4" ] }
async-channel = { version = "2" } async-channel = { version = "2" }
async-std = { version = "1" } async-std = { version = "1" }
cairo-rs = { version = "0.18" } cairo-rs = { version = "0.18" }

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<schemalist> <schemalist>
<schema id="com.luminescent-dreams.kifu-gtk.dev" path="/com/luminescent-dreams/kifu-gtk/dev/"> <schema id="com.luminescent-dreams.kifu-gtk.dev" path="/com/luminescent-dreams/kifu-gtk/dev/">
<key name="database-path" type="s"> <key name="library-path" type="s">
<default>""</default> <default>""</default>
<summary>Path to the directory of games</summary> <summary>Path to the directory of games</summary>
</key> </key>
@ -10,4 +10,4 @@
<summary>Language override, use system settings if empty</summary> <summary>Language override, use system settings if empty</summary>
</key> </key>
</schema> </schema>
</schemalist> </schemalist>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<schemalist> <schemalist>
<schema id="com.luminescent-dreams.kifu-gtk" path="/com/luminescent-dreams/kifu-gtk/"> <schema id="com.luminescent-dreams.kifu-gtk" path="/com/luminescent-dreams/kifu-gtk/">
<key name="database-path" type="s"> <key name="library-path" type="s">
<default>""</default> <default>""</default>
<summary>Path to the directory of games</summary> <summary>Path to the directory of games</summary>
</key> </key>
@ -10,4 +10,4 @@
<summary>Language override, use system settings if empty</summary> <summary>Language override, use system settings if empty</summary>
</key> </key>
</schema> </schema>
</schemalist> </schemalist>

175
kifu/gtk/src/app_window.rs Normal file
View File

@ -0,0 +1,175 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
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 <https://www.gnu.org/licenses/>.
*/
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<AppView>,
// 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::<Database>() {
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(&gtk::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<AppView>) {
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(&nothing_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<gtk::Widget>) -> adw::ViewStack {
// self.content.set_child(Some(content));
// }
}

View File

@ -1,10 +1,29 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
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 <https://www.gnu.org/licenses/>.
*/
pub mod ui; pub mod ui;
mod app_window;
pub use app_window::AppWindow;
mod view_models; mod view_models;
mod views; mod views;
use async_std::task::yield_now; 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 std::{rc::Rc, sync::Arc};
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
@ -15,6 +34,7 @@ pub struct CoreApi {
} }
impl CoreApi { impl CoreApi {
/*
pub fn dispatch(&self, request: CoreRequest) { pub fn dispatch(&self, request: CoreRequest) {
/* /*
spawn({ spawn({
@ -26,6 +46,7 @@ impl CoreApi {
}); });
*/ */
} }
*/
} }
pub fn perftrace<F, A>(trace_name: &str, f: F) -> A pub fn perftrace<F, A>(trace_name: &str, f: F) -> A

View File

@ -1,8 +1,10 @@
use adw::prelude::*; use adw::prelude::*;
use kifu_core::{Config, ConfigOption, Core, CoreRequest, CoreResponse, DatabasePath}; use gio::ActionEntry;
use kifu_core::{Config, ConfigOption, Core};
use kifu_gtk::{ use kifu_gtk::{
perftrace, perftrace,
ui::{AppWindow, ConfigurationPage, Home, PlayingField}, // ui::{ConfigurationPage, Home, PlayingField},
AppWindow,
CoreApi, CoreApi,
}; };
use std::sync::{Arc, RwLock}; 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/"; const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/kifu-gtk/";
/*
fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) { fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) {
let playing_field = Arc::new(RwLock::new(None)); let playing_field = Arc::new(RwLock::new(None));
match message { 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() { fn main() {
gio::resources_register_include!("com.luminescent-dreams.kifu-gtk.gresource") gio::resources_register_include!("com.luminescent-dreams.kifu-gtk.gresource")
@ -59,10 +82,7 @@ fn main() {
APP_ID_PROD APP_ID_PROD
}; };
let settings = gio::Settings::new(app_id); let config = load_config(&app_id);
let db_path: String = settings.string("database-path").into();
let mut config = Config::new();
config.set(ConfigOption::DatabasePath(db_path.into()));
let runtime = Arc::new( let runtime = Arc::new(
tokio::runtime::Builder::new_multi_thread() tokio::runtime::Builder::new_multi_thread()
@ -101,16 +121,20 @@ fn main() {
.resource_base_path("/com/luminescent-dreams/kifu-gtk") .resource_base_path("/com/luminescent-dreams/kifu-gtk")
.build(); .build();
app.connect_activate({ app.connect_activate({
let runtime = runtime.clone(); let runtime = runtime.clone();
move |app| { move |app| {
let app_window = AppWindow::new(app); let app_window = AppWindow::new(app, core.clone());
setup_app_configuration_action(app);
let api = CoreApi { let api = CoreApi {
rt: runtime.clone(), rt: runtime.clone(),
core: core.clone(), core: core.clone(),
}; };
/*
let action_config = gio::SimpleAction::new("show-config", None); let action_config = gio::SimpleAction::new("show-config", None);
action_config.connect_activate({ action_config.connect_activate({
let api = api.clone(); let api = api.clone();
@ -119,6 +143,7 @@ fn main() {
} }
}); });
app.add_action(&action_config); app.add_action(&action_config);
*/
app_window.window.present(); app_window.window.present();
@ -134,7 +159,7 @@ fn main() {
}); });
*/ */
api.dispatch(CoreRequest::Home); // api.dispatch(CoreRequest::Home);
} }
}); });

View File

@ -1,100 +1,27 @@
use adw::prelude::*; // mod chat;
use gio::resources_lookup_data; // pub use chat::Chat;
use glib::IsA;
use gtk::STYLE_PROVIDER_PRIORITY_USER;
mod chat; // mod config;
pub use chat::Chat; // pub use config::ConfigurationPage;
mod config; // mod game_preview;
pub use config::ConfigurationPage; // pub use game_preview::GamePreview;
mod game_preview; // mod library;
pub use game_preview::GamePreview; // pub use library::Library;
mod library; // mod player_card;
pub use library::Library; // pub use player_card::PlayerCard;
mod player_card; // mod playing_field;
pub use player_card::PlayerCard; // pub use playing_field::PlayingField;
mod playing_field; // mod home;
pub use playing_field::PlayingField; // pub use home::Home;
mod home; // mod board;
pub use home::Home; // pub use board::Board;
mod board;
pub use board::Board;
#[cfg(feature = "screenplay")] #[cfg(feature = "screenplay")]
pub use playing_field::playing_field_view; 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(&gtk::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<gtk::Widget>) {
self.content.set_child(Some(content));
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. You should have received a copy of the GNU General Public License along with Kifu. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::LocalObserver; use crate::{views, LocalObserver, views::SettingsView};
use kifu_core::{Core, CoreNotification}; 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 { pub struct SettingsViewModel {
core: Core, 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<CoreNotification>, notification_observer: LocalObserver<CoreNotification>,
widget: gtk::Box, pub widget: views::SettingsView,
} }
impl SettingsViewModel { impl SettingsViewModel {
fn new(core: Core) -> Self { pub fn new(core: Core) -> Self {
let notification_observer = LocalObserver::new(&core, |msg| { let notification_observer = LocalObserver::new(&core, |msg| {
println!("SettingsViewModel called with message: {:?}", msg) println!("SettingsViewModel called with message: {:?}", msg)
}); });
@ -32,7 +39,7 @@ impl SettingsViewModel {
Self { Self {
core, core,
notification_observer, notification_observer,
widget: gtk::Box::new(gtk::Orientation::Horizontal, 0), widget: SettingsView::new(),
} }
} }
} }

View File

@ -0,0 +1,2 @@
mod settings;
pub use settings::SettingsView;

View File

@ -0,0 +1,46 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
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 <https://www.gnu.org/licenses/>.
*/
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<SettingsPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}
impl SettingsView {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s
}
}