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",
"image",
"kifu-core",
"libadwaita",
"sgf",
"tokio",
]

View File

@ -1,34 +1,22 @@
use crate::{
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank},
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView},
types::{AppState, Config, DatabasePath, GameState, Player, Rank},
ui::{home, playing_field, HomeView, PlayingFieldView},
};
use serde::{Deserialize, Serialize};
use std::{
path::PathBuf,
sync::{Arc, RwLock},
};
use std::sync::{Arc, RwLock};
use typeshare::typeshare;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum CoreRequest {
ChangeSetting(ChangeSettingRequest),
CreateGame(CreateGameRequest),
Home,
OpenConfiguration,
PlayingField,
PlayStone(PlayStoneRequest),
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)]
#[typeshare]
pub struct PlayStoneRequest {
@ -69,15 +57,13 @@ impl From<HotseatPlayerRequest> for Player {
#[typeshare]
#[serde(tag = "type", content = "content")]
pub enum CoreResponse {
ConfigurationView(ConfigurationView),
HomeView(HomeView),
PlayingFieldView(PlayingFieldView),
UpdatedConfigurationView(ConfigurationView),
}
#[derive(Clone, Debug)]
pub struct CoreApp {
config: Arc<RwLock<Config>>,
config: Config,
state: Arc<RwLock<AppState>>,
}
@ -88,23 +74,25 @@ impl CoreApp {
let db_path: DatabasePath = config.get().unwrap();
let state = Arc::new(RwLock::new(AppState::new(db_path)));
Self {
config: Arc::new(RwLock::new(config)),
state,
}
Self { config, state }
}
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
match request {
CoreRequest::ChangeSetting(request) => match request {
ChangeSettingRequest::LibraryPath(path) => {
let mut config = self.config.write().unwrap();
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
path,
))));
CoreResponse::UpdatedConfigurationView(configuration(&config))
}
},
/*
CoreRequest::LaunchScreen => {
let app_state = self.state.read().unwrap();
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.
- 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) => {
let mut app_state = self.state.write().unwrap();
let white_player = {
@ -128,9 +116,6 @@ impl CoreApp {
CoreRequest::Home => {
CoreResponse::HomeView(home(self.state.read().unwrap().database.all_games()))
}
CoreRequest::OpenConfiguration => {
CoreResponse::ConfigurationView(configuration(&self.config.read().unwrap()))
}
CoreRequest::PlayingField => {
let app_state = self.state.read().unwrap();
let game = app_state.game.as_ref().unwrap();

View File

@ -39,16 +39,11 @@ impl Database {
.unwrap()
.read_to_string(&mut buffer)
.unwrap();
match parse_sgf(&buffer) {
Ok(sgfs) => {
for sgf in sgfs {
match sgf {
Game::Go(game) => games.push(game),
Game::Unsupported(_) => {}
}
}
for sgf in parse_sgf(&buffer).unwrap() {
match sgf {
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;
pub use api::{
ChangeSettingRequest, CoreApp, CoreRequest, CoreResponse, CreateGameRequest,
HotseatPlayerRequest, PlayerInfoRequest,
CoreApp, CoreRequest, CoreResponse, CreateGameRequest, HotseatPlayerRequest, PlayerInfoRequest,
};
mod board;

View File

@ -16,7 +16,7 @@ define_config! {
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
pub struct DatabasePath(pub PathBuf);
pub struct DatabasePath(PathBuf);
impl std::ops::Deref for DatabasePath {
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::Black(ref win)) => format!("Black 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(),
};

View File

@ -1,31 +1,3 @@
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
pub mod action;
pub mod game_preview;
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;
pub use elements::{game_preview::GamePreviewElement, menu::Menu, Action, Field, Toggle};
pub use elements::{action::Action, game_preview::GamePreviewElement, menu::Menu};
mod playing_field;
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
[dependencies]
adw = { version = "0.4", package = "libadwaita", features = [ "v1_2" ] }
cairo-rs = { version = "0.17" }
gio = { 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_gtk::{
perftrace,
ui::{ConfigurationPage, Home, Layout, PlayingField},
ui::{Home, PlayingField},
CoreApi,
};
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));
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", || {
let api = api.clone();
let home = Home::new(api, view);
layout.set_content(&home);
window.set_child(Some(&home));
}),
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
let api = api.clone();
@ -31,16 +23,13 @@ fn handle_response(api: CoreApi, layout: Layout, message: CoreResponse) {
if playing_field.is_none() {
perftrace("creating a new playing field", || {
let field = PlayingField::new(api, view);
layout.set_content(&field);
window.set_child(Some(&field));
*playing_field = Some(field);
})
} else {
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")
.resource_base_path("/com/luminescent-dreams/kifu-gtk")
.build();
app.connect_activate({
@ -93,31 +81,14 @@ fn main() {
core: core.clone(),
};
let action_config = gio::SimpleAction::new("show-config", None);
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));
let window = gtk::ApplicationWindow::new(app);
window.present();
gtk_rx.attach(None, {
let api = api.clone();
move |message| {
perftrace("handle_response", || {
handle_response(api.clone(), layout.clone(), message)
handle_response(api.clone(), window.clone(), message)
});
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();
s.set_orientation(gtk::Orientation::Horizontal);
s.set_homogeneous(true);
s.set_hexpand(false);
s.set_hexpand(true);
s.append(&s.imp().date);
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;
pub use chat::Chat;
mod config;
pub use config::ConfigurationPage;
mod game_preview;
pub use game_preview::GamePreview;
mod library;
pub use library::Library;
mod layout;
pub use layout::Layout;
mod player_card;
pub use player_card::PlayerCard;

View File

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