Compare commits

..

No commits in common. "784f3ff7f4808988ba101c78e2761509e66a2961" and "3c063af525ff0293bc29f8018be5d456b9eb6aad" have entirely different histories.

17 changed files with 47 additions and 275 deletions

1
Cargo.lock generated
View File

@ -1348,7 +1348,6 @@ dependencies = [
"gtk4", "gtk4",
"image", "image",
"kifu-core", "kifu-core",
"libadwaita",
"sgf", "sgf",
"tokio", "tokio",
] ]

View File

@ -1,34 +1,22 @@
use crate::{ use crate::{
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank}, types::{AppState, Config, DatabasePath, GameState, Player, Rank},
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView}, ui::{home, playing_field, HomeView, PlayingFieldView},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::sync::{Arc, RwLock};
path::PathBuf,
sync::{Arc, RwLock},
};
use typeshare::typeshare; use typeshare::typeshare;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare] #[typeshare]
#[serde(tag = "type", content = "content")] #[serde(tag = "type", content = "content")]
pub enum CoreRequest { pub enum CoreRequest {
ChangeSetting(ChangeSettingRequest),
CreateGame(CreateGameRequest), CreateGame(CreateGameRequest),
Home, Home,
OpenConfiguration,
PlayingField, PlayingField,
PlayStone(PlayStoneRequest), PlayStone(PlayStoneRequest),
StartGame, StartGame,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum ChangeSettingRequest {
LibraryPath(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare] #[typeshare]
pub struct PlayStoneRequest { pub struct PlayStoneRequest {
@ -69,15 +57,13 @@ impl From<HotseatPlayerRequest> for Player {
#[typeshare] #[typeshare]
#[serde(tag = "type", content = "content")] #[serde(tag = "type", content = "content")]
pub enum CoreResponse { pub enum CoreResponse {
ConfigurationView(ConfigurationView),
HomeView(HomeView), HomeView(HomeView),
PlayingFieldView(PlayingFieldView), PlayingFieldView(PlayingFieldView),
UpdatedConfigurationView(ConfigurationView),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CoreApp { pub struct CoreApp {
config: Arc<RwLock<Config>>, config: Config,
state: Arc<RwLock<AppState>>, state: Arc<RwLock<AppState>>,
} }
@ -88,23 +74,25 @@ impl CoreApp {
let db_path: DatabasePath = config.get().unwrap(); let db_path: DatabasePath = config.get().unwrap();
let state = Arc::new(RwLock::new(AppState::new(db_path))); let state = Arc::new(RwLock::new(AppState::new(db_path)));
Self { Self { config, state }
config: Arc::new(RwLock::new(config)),
state,
}
} }
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse { pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
match request { match request {
CoreRequest::ChangeSetting(request) => match request { /*
ChangeSettingRequest::LibraryPath(path) => { CoreRequest::LaunchScreen => {
let mut config = self.config.write().unwrap(); let app_state = self.state.read().unwrap();
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
path, At launch, I want to either show a list of games in progress, the current game, or the game creation screen.
)))); - if a live game is in progress, immmediately go to that game. Such a game will be classified at game creation, so it should be persisted to the state.
CoreResponse::UpdatedConfigurationView(configuration(&config)) - if no live games are in progress, but there are slow games in progress, show a list of the slow games and let the player choose which one to jump into.
} - if no games are in progress, show only the game creation screen
}, - game creation menu should be present both when there are only slow games and when there are no games
- the UI returned here will always be available in other places, such as when the user is viewing a game and wants to return to this page
For the initial version, I want only to show the game creation screen. Then I will backtrack record application state so that the only decisions can be made.
}
*/
CoreRequest::CreateGame(create_request) => { CoreRequest::CreateGame(create_request) => {
let mut app_state = self.state.write().unwrap(); let mut app_state = self.state.write().unwrap();
let white_player = { let white_player = {
@ -128,9 +116,6 @@ impl CoreApp {
CoreRequest::Home => { CoreRequest::Home => {
CoreResponse::HomeView(home(self.state.read().unwrap().database.all_games())) CoreResponse::HomeView(home(self.state.read().unwrap().database.all_games()))
} }
CoreRequest::OpenConfiguration => {
CoreResponse::ConfigurationView(configuration(&self.config.read().unwrap()))
}
CoreRequest::PlayingField => { CoreRequest::PlayingField => {
let app_state = self.state.read().unwrap(); let app_state = self.state.read().unwrap();
let game = app_state.game.as_ref().unwrap(); let game = app_state.game.as_ref().unwrap();

View File

@ -39,16 +39,11 @@ impl Database {
.unwrap() .unwrap()
.read_to_string(&mut buffer) .read_to_string(&mut buffer)
.unwrap(); .unwrap();
match parse_sgf(&buffer) { for sgf in parse_sgf(&buffer).unwrap() {
Ok(sgfs) => { match sgf {
for sgf in sgfs { Game::Go(game) => games.push(game),
match sgf { Game::Unsupported(_) => {}
Game::Go(game) => games.push(game),
Game::Unsupported(_) => {}
}
}
} }
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
} }
} }
} }

View File

@ -3,8 +3,7 @@ extern crate config_derive;
mod api; mod api;
pub use api::{ pub use api::{
ChangeSettingRequest, CoreApp, CoreRequest, CoreResponse, CreateGameRequest, CoreApp, CoreRequest, CoreResponse, CreateGameRequest, HotseatPlayerRequest, PlayerInfoRequest,
HotseatPlayerRequest, PlayerInfoRequest,
}; };
mod board; mod board;

View File

@ -16,7 +16,7 @@ define_config! {
} }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
pub struct DatabasePath(pub PathBuf); pub struct DatabasePath(PathBuf);
impl std::ops::Deref for DatabasePath { impl std::ops::Deref for DatabasePath {
type Target = PathBuf; type Target = PathBuf;

View File

@ -1,24 +0,0 @@
use crate::{
types::{Config, DatabasePath},
ui::Field,
};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
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

@ -0,0 +1,10 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
pub struct Action<A> {
pub id: String,
pub label: String,
pub action: A,
}

View File

@ -50,7 +50,6 @@ impl GamePreviewElement {
Some(GameResult::Draw) => "Draw".to_owned(), Some(GameResult::Draw) => "Draw".to_owned(),
Some(GameResult::Black(ref win)) => format!("Black by {}", format_win(win)), Some(GameResult::Black(ref win)) => format!("Black by {}", format_win(win)),
Some(GameResult::White(ref win)) => format!("White by {}", format_win(win)), Some(GameResult::White(ref win)) => format!("White by {}", format_win(win)),
Some(GameResult::Unknown(ref text)) => format!("Unknown: {}", text),
None => "".to_owned(), None => "".to_owned(),
}; };

View File

@ -1,31 +1,3 @@
use serde::{Deserialize, Serialize}; pub mod action;
use typeshare::typeshare;
pub mod game_preview; pub mod game_preview;
pub mod menu; pub mod menu;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
pub struct Action<A> {
pub id: String,
pub label: String,
pub action: A,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
pub struct Toggle<A> {
pub id: String,
pub label: String,
pub value: bool,
pub action: A,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[typeshare]
pub struct Field<A> {
pub id: String,
pub label: String,
pub value: Option<String>,
pub action: A,
}

View File

@ -1,8 +1,5 @@
mod configuration;
pub use configuration::{configuration, ConfigurationView};
mod elements; mod elements;
pub use elements::{game_preview::GamePreviewElement, menu::Menu, Action, Field, Toggle}; pub use elements::{action::Action, game_preview::GamePreviewElement, menu::Menu};
mod playing_field; mod playing_field;
pub use playing_field::{playing_field, PlayingFieldView}; pub use playing_field::{playing_field, PlayingFieldView};

View File

@ -9,7 +9,6 @@ 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.4", package = "libadwaita", features = [ "v1_2" ] }
cairo-rs = { version = "0.17" } cairo-rs = { version = "0.17" }
gio = { version = "0.17" } gio = { version = "0.17" }
glib = { version = "0.17" } glib = { version = "0.17" }

View File

@ -1,28 +1,20 @@
use adw::prelude::*; use gtk::prelude::*;
use kifu_core::{CoreApp, CoreRequest, CoreResponse}; use kifu_core::{CoreApp, CoreRequest, CoreResponse};
use kifu_gtk::{ use kifu_gtk::{
perftrace, perftrace,
ui::{ConfigurationPage, Home, Layout, PlayingField}, ui::{Home, PlayingField},
CoreApi, CoreApi,
}; };
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
fn handle_response(api: CoreApi, layout: Layout, message: CoreResponse) { fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreResponse) {
let playing_field = Arc::new(RwLock::new(None)); let playing_field = Arc::new(RwLock::new(None));
match message { match message {
CoreResponse::ConfigurationView(view) => perftrace("ConfigurationView", || {
let config_page = ConfigurationPage::new(api, view);
let window = adw::PreferencesWindow::new();
window.add(&config_page);
window.set_visible_page(&config_page);
window.present();
}),
CoreResponse::HomeView(view) => perftrace("HomeView", || { CoreResponse::HomeView(view) => perftrace("HomeView", || {
let api = api.clone(); let api = api.clone();
let home = Home::new(api, view); let home = Home::new(api, view);
layout.set_content(&home); window.set_child(Some(&home));
}), }),
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || { CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
let api = api.clone(); let api = api.clone();
@ -31,16 +23,13 @@ fn handle_response(api: CoreApi, layout: Layout, message: CoreResponse) {
if playing_field.is_none() { if playing_field.is_none() {
perftrace("creating a new playing field", || { perftrace("creating a new playing field", || {
let field = PlayingField::new(api, view); let field = PlayingField::new(api, view);
layout.set_content(&field); window.set_child(Some(&field));
*playing_field = Some(field); *playing_field = Some(field);
}) })
} else { } else {
playing_field.as_ref().map(|field| field.update_view(view)); playing_field.as_ref().map(|field| field.update_view(view));
} }
}), }),
CoreResponse::UpdatedConfigurationView(view) => perftrace("UpdatedConfiguration", || {
println!("updated configuration: {:?}", view);
}),
} }
} }
@ -76,9 +65,8 @@ fn main() {
} }
}); });
let app = adw::Application::builder() let app = gtk::Application::builder()
.application_id("com.luminescent-dreams.kifu-gtk") .application_id("com.luminescent-dreams.kifu-gtk")
.resource_base_path("/com/luminescent-dreams/kifu-gtk")
.build(); .build();
app.connect_activate({ app.connect_activate({
@ -93,31 +81,14 @@ fn main() {
core: core.clone(), core: core.clone(),
}; };
let action_config = gio::SimpleAction::new("show-config", None); let window = gtk::ApplicationWindow::new(app);
action_config.connect_activate({
let api = api.clone();
move |_, _| {
api.dispatch(CoreRequest::OpenConfiguration);
}
});
app.add_action(&action_config);
let window = adw::ApplicationWindow::builder()
.application(app)
.width_request(800)
.height_request(500)
.build();
let layout = Layout::new();
window.set_content(Some(&layout));
window.present(); window.present();
gtk_rx.attach(None, { gtk_rx.attach(None, {
let api = api.clone(); let api = api.clone();
move |message| { move |message| {
perftrace("handle_response", || { perftrace("handle_response", || {
handle_response(api.clone(), layout.clone(), message) handle_response(api.clone(), window.clone(), message)
}); });
Continue(true) Continue(true)
} }

View File

@ -1,52 +0,0 @@
use crate::CoreApi;
use adw::{prelude::*, subclass::prelude::*};
use glib::Object;
use kifu_core::{ui::ConfigurationView, ChangeSettingRequest, CoreRequest};
#[derive(Default)]
pub struct ConfigurationPagePrivate {}
#[glib::object_subclass]
impl ObjectSubclass for ConfigurationPagePrivate {
const NAME: &'static str = "Configuration";
type Type = ConfigurationPage;
type ParentType = adw::PreferencesPage;
}
impl ObjectImpl for ConfigurationPagePrivate {}
impl WidgetImpl for ConfigurationPagePrivate {}
impl PreferencesPageImpl for ConfigurationPagePrivate {}
glib::wrapper! {
pub struct ConfigurationPage(ObjectSubclass<ConfigurationPagePrivate>)
@extends adw::PreferencesPage, gtk::Widget,
@implements gtk::Orientable;
}
impl ConfigurationPage {
pub fn new(api: CoreApi, view: ConfigurationView) -> Self {
let s: Self = Object::builder().build();
let group = adw::PreferencesGroup::builder().build();
let library_entry = &adw::EntryRow::builder()
.name("library-path")
.title(view.library.label)
.show_apply_button(true)
.build();
if let Some(path) = view.library.value {
library_entry.set_text(&path);
}
library_entry.connect_apply(move |entry| {
api.dispatch(CoreRequest::ChangeSetting(
ChangeSettingRequest::LibraryPath(entry.text().into()),
));
});
group.add(library_entry);
s.add(&group);
s
}
}

View File

@ -31,7 +31,7 @@ impl GamePreview {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Horizontal); s.set_orientation(gtk::Orientation::Horizontal);
s.set_homogeneous(true); s.set_homogeneous(true);
s.set_hexpand(false); s.set_hexpand(true);
s.append(&s.imp().date); s.append(&s.imp().date);
s.append(&s.imp().title); s.append(&s.imp().title);

View File

@ -1,71 +0,0 @@
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use std::{cell::RefCell, rc::Rc};
// Deprecated even from day 1. We want to use ToolbarView as soon as it's available in the versions
// of Libadwaita available in NixOS.
pub struct LayoutPrivate {
pub header: adw::HeaderBar,
pub content: Rc<RefCell<gtk::Widget>>,
}
impl Default for LayoutPrivate {
fn default() -> Self {
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::StatusPage::builder().title("Nothing here").build();
Self {
header,
content: Rc::new(RefCell::new(content.into())),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for LayoutPrivate {
const NAME: &'static str = "Layout";
type Type = Layout;
type ParentType = gtk::Box;
}
impl ObjectImpl for LayoutPrivate {}
impl WidgetImpl for LayoutPrivate {}
impl BoxImpl for LayoutPrivate {}
glib::wrapper! {
pub struct Layout(ObjectSubclass<LayoutPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}
impl Layout {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Vertical);
s.set_homogeneous(false);
s.append(&s.imp().header);
s.append(&*s.imp().content.borrow());
s
}
pub fn set_content(&self, content: &impl IsA<gtk::Widget>) {
let mut widget = self.imp().content.borrow_mut();
self.remove(&*widget);
*widget = content.clone().upcast::<gtk::Widget>();
self.append(&*widget);
}
}

View File

@ -1,18 +1,12 @@
mod chat; mod chat;
pub use chat::Chat; pub use chat::Chat;
mod config;
pub use config::ConfigurationPage;
mod game_preview; mod game_preview;
pub use game_preview::GamePreview; pub use game_preview::GamePreview;
mod library; mod library;
pub use library::Library; pub use library::Library;
mod layout;
pub use layout::Layout;
mod player_card; mod player_card;
pub use player_card::PlayerCard; pub use player_card::PlayerCard;

View File

@ -242,7 +242,6 @@ pub enum GameResult {
Draw, Draw,
Black(Win), Black(Win),
White(Win), White(Win),
Unknown(String),
} }
impl TryFrom<&str> for GameResult { impl TryFrom<&str> for GameResult {
@ -257,7 +256,7 @@ impl TryFrom<&str> for GameResult {
let res = match parts[0].to_ascii_lowercase().as_str() { let res = match parts[0].to_ascii_lowercase().as_str() {
"b" => GameResult::Black, "b" => GameResult::Black,
"w" => GameResult::White, "w" => GameResult::White,
_ => return Ok(GameResult::Unknown(parts[0].to_owned())), _ => panic!("unknown result format"),
}; };
match parts[1].to_ascii_lowercase().as_str() { match parts[1].to_ascii_lowercase().as_str() {
"r" | "resign" => Ok(res(Win::Resignation)), "r" | "resign" => Ok(res(Win::Resignation)),