Compare commits

..

40 Commits

Author SHA1 Message Date
Savanni D'Gerinel b9425af234 Fix merge errors 2023-08-29 23:05:37 -04:00
Savanni D'Gerinel 4bd6388913 Tweak the configuration 2023-08-29 23:03:36 -04:00
Savanni D'Gerinel 8dad470440 Recreate a game from SGF 2023-08-29 23:03:09 -04:00
Savanni D'Gerinel e0fb7714d0 Cleanups 2023-08-29 23:03:09 -04:00
Savanni D'Gerinel b866249c9d Overhaul the sgf representation 2023-08-29 23:03:06 -04:00
Savanni D'Gerinel 2a6a5de5e1 Added the build scripts for kifu-gtk 2023-08-29 23:01:29 -04:00
Savanni D'Gerinel 1489121877 Get the width of the application back under control 2023-08-25 00:07:29 -04:00
Savanni D'Gerinel 562d4871a1 Create padding within the content view 2023-08-24 22:33:36 -04:00
Savanni D'Gerinel 16c8dcb682 Add a CSS stylesheet 2023-08-24 22:10:05 -04:00
Savanni D'Gerinel cc828c417a Change the layout/app_window to an ordinary object with necessary objects 2023-08-24 21:56:03 -04:00
Savanni D'Gerinel 784f3ff7f4 Be able to update the library path in the core 2023-08-24 20:52:27 -04:00
Savanni D'Gerinel 5439e2ac04 Set up a configuration UI 2023-08-24 20:24:41 -04:00
Savanni D'Gerinel 0bf6e079a2 Set up the configuration action 2023-08-23 17:51:51 -04:00
Savanni D'Gerinel 3998538e88 Set up the hamburger menu 2023-08-23 17:31:34 -04:00
Savanni D'Gerinel 793cd67218 Add a header bar and content field for applications 2023-08-23 15:57:09 -04:00
Savanni D'Gerinel ff13ff3c0e Update to a libadwaita app 2023-08-20 22:12:00 -04:00
Savanni D'Gerinel cc3ad372e6 Flip totally to a libadwaita program 2023-08-20 21:37:40 -04:00
Savanni D'Gerinel 3c063af525 Add the game result to the list of visible games 2023-08-20 13:17:54 -04:00
Savanni D'Gerinel aa64bf4c7e Remove the launch screen 2023-08-20 12:58:58 -04:00
Savanni D'Gerinel f75e0d4d65 Remove the library_view 2023-08-20 12:56:42 -04:00
Savanni D'Gerinel d8534a08eb Show the name of the game, and create one if it doesn't exist 2023-08-20 12:53:14 -04:00
Savanni D'Gerinel e5d0b7d20f Improve formatting. Rename GameDatabase to Library 2023-08-20 12:40:46 -04:00
Savanni D'Gerinel e9ffab1187 Construct a game preview component and render basic information into it 2023-08-20 12:31:44 -04:00
Savanni D'Gerinel a584fb4de3 Get the scrollbar to expand with the window 2023-08-19 23:45:19 -04:00
Savanni D'Gerinel e3f4ca246d Create the list of games 2023-08-19 23:24:01 -04:00
Savanni D'Gerinel 07b7351501 Flatten configuration by one level 2023-08-19 20:46:43 -04:00
Savanni D'Gerinel 70a295d4b1 Start combining the new game and library views 2023-08-19 20:09:50 -04:00
Savanni D'Gerinel 5478d388cb Set up a reflowing layout for the cards 2023-08-19 19:52:01 -04:00
Savanni D'Gerinel e203b17c8b Try to set up a title bar 2023-08-19 19:52:01 -04:00
Savanni D'Gerinel 69583dfd64 Create placeholder elements for each playlist card 2023-08-19 19:52:01 -04:00
Savanni D'Gerinel d59c2585db Set up configuration 2023-08-19 19:52:01 -04:00
Savanni D'Gerinel a6fcbfac71 Make the main app window appear, start working on config 2023-08-19 19:52:01 -04:00
Savanni D'Gerinel 4f940099da Added the build scripts for kifu-gtk 2023-08-19 19:49:17 -04:00
Savanni D'Gerinel f4735dd16b Add some imports that the configuration library needs 2023-08-17 23:28:49 -04:00
Savanni D'Gerinel b662ac519a Start on a configuration library 2023-08-17 23:28:49 -04:00
Savanni D'Gerinel efec8dfe5a Convert the kifu config to the config crate 2023-08-17 23:28:49 -04:00
Savanni D'Gerinel 0765d94a5e Create a type-safe configuration library 2023-08-17 23:28:49 -04:00
Savanni D'Gerinel 40b33797f3 Start on a configuration library 2023-08-17 23:28:49 -04:00
Savanni D'Gerinel 24e88da8e2 Add a desktop file and a bundler 2023-08-17 20:54:05 -04:00
Savanni D'Gerinel 456d872b40 Fix end of month handling 2023-08-12 21:35:14 -04:00
46 changed files with 1520 additions and 664 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ target
node_modules
dist
result
*.tgz

531
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,8 @@
[workspace]
members = [
"changeset",
"config",
"config-derive",
"coordinates",
"cyberpunk-splash",
"dashboard",
@ -8,6 +10,7 @@ members = [
"flow",
"fluent-ergonomics",
"geo-types",
"gm-control-panel",
"hex-grid",
"ifc",
"kifu/core",

View File

@ -4,6 +4,8 @@ set -euo pipefail
RUST_ALL_TARGETS=(
"changeset"
"config"
"config-derive"
"coordinates"
"cyberpunk-splash"
"dashboard"
@ -11,6 +13,7 @@ RUST_ALL_TARGETS=(
"flow"
"fluent-ergonomics"
"geo-types"
"gm-control-panel"
"hex-grid"
"ifc"
"kifu-core"
@ -29,6 +32,16 @@ build_rust_targets() {
done
}
build_dist() {
local TARGETS=${@/$CMD}
for target in $TARGETS; do
if [ -f $target/dist.sh ]; then
cd $target && ./dist.sh
fi
done
}
export CARGO=`which cargo`
if [ -z "${TARGET-}" ]; then
@ -45,7 +58,9 @@ if [ "${CMD}" == "clean" ]; then
fi
for cmd in $CMD; do
if [ "${TARGET}" == "all" ]; then
if [ "${CMD}" == "dist" ]; then
build_dist $TARGET
elif [ "${TARGET}" == "all" ]; then
build_rust_targets $cmd ${RUST_ALL_TARGETS[*]}
else
build_rust_targets $cmd $TARGET

14
config-derive/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "config-derive"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
proc-macro = true
[dependencies]
quote = { version = "1" }
syn = { version = "1", features = [ "extra-traits" ] }

23
config-derive/src/lib.rs Normal file
View File

@ -0,0 +1,23 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(ConfigOption)]
pub fn derive(input: TokenStream) -> TokenStream {
let DeriveInput { ident, .. } = parse_macro_input!(input as DeriveInput);
let result = quote! {
impl From<&Config> for Option<#ident> {
fn from(config: &Config) -> Self {
match config.values.get(&ConfigName::#ident) {
Some(ConfigOption::#ident(val)) => Some(val.clone()),
_ => None,
}
}
}
};
result.into()
}

16
config/Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "config"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
config-derive = { path = "../config-derive" }
serde_json = { version = "1" }
serde = { version = "1", features = [ "derive" ] }
thiserror = { version = "1" }
[dev-dependencies]
cool_asserts = { version = "2" }

160
config/src/lib.rs Normal file
View File

@ -0,0 +1,160 @@
/*
use std::{
collections::HashMap,
fs::File,
hash::Hash,
io::{ErrorKind, Read},
path::PathBuf,
};
*/
pub use config_derive::ConfigOption;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigReadError {
#[error("Cannot read the configuration file: {0}")]
CannotRead(std::io::Error),
#[error("Cannot open the configuration file for reading: {0}")]
CannotOpen(std::io::Error),
#[error("Invalid json data found in the configurationfile: {0}")]
InvalidJSON(serde_json::Error),
}
#[macro_export]
macro_rules! define_config {
($($name:ident($struct:ident),)+) => (
#[derive(Clone, Debug, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum ConfigName {
$($name),+
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum ConfigOption {
$($name($struct)),+
}
#[derive(Clone, Debug)]
pub struct Config {
values: std::collections::HashMap<ConfigName, ConfigOption>,
}
impl Config {
pub fn new() -> Self {
Self {
values: std::collections::HashMap::new(),
}
}
pub fn from_path(config_path: std::path::PathBuf) -> Result<Self, $crate::ConfigReadError> {
let mut settings = config_path.clone();
settings.push("config");
match std::fs::File::open(settings) {
Ok(mut file) => {
let mut buf = String::new();
std::io::Read::read_to_string(&mut file, &mut buf)
.map_err(|err| $crate::ConfigReadError::CannotRead(err))?;
let values = serde_json::from_str(buf.as_ref())
.map_err(|err| $crate::ConfigReadError::InvalidJSON(err))?;
Ok(Self {
values,
})
}
Err(io_err) => {
match io_err.kind() {
std::io::ErrorKind::NotFound => {
/* create the path and an empty file */
Ok(Self {
values: std::collections::HashMap::new(),
})
}
_ => Err($crate::ConfigReadError::CannotOpen(io_err)),
}
}
}
}
pub fn set(&mut self, val: ConfigOption) {
let _ = match val {
$(ConfigOption::$struct(_) => self.values.insert(ConfigName::$name, val)),+
};
}
pub fn get<'a, T>(&'a self) -> Option<T>
where
Option<T>: From<&'a Self>,
{
self.into()
}
}
)
}
#[cfg(test)]
mod test {
use super::*;
use cool_asserts::assert_matches;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
define_config! {
DatabasePath(DatabasePath),
Me(Me),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)]
pub struct DatabasePath(PathBuf);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
enum Rank {
Kyu(i8),
Dan(i8),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, ConfigOption)]
pub struct Me {
name: String,
rank: Option<Rank>,
}
#[test]
fn it_can_set_and_get_options() {
let mut config: Config = Config::new();
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
"./fixtures/five_games",
))));
assert_eq!(
Some(DatabasePath(PathBuf::from("./fixtures/five_games"))),
config.get()
);
}
#[test]
fn it_can_serialize_and_deserialize() {
let mut config = Config::new();
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
"fixtures/five_games",
))));
config.set(ConfigOption::Me(Me {
name: "Savanni".to_owned(),
rank: Some(Rank::Kyu(10)),
}));
let s = serde_json::to_string(&config.values).unwrap();
println!("{}", s);
let values: HashMap<ConfigName, ConfigOption> = serde_json::from_str(s.as_ref()).unwrap();
println!("options: {:?}", values);
assert_matches!(values.get(&ConfigName::DatabasePath),
Some(ConfigOption::DatabasePath(ref db_path)) =>
assert_eq!(Some(db_path.clone()), config.get())
);
assert_matches!(values.get(&ConfigName::Me), Some(ConfigOption::Me(val)) =>
assert_eq!(Some(val.clone()), config.get())
);
}
}

View File

@ -0,0 +1,6 @@
[Desktop Entry]
Type=Application
Version=1.0
Name=dashboard
Comment=My personal system dashboard
Exec=dashboard

11
dashboard/dist.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
set -x
mkdir -p dist
cp dashboard.desktop dist
cp ../target/release/dashboard dist
strip dist/dashboard
tar -cf dashboard.tgz dist/

View File

@ -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"

View File

@ -0,0 +1,7 @@
fn main() {
glib_build_tools::compile_resources(
"resources",
"resources/gresources.xml",
"com.luminescent-dreams.gm-control-panel.gresource",
);
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/com/luminescent-dreams/gm-control-panel/">
<file>style.css</file>
</gresource>
</gresources>

View File

@ -0,0 +1,6 @@
.playlist-card {
margin: 8px;
padding: 8px;
min-width: 100px;
min-height: 100px;
}

View File

@ -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<PlaylistCard>,
}
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<PlaylistCard> = 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,
}
}
}

View File

@ -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
}
}

View File

@ -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<RwLock<Option<Sender<Message>>>>,
}
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::<Message>(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<String> = env::args().collect();
ApplicationExtManual::run_with_args(&app, &args);
runtime.shutdown_background();
}

View File

@ -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<PlaylistCardPrivate>) @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))
}
}

View File

@ -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"),
}
}
}

View File

@ -250,8 +250,12 @@ impl From<chrono::NaiveDate> for IFC {
{
days = days - 1;
}
let month: u8 = (days / 28).try_into().unwrap();
let day: u8 = (days % 28).try_into().unwrap();
let mut month: u8 = (days / 28).try_into().unwrap();
let mut day: u8 = (days % 28).try_into().unwrap();
if day == 0 {
month = month - 1;
day = 28;
}
Self::Day(Day {
year: date.year(),
month: month + 1,
@ -268,167 +272,6 @@ impl From<IFC> for chrono::NaiveDate {
}
}
/*
impl IFC {
pub fn year_day(year: u32) -> Self {
Self {
year,
ordinal: if is_leap_year(year) { 366 } else { 365 }
}
}
}
*/
/*
impl From<chrono::Date<chrono::Utc>> for IFC {
fn from(d: chrono::Date<chrono::Utc>) -> IFC {
IFC::from(d.naive_utc())
}
}
impl From<chrono::Date<chrono_tz::Tz>> for IFC {
fn from(d: chrono::Date<chrono_tz::Tz>) -> IFC {
IFC::from(d.naive_utc())
}
}
impl From<chrono::NaiveDate> for IFC {
fn from(d: NaiveDate) -> IFC {
//println!("d: {} [{}]", d.format("%Y-%m-%d"), d.ordinal());
IFC {
year: (d.year() + 10000) as u32,
ordinal: d.ordinal0(),
leap_year: is_leap_year(d.year()),
}
}
}
*/
/*
impl Datelike for IFC {
fn year(&self) -> i32 {
// self.year as i32
unimplemented!()
}
fn month(&self) -> u32 {
// self.month0() + 1
unimplemented!()
}
fn month0(&self) -> u32 {
/*
if self.leap_year && self.ordinal == 365 {
12
} else if self.leap_year && self.ordinal == 168 {
5
} else {
self.ordinal / 28
}
*/
unimplemented!()
}
fn day(&self) -> u32 {
// self.day0() + 1
unimplemented!()
}
fn day0(&self) -> u32 {
/*
if self.leap_year {
if self.ordinal == 365 {
28
} else if self.ordinal == 168 {
28
} else if self.ordinal > 168 {
(self.ordinal - 1).rem_euclid(28) as u32
} else {
self.ordinal.rem_euclid(28) as u32
}
} else {
if self.ordinal == 364 {
28
} else {
self.ordinal.rem_euclid(28) as u32
}
}
*/
unimplemented!()
}
fn ordinal(&self) -> u32 {
// self.ordinal + 1
unimplemented!()
}
fn ordinal0(&self) -> u32 {
// self.ordinal
unimplemented!()
}
fn weekday(&self) -> chrono::Weekday {
if self.day0() == 28 {
chrono::Weekday::Sat
} else {
match self.day0().rem_euclid(7) {
0 => chrono::Weekday::Sun,
1 => chrono::Weekday::Mon,
2 => chrono::Weekday::Tue,
3 => chrono::Weekday::Wed,
4 => chrono::Weekday::Thu,
5 => chrono::Weekday::Fri,
6 => chrono::Weekday::Sat,
_ => panic!("rem_euclid should not return anything outside the 0..6 range"),
}
}
}
fn iso_week(&self) -> chrono::IsoWeek {
panic!("iso_week is not implemented because chrono does not expose any constructors for IsoWeek!");
}
fn with_year(&self, year: i32) -> Option<IFC> {
/*
Some(IFC {
year: (year as u32) + 10000,
ordinal: self.ordinal,
leap_year: is_leap_year(year),
})
*/
unimplemented!()
}
fn with_month(&self, month: u32) -> Option<IFC> {
// Some(IFC::ymd(self.year, month as u8, self.day() as u8))
unimplemented!()
}
fn with_month0(&self, month: u32) -> Option<IFC> {
// Some(IFC::ymd(self.year, month as u8 + 1, self.day() as u8))
unimplemented!()
}
fn with_day(&self, day: u32) -> Option<IFC> {
// Some(IFC::ymd(self.year, self.month() as u8, day as u8))
unimplemented!()
}
fn with_day0(&self, day: u32) -> Option<IFC> {
// Some(IFC::ymd(self.year, self.month() as u8, day as u8 + 1))
unimplemented!()
}
fn with_ordinal(&self, ordinal: u32) -> Option<IFC> {
/*
Some(IFC {
year: self.year,
ordinal,
leap_year: self.leap_year,
})
*/
unimplemented!()
}
fn with_ordinal0(&self, ordinal: u32) -> Option<IFC> {
/*
Some(IFC {
year: self.year,
ordinal: ordinal + 1,
leap_year: self.leap_year,
})
*/
unimplemented!()
}
}
*/
#[cfg(test)]
mod tests {
use super::*;
@ -612,10 +455,34 @@ mod tests {
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 1).unwrap()),
IFC::ymd(12022, 1, 1).unwrap()
);
assert_eq!(
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 2).unwrap()),
IFC::ymd(12022, 1, 2).unwrap()
);
assert_eq!(
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 3).unwrap()),
IFC::ymd(12022, 1, 3).unwrap()
);
assert_eq!(
IFC::from(chrono::NaiveDate::from_ymd_opt(2023, 01, 26).unwrap()),
IFC::ymd(2023, 1, 26).unwrap()
);
assert_eq!(
IFC::from(chrono::NaiveDate::from_ymd_opt(2023, 01, 27).unwrap()),
IFC::ymd(2023, 1, 27).unwrap()
);
assert_eq!(
IFC::from(chrono::NaiveDate::from_ymd_opt(2023, 01, 28).unwrap()),
IFC::ymd(2023, 1, 28).unwrap()
);
assert_eq!(
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 29).unwrap()),
IFC::ymd(12022, 2, 1).unwrap()
);
assert_eq!(
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 30).unwrap()),
IFC::ymd(12022, 2, 2).unwrap()
);
assert_eq!(
IFC::from(NaiveDate::from_ymd_opt(12022, 2, 26).unwrap()),
IFC::ymd(12022, 3, 1).unwrap()
@ -644,6 +511,10 @@ mod tests {
IFC::from(NaiveDate::from_ymd_opt(12022, 8, 13).unwrap()),
IFC::ymd(12022, 9, 1).unwrap()
);
assert_eq!(
IFC::from(chrono::NaiveDate::from_ymd_opt(2023, 08, 12).unwrap()),
IFC::ymd(2023, 8, 28).unwrap()
);
assert_eq!(
IFC::from(NaiveDate::from_ymd_opt(12022, 9, 10).unwrap()),
IFC::ymd(12022, 10, 1).unwrap()

View File

@ -7,6 +7,8 @@ edition = "2021"
[dependencies]
chrono = { version = "0.4" }
config = { path = "../../config" }
config-derive = { path = "../../config-derive" }
sgf = { path = "../../sgf" }
grid = { version = "0.9" }
serde_json = { version = "1" }

View File

@ -1,23 +1,34 @@
use crate::{
types::{AppState, GameState, Player, Rank},
ui::{home, playing_field, HomeView, PlayingFieldView},
Config, DatabasePath,
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank},
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView},
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, RwLock};
use std::{
path::PathBuf,
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 {
@ -58,46 +69,42 @@ 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: Config,
config: Arc<RwLock<Config>>,
state: Arc<RwLock<AppState>>,
}
impl CoreApp {
pub fn new(config_path: std::path::PathBuf) -> Self {
println!("config_path: {:?}", config_path);
let config = Config::from_path(config_path).expect("configuration to open");
let db_path: DatabasePath = config.get();
let db_path: DatabasePath = config.get().unwrap();
let state = Arc::new(RwLock::new(AppState::new(db_path)));
println!("config: {:?}", config);
println!("games database: {:?}", state.read().unwrap().database.len());
Self { config, state }
Self {
config: Arc::new(RwLock::new(config)),
state,
}
}
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
match request {
/*
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::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::CreateGame(create_request) => {
let mut app_state = self.state.write().unwrap();
let white_player = {
@ -121,6 +128,9 @@ 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

@ -1,198 +0,0 @@
use crate::types::Player;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::File,
io::{ErrorKind, Read},
path::PathBuf,
};
use thiserror::Error;
/*
pub trait ConfigOption {
type Value;
}
pub struct DatabasePath(PathBuf);
impl ConfigOption for DatabasePath {
type Value = PathBuf;
}
impl ConfigOption for Player {
type Value = Player;
}
pub trait Config {
// fn set_option(option: ConfigOption);
fn get_option<N, C: ConfigOption>(name: Name) -> C<Name = Name>
}
*/
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
enum OptionNames {
DatabasePath,
Me,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ConfigOption {
DatabasePath(DatabasePath),
Me(Me),
}
#[derive(Debug, Error)]
pub enum ConfigReadError {
#[error("Cannot read the configuration file: {0}")]
CannotRead(std::io::Error),
#[error("Cannot open the configuration file for reading: {0}")]
CannotOpen(std::io::Error),
#[error("Invalid json data found in the configurationfile: {0}")]
InvalidJSON(serde_json::Error),
}
#[derive(Clone, Debug)]
pub struct Config {
config_path: PathBuf,
values: HashMap<OptionNames, ConfigOption>,
}
impl Config {
pub fn new(config_path: PathBuf) -> Self {
Self {
config_path,
values: HashMap::new(),
}
}
pub fn from_path(config_path: PathBuf) -> Result<Self, ConfigReadError> {
let mut settings = config_path.clone();
settings.push("config");
match File::open(settings) {
Ok(mut file) => {
let mut buf = String::new();
file.read_to_string(&mut buf)
.map_err(|err| ConfigReadError::CannotRead(err))?;
let values = serde_json::from_str(buf.as_ref())
.map_err(|err| ConfigReadError::InvalidJSON(err))?;
Ok(Self {
config_path,
values,
})
}
Err(io_err) => {
match io_err.kind() {
ErrorKind::NotFound => {
/* create the path and an empty file */
Ok(Self {
config_path,
values: HashMap::new(),
})
}
_ => Err(ConfigReadError::CannotOpen(io_err)),
}
}
}
}
pub fn set(&mut self, val: ConfigOption) {
let _ = match val {
ConfigOption::DatabasePath(_) => self.values.insert(OptionNames::DatabasePath, val),
ConfigOption::Me(_) => self.values.insert(OptionNames::Me, val),
};
}
pub fn get<'a, T>(&'a self) -> T
where
T: From<&'a Self>,
{
self.into()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct DatabasePath(PathBuf);
impl std::ops::Deref for DatabasePath {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<&Config> for DatabasePath {
fn from(config: &Config) -> Self {
match config.values.get(&OptionNames::DatabasePath) {
Some(ConfigOption::DatabasePath(path)) => path.clone(),
_ => DatabasePath(config.config_path.clone()),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Me(Player);
impl From<&Config> for Option<Me> {
fn from(config: &Config) -> Self {
config
.values
.get(&OptionNames::Me)
.and_then(|val| match val {
ConfigOption::Me(me) => Some(me.clone()),
_ => None,
})
}
}
impl std::ops::Deref for Me {
type Target = Player;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::types::Rank;
use cool_asserts::assert_matches;
#[test]
fn it_can_set_and_get_options() {
let mut config = Config::new(PathBuf::from("."));
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
"fixtures/five_games",
))));
config.set(ConfigOption::Me(Me(Player {
name: "Savanni".to_owned(),
rank: Some(Rank::Kyu(10)),
})));
}
#[test]
fn it_can_serialize_and_deserialize() {
let mut config = Config::new(PathBuf::from("."));
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
"fixtures/five_games",
))));
config.set(ConfigOption::Me(Me(Player {
name: "Savanni".to_owned(),
rank: Some(Rank::Kyu(10)),
})));
let s = serde_json::to_string(&config.values).unwrap();
println!("{}", s);
let values: HashMap<OptionNames, ConfigOption> = serde_json::from_str(s.as_ref()).unwrap();
println!("options: {:?}", values);
assert_matches!(values.get(&OptionNames::DatabasePath),
Some(ConfigOption::DatabasePath(db_path)) =>
assert_eq!(*db_path, config.get())
);
assert_matches!(values.get(&OptionNames::Me), Some(ConfigOption::Me(val)) =>
assert_eq!(Some(val.clone()), config.get())
);
}
}

View File

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

View File

@ -1,13 +1,19 @@
#[macro_use]
extern crate config_derive;
mod api;
pub use api::{
CoreApp, CoreRequest, CoreResponse, CreateGameRequest, HotseatPlayerRequest, PlayerInfoRequest,
ChangeSettingRequest, CoreApp, CoreRequest, CoreResponse, CreateGameRequest,
HotseatPlayerRequest, PlayerInfoRequest,
};
mod board;
pub use board::*;
/*
mod config;
pub use config::*;
*/
mod database;

View File

@ -1,14 +1,40 @@
use crate::{
api::PlayStoneRequest,
board::{Board, Coordinate},
config::DatabasePath,
database::Database,
};
use config::define_config;
use config_derive::ConfigOption;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, time::Duration};
use thiserror::Error;
use typeshare::typeshare;
define_config! {
DatabasePath(DatabasePath),
Me(Me),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
pub struct DatabasePath(pub PathBuf);
impl std::ops::Deref for DatabasePath {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
pub struct Me(Player);
impl std::ops::Deref for Me {
type Target = Player;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, PartialEq, Error)]
pub enum BoardError {
#[error("Position is invalid")]

View File

@ -0,0 +1,24 @@
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

@ -1,10 +0,0 @@
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

@ -1,36 +1,70 @@
use serde::{Deserialize, Serialize};
use sgf::{
go::{Game, Rank},
Date,
};
use sgf::go::{Game, GameResult, Win};
use typeshare::typeshare;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[typeshare]
pub struct GamePreviewElement {
pub date: Vec<Date>,
pub date: String,
pub name: String,
pub black_player: String,
pub black_rank: Option<Rank>,
pub white_player: String,
pub white_rank: Option<Rank>,
pub result: String,
}
impl GamePreviewElement {
pub fn new(game: &Game) -> GamePreviewElement {
let black_player = match game.info.black_player {
Some(ref black_player) => black_player.clone(),
None => "unknown".to_owned(),
};
let white_player = match game.info.white_player {
Some(ref white_player) => white_player.clone(),
None => "unknown".to_owned(),
};
let black_player = match game.info.black_rank {
Some(rank) => format!("{} ({})", black_player, rank.to_string()),
None => black_player,
};
let white_player = match game.info.white_rank {
Some(rank) => format!("{} ({})", white_player, rank.to_string()),
None => white_player,
};
let name = match game.info.game_name {
Some(ref name) => name.clone(),
None => format!("{} vs. {}", black_player, white_player),
};
let format_win = |win: &Win| match win {
Win::Resignation => "Resignation".to_owned(),
Win::Time => "Timeout".to_owned(),
Win::Forfeit => "Forfeit".to_owned(),
Win::Score(score) => format!("{:.1}", score),
};
let result = match game.info.result {
Some(GameResult::Annulled) => "Annulled".to_owned(),
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(),
};
GamePreviewElement {
date: game.info.date.clone(),
black_player: game
date: game
.info
.black_player
.clone()
.unwrap_or("black_player".to_owned()),
black_rank: game.info.black_rank.clone(),
white_player: game
.info
.white_player
.clone()
.unwrap_or("white_player".to_owned()),
white_rank: game.info.white_rank.clone(),
.date
.first()
.map(|dt| dt.to_string())
.unwrap_or("".to_owned()),
name,
black_player,
white_player,
result,
}
}
}

View File

@ -1,3 +1,31 @@
pub mod action;
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
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,14 +0,0 @@
use crate::{
ui::types;
};
use serde::{Deserialize, Serialize};
use typeshare::typeshare;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum LaunchScreenView {
CreateGame(CreateGameView)
}
// This will be called when the Kifu application starts.
pub fn launch_screen() -> LaunchScreenView {
}

View File

@ -1,12 +1,12 @@
mod configuration;
pub use configuration::{configuration, ConfigurationView};
mod elements;
pub use elements::{action::Action, game_preview::GamePreviewElement, menu::Menu};
pub use elements::{game_preview::GamePreviewElement, menu::Menu, Action, Field, Toggle};
mod playing_field;
pub use playing_field::{playing_field, PlayingFieldView};
// mod launch_screen;
// pub use launch_screen::{launch_screen, LaunchScreenView};
mod home;
pub use home::{home, HomeView, HotseatPlayerElement, PlayerElement};

View File

@ -9,14 +9,16 @@ 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" }
gtk = { version = "0.6", package = "gtk4" }
gtk = { version = "0.6", package = "gtk4", features = [ "v4_8" ] }
image = { version = "0.24" }
kifu-core = { path = "../core" }
pango = { version = "*" }
sgf = { path = "../../sgf" }
tokio = { version = "1.26", features = [ "full" ] }
screenplay = { path = "../../screenplay" }
[build-dependencies]
glib-build-tools = "0.17"

View File

@ -1,7 +1,7 @@
fn main() {
glib_build_tools::compile_resources(
&["resources"],
"resources/resources.gresources.xml",
"resources/gresources.xml",
"com.luminescent-dreams.kifu-gtk.gresource",
);
}

View File

@ -1 +1,8 @@
{"Me":{"name":"Savanni","rank":{"Kyu":10}},"DatabasePath":"kifu/core/fixtures/five_games"}
<<<<<<< HEAD
{
"Me":{
"name":"Savanni",
"rank":{"Kyu":10}
},
"DatabasePath": "kifu/core/fixtures/five_games/"
}

View File

@ -2,5 +2,6 @@
<gresources>
<gresource prefix="/com/luminescent-dreams/kifu-gtk/">
<file>wood_texture.jpg</file>
<file>style.css</file>
</gresource>
</gresources>

View File

@ -0,0 +1,3 @@
.content {
padding: 8px;
}

View File

@ -1,20 +1,28 @@
use gtk::prelude::*;
use adw::prelude::*;
use kifu_core::{CoreApp, CoreRequest, CoreResponse};
use kifu_gtk::{
perftrace,
ui::{Home, PlayingField},
ui::{AppWindow, ConfigurationPage, Home, PlayingField},
CoreApi,
};
use std::sync::{Arc, RwLock};
fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreResponse) {
fn handle_response(api: CoreApi, app_window: &AppWindow, 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 new_game = Home::new(api, view);
window.set_child(Some(&new_game));
let home = Home::new(api, view);
app_window.set_content(&home);
}),
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
let api = api.clone();
@ -23,13 +31,16 @@ fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreRe
if playing_field.is_none() {
perftrace("creating a new playing field", || {
let field = PlayingField::new(api, view);
window.set_child(Some(&field));
app_window.set_content(&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);
}),
}
}
@ -65,8 +76,9 @@ fn main() {
}
});
let app = gtk::Application::builder()
let app = adw::Application::builder()
.application_id("com.luminescent-dreams.kifu-gtk")
.resource_base_path("/com/luminescent-dreams/kifu-gtk")
.build();
app.connect_activate({
@ -75,20 +87,30 @@ fn main() {
let (gtk_tx, gtk_rx) =
gtk::glib::MainContext::channel::<CoreResponse>(gtk::glib::PRIORITY_DEFAULT);
let app_window = AppWindow::new(&app);
let api = CoreApi {
gtk_tx,
rt: runtime.clone(),
core: core.clone(),
};
let window = gtk::ApplicationWindow::new(app);
window.present();
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);
app_window.window.present();
gtk_rx.attach(None, {
let api = api.clone();
move |message| {
perftrace("handle_response", || {
handle_response(api.clone(), window.clone(), message)
handle_response(api.clone(), &app_window, message)
});
Continue(true)
}

52
kifu/gtk/src/ui/config.rs Normal file
View File

@ -0,0 +1,52 @@
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

@ -3,7 +3,13 @@ use gtk::{glib, prelude::*, subclass::prelude::*};
use kifu_core::ui::GamePreviewElement;
#[derive(Default)]
pub struct GamePreviewPrivate;
pub struct GamePreviewPrivate {
date: gtk::Label,
title: gtk::Label,
black_player: gtk::Label,
white_player: gtk::Label,
result: gtk::Label,
}
#[glib::object_subclass]
impl ObjectSubclass for GamePreviewPrivate {
@ -21,22 +27,26 @@ glib::wrapper! {
}
impl GamePreview {
pub fn new(element: GamePreviewElement) -> GamePreview {
pub fn new() -> GamePreview {
let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Horizontal);
s.set_homogeneous(true);
s.set_hexpand(false);
println!("game_preview: {:?}", element);
let black_player = match element.black_rank {
Some(rank) => format!("{} ({})", element.black_player, rank.to_string()),
None => element.black_player,
};
let white_player = match element.white_rank {
Some(rank) => format!("{} ({})", element.white_player, rank.to_string()),
None => element.white_player,
};
s.append(&gtk::Label::new(Some(&black_player)));
s.append(&gtk::Label::new(Some(&white_player)));
s.append(&s.imp().date);
s.append(&s.imp().title);
s.append(&s.imp().black_player);
s.append(&s.imp().white_player);
s.append(&s.imp().result);
s
}
pub fn set_game(&self, element: GamePreviewElement) {
self.imp().black_player.set_text(&element.black_player);
self.imp().white_player.set_text(&element.white_player);
self.imp().title.set_text(&element.name);
self.imp().date.set_text(&element.date);
self.imp().result.set_text(&element.result);
}
}

View File

@ -1,5 +1,4 @@
use crate::ui::GamePreview;
use crate::CoreApi;
use crate::{ui::Library, CoreApi};
use glib::Object;
use gtk::{glib, prelude::*, subclass::prelude::*};
use kifu_core::{
@ -101,31 +100,54 @@ impl Default for HomePrivate {
impl ObjectSubclass for HomePrivate {
const NAME: &'static str = "Home";
type Type = Home;
type ParentType = gtk::Grid;
type ParentType = gtk::Box;
}
impl ObjectImpl for HomePrivate {}
impl WidgetImpl for HomePrivate {}
impl GridImpl for HomePrivate {}
impl BoxImpl for HomePrivate {}
glib::wrapper! {
pub struct Home(ObjectSubclass<HomePrivate>) @extends gtk::Grid, gtk::Widget;
pub struct Home(ObjectSubclass<HomePrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}
impl Home {
pub fn new(api: CoreApi, view: HomeView) -> Home {
let s: Self = Object::builder().build();
s.set_spacing(4);
s.set_homogeneous(false);
s.set_orientation(gtk::Orientation::Vertical);
let players = gtk::Box::builder()
.spacing(4)
.orientation(gtk::Orientation::Horizontal)
.build();
s.append(&players);
let black_player = PlayerDataEntry::new(view.black_player);
s.attach(&black_player, 1, 1, 1, 1);
players.append(&black_player);
*s.imp().black_player.borrow_mut() = Some(black_player.clone());
let white_player = PlayerDataEntry::new(view.white_player);
s.attach(&white_player, 2, 1, 1, 1);
players.append(&white_player);
*s.imp().white_player.borrow_mut() = Some(white_player.clone());
let new_game_button = gtk::Button::builder().label(&view.start_game.label).build();
s.attach(&new_game_button, 2, 2, 1, 1);
let new_game_button = gtk::Button::builder()
.css_classes(vec!["suggested-action"])
.label(&view.start_game.label)
.build();
s.append(&new_game_button);
let library = Library::new();
let library_view = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never)
.min_content_width(360)
.vexpand(true)
.hexpand(true)
.child(&library)
.build();
s.append(&library_view);
library.set_games(view.games);
new_game_button.connect_clicked({
move |_| {
@ -139,12 +161,6 @@ impl Home {
}
});
let game_list = gtk::Box::new(gtk::Orientation::Vertical, 0);
s.attach(&game_list, 1, 3, 2, 1);
view.games
.iter()
.for_each(|game_preview| game_list.append(&GamePreview::new(game_preview.clone())));
s
}
}

166
kifu/gtk/src/ui/library.rs Normal file
View File

@ -0,0 +1,166 @@
use crate::ui::GamePreview;
use adw::{prelude::*, subclass::prelude::*};
use glib::Object;
use gtk::{glib, prelude::*, subclass::prelude::*};
use kifu_core::ui::GamePreviewElement;
use std::{cell::RefCell, rc::Rc};
#[derive(Default)]
pub struct GameObjectPrivate {
game: Rc<RefCell<Option<GamePreviewElement>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for GameObjectPrivate {
const NAME: &'static str = "GameObject";
type Type = GameObject;
}
impl ObjectImpl for GameObjectPrivate {}
glib::wrapper! {
pub struct GameObject(ObjectSubclass<GameObjectPrivate>);
}
impl GameObject {
pub fn new(game: GamePreviewElement) -> Self {
let s: Self = Object::builder().build();
*s.imp().game.borrow_mut() = Some(game);
s
}
pub fn game(&self) -> Option<GamePreviewElement> {
self.imp().game.borrow().clone()
}
}
pub struct LibraryPrivate {
model: gio::ListStore,
list_view: gtk::ColumnView,
}
impl Default for LibraryPrivate {
fn default() -> Self {
let vector: Vec<GameObject> = vec![];
let model = gio::ListStore::new(glib::types::Type::OBJECT);
model.extend_from_slice(&vector);
/*
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let preview = GamePreview::new();
list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be a ListItem")
.set_child(Some(&preview));
});
factory.connect_bind(move |_, list_item| {
let game_element = list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<GameObject>()
.expect("The item has to be a GameObject.");
let preview = list_item
.downcast_ref::<gtk::ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<GamePreview>()
.expect("The child has to be a GamePreview object.");
match game_element.game() {
Some(game) => preview.set_game(game),
None => (),
};
});
*/
let selection_model = gtk::NoSelection::new(Some(model.clone()));
let list_view = gtk::ColumnView::builder().model(&selection_model).build();
fn make_factory<F>(bind: F) -> gtk::SignalListItemFactory
where
F: Fn(GamePreviewElement) -> String + 'static,
{
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(|_, list_item| {
list_item
.downcast_ref::<gtk::ListItem>()
.unwrap()
.set_child(Some(
&gtk::Label::builder()
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::End)
.build(),
))
});
factory.connect_bind(move |_, list_item| {
let list_item = list_item.downcast_ref::<gtk::ListItem>().unwrap();
let game = list_item.item().and_downcast::<GameObject>().unwrap();
let preview = list_item.child().and_downcast::<gtk::Label>().unwrap();
match game.game() {
Some(game) => preview.set_text(&bind(game)),
None => (),
};
});
factory
}
list_view.append_column(&gtk::ColumnViewColumn::new(
Some("date"),
Some(make_factory(|g| g.date)),
));
list_view.append_column(&gtk::ColumnViewColumn::new(
Some("title"),
Some(make_factory(|g| g.name)),
));
list_view.append_column(&gtk::ColumnViewColumn::new(
Some("black"),
Some(make_factory(|g| g.black_player)),
));
list_view.append_column(&gtk::ColumnViewColumn::new(
Some("white"),
Some(make_factory(|g| g.white_player)),
));
list_view.append_column(&gtk::ColumnViewColumn::new(
Some("result"),
Some(make_factory(|g| g.result)),
));
Self { model, list_view }
}
}
#[glib::object_subclass]
impl ObjectSubclass for LibraryPrivate {
const NAME: &'static str = "Library";
type Type = Library;
type ParentType = adw::Bin;
}
impl ObjectImpl for LibraryPrivate {}
impl WidgetImpl for LibraryPrivate {}
impl BinImpl for LibraryPrivate {}
glib::wrapper! {
pub struct Library(ObjectSubclass<LibraryPrivate>) @extends adw::Bin, gtk::Widget;
}
impl Library {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s.set_child(Some(&s.imp().list_view));
s
}
pub fn set_games(&self, games: Vec<GamePreviewElement>) {
let games = games
.into_iter()
.map(|g| GameObject::new(g))
.collect::<Vec<GameObject>>();
self.imp().model.extend_from_slice(&games);
}
}

View File

@ -1,9 +1,20 @@
use adw::prelude::*;
use gio::resources_lookup_data;
use glib::IsA;
use gtk::{prelude::*, STYLE_PROVIDER_PRIORITY_USER};
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 player_card;
pub use player_card::PlayerCard;
@ -18,3 +29,72 @@ 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(&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

@ -1,3 +1,3 @@
[toolchain]
channel = "1.68.2"
channel = "1.71.1"
targets = [ "wasm32-unknown-unknown" ]

View File

@ -24,6 +24,16 @@ pub enum Date {
Date(chrono::NaiveDate),
}
impl Date {
pub fn to_string(&self) -> String {
match self {
Date::Year(y) => format!("{}", y),
Date::YearMonth(y, m) => format!("{}-{}", y, m),
Date::Date(date) => format!("{}-{}-{}", date.year(), date.month(), date.day()),
}
}
}
/*
impl TryFrom<&str> for Date {
type Error = String;

View File

@ -105,6 +105,9 @@ impl TryFrom<Tree> for Game {
};
let mut info = GameInfo::default();
info.app_name = tree.root.find_prop("AP").map(|prop| prop.values[0].clone());
info.game_name = tree.root.find_prop("GN").map(|prop| prop.values[0].clone());
info.black_player = tree.root.find_prop("PB").map(|prop| prop.values.join(", "));
info.black_rank = tree
@ -236,6 +239,7 @@ pub enum GameResult {
Draw,
Black(Win),
White(Win),
Unknown(String),
}
impl TryFrom<&str> for GameResult {
@ -250,7 +254,7 @@ impl TryFrom<&str> for GameResult {
let res = match parts[0].to_ascii_lowercase().as_str() {
"b" => GameResult::Black,
"w" => GameResult::White,
_ => panic!("unknown result format"),
_ => return Ok(GameResult::Unknown(parts[0].to_owned())),
};
match parts[1].to_ascii_lowercase().as_str() {
"r" | "resign" => Ok(res(Win::Resignation)),