Compare commits

..

5 Commits

46 changed files with 664 additions and 1520 deletions

1
.gitignore vendored
View File

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

531
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,14 +0,0 @@
[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" ] }

View File

@ -1,23 +0,0 @@
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()
}

View File

@ -1,16 +0,0 @@
[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" }

View File

@ -1,160 +0,0 @@
/*
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

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

View File

@ -1,11 +0,0 @@
#!/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

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

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

View File

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

View File

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

View File

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

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

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

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

@ -1,16 +0,0 @@
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,12 +250,8 @@ impl From<chrono::NaiveDate> for IFC {
{ {
days = days - 1; days = days - 1;
} }
let mut month: u8 = (days / 28).try_into().unwrap(); let month: u8 = (days / 28).try_into().unwrap();
let mut day: u8 = (days % 28).try_into().unwrap(); let day: u8 = (days % 28).try_into().unwrap();
if day == 0 {
month = month - 1;
day = 28;
}
Self::Day(Day { Self::Day(Day {
year: date.year(), year: date.year(),
month: month + 1, month: month + 1,
@ -272,6 +268,167 @@ 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -455,34 +612,10 @@ mod tests {
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 1).unwrap()), IFC::from(NaiveDate::from_ymd_opt(12022, 1, 1).unwrap()),
IFC::ymd(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!( assert_eq!(
IFC::from(NaiveDate::from_ymd_opt(12022, 1, 29).unwrap()), IFC::from(NaiveDate::from_ymd_opt(12022, 1, 29).unwrap()),
IFC::ymd(12022, 2, 1).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!( assert_eq!(
IFC::from(NaiveDate::from_ymd_opt(12022, 2, 26).unwrap()), IFC::from(NaiveDate::from_ymd_opt(12022, 2, 26).unwrap()),
IFC::ymd(12022, 3, 1).unwrap() IFC::ymd(12022, 3, 1).unwrap()
@ -511,10 +644,6 @@ mod tests {
IFC::from(NaiveDate::from_ymd_opt(12022, 8, 13).unwrap()), IFC::from(NaiveDate::from_ymd_opt(12022, 8, 13).unwrap()),
IFC::ymd(12022, 9, 1).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!( assert_eq!(
IFC::from(NaiveDate::from_ymd_opt(12022, 9, 10).unwrap()), IFC::from(NaiveDate::from_ymd_opt(12022, 9, 10).unwrap()),
IFC::ymd(12022, 10, 1).unwrap() IFC::ymd(12022, 10, 1).unwrap()

View File

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

View File

@ -1,34 +1,23 @@
use crate::{ use crate::{
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank}, types::{AppState, GameState, Player, Rank},
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView}, ui::{home, playing_field, HomeView, PlayingFieldView},
Config, DatabasePath,
}; };
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,42 +58,46 @@ 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>>,
} }
impl CoreApp { impl CoreApp {
pub fn new(config_path: std::path::PathBuf) -> Self { 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 config = Config::from_path(config_path).expect("configuration to open");
let db_path: DatabasePath = config.get().unwrap(); let db_path: DatabasePath = config.get();
let state = Arc::new(RwLock::new(AppState::new(db_path))); let state = Arc::new(RwLock::new(AppState::new(db_path)));
Self { println!("config: {:?}", config);
config: Arc::new(RwLock::new(config)), println!("games database: {:?}", state.read().unwrap().database.len());
state,
} Self { 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 +121,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();

198
kifu/core/src/config.rs Normal file
View File

@ -0,0 +1,198 @@
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,18 +39,13 @@ 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) => {
for sgf in sgfs {
match sgf { match sgf {
Game::Go(game) => games.push(game), Game::Go(game) => games.push(game),
Game::Unsupported(_) => {} Game::Unsupported(_) => {}
} }
} }
} }
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
}
}
} }
Err(err) => println!("failed entry: {:?}", err), Err(err) => println!("failed entry: {:?}", err),
} }

View File

@ -1,19 +1,13 @@
#[macro_use]
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;
pub use board::*; pub use board::*;
/*
mod config; mod config;
pub use config::*; pub use config::*;
*/
mod database; mod database;

View File

@ -1,40 +1,14 @@
use crate::{ use crate::{
api::PlayStoneRequest, api::PlayStoneRequest,
board::{Board, Coordinate}, board::{Board, Coordinate},
config::DatabasePath,
database::Database, database::Database,
}; };
use config::define_config;
use config_derive::ConfigOption;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{path::PathBuf, time::Duration}; use std::{path::PathBuf, time::Duration};
use thiserror::Error; use thiserror::Error;
use typeshare::typeshare; 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)] #[derive(Debug, PartialEq, Error)]
pub enum BoardError { pub enum BoardError {
#[error("Position is invalid")] #[error("Position is invalid")]

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

@ -1,70 +1,36 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sgf::go::{Game, GameResult, Win}; use sgf::{
go::{Game, Rank},
Date,
};
use typeshare::typeshare; use typeshare::typeshare;
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[typeshare] #[typeshare]
pub struct GamePreviewElement { pub struct GamePreviewElement {
pub date: String, pub date: Vec<Date>,
pub name: String,
pub black_player: String, pub black_player: String,
pub black_rank: Option<Rank>,
pub white_player: String, pub white_player: String,
pub result: String, pub white_rank: Option<Rank>,
} }
impl GamePreviewElement { impl GamePreviewElement {
pub fn new(game: &Game) -> 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 { GamePreviewElement {
date: game date: game.info.date.clone(),
black_player: game
.info .info
.date .black_player
.first() .clone()
.map(|dt| dt.to_string()) .unwrap_or("black_player".to_owned()),
.unwrap_or("".to_owned()), black_rank: game.info.black_rank.clone(),
name, white_player: game
black_player, .info
white_player, .white_player
result, .clone()
.unwrap_or("white_player".to_owned()),
white_rank: game.info.white_rank.clone(),
} }
} }
} }

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

@ -0,0 +1,14 @@
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; 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};
// mod launch_screen;
// pub use launch_screen::{launch_screen, LaunchScreenView};
mod home; mod home;
pub use home::{home, HomeView, HotseatPlayerElement, PlayerElement}; pub use home::{home, HomeView, HotseatPlayerElement, PlayerElement};

View File

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

View File

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

View File

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

View File

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

View File

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

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::{AppWindow, ConfigurationPage, Home, PlayingField}, ui::{Home, PlayingField},
CoreApi, CoreApi,
}; };
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
fn handle_response(api: CoreApi, app_window: &AppWindow, 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 new_game = Home::new(api, view);
app_window.set_content(&home); window.set_child(Some(&new_game));
}), }),
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, app_window: &AppWindow, 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);
app_window.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({
@ -87,30 +75,20 @@ fn main() {
let (gtk_tx, gtk_rx) = let (gtk_tx, gtk_rx) =
gtk::glib::MainContext::channel::<CoreResponse>(gtk::glib::PRIORITY_DEFAULT); gtk::glib::MainContext::channel::<CoreResponse>(gtk::glib::PRIORITY_DEFAULT);
let app_window = AppWindow::new(&app);
let api = CoreApi { let api = CoreApi {
gtk_tx, gtk_tx,
rt: runtime.clone(), rt: runtime.clone(),
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({ window.present();
let api = api.clone();
move |_, _| {
api.dispatch(CoreRequest::OpenConfiguration);
}
});
app.add_action(&action_config);
app_window.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(), &app_window, 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

@ -3,13 +3,7 @@ use gtk::{glib, prelude::*, subclass::prelude::*};
use kifu_core::ui::GamePreviewElement; use kifu_core::ui::GamePreviewElement;
#[derive(Default)] #[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] #[glib::object_subclass]
impl ObjectSubclass for GamePreviewPrivate { impl ObjectSubclass for GamePreviewPrivate {
@ -27,26 +21,22 @@ glib::wrapper! {
} }
impl GamePreview { impl GamePreview {
pub fn new() -> GamePreview { pub fn new(element: GamePreviewElement) -> 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_hexpand(false);
s.append(&s.imp().date); println!("game_preview: {:?}", element);
s.append(&s.imp().title); let black_player = match element.black_rank {
s.append(&s.imp().black_player); Some(rank) => format!("{} ({})", element.black_player, rank.to_string()),
s.append(&s.imp().white_player); None => element.black_player,
s.append(&s.imp().result); };
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 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,4 +1,5 @@
use crate::{ui::Library, CoreApi}; use crate::ui::GamePreview;
use crate::CoreApi;
use glib::Object; use glib::Object;
use gtk::{glib, prelude::*, subclass::prelude::*}; use gtk::{glib, prelude::*, subclass::prelude::*};
use kifu_core::{ use kifu_core::{
@ -100,54 +101,31 @@ impl Default for HomePrivate {
impl ObjectSubclass for HomePrivate { impl ObjectSubclass for HomePrivate {
const NAME: &'static str = "Home"; const NAME: &'static str = "Home";
type Type = Home; type Type = Home;
type ParentType = gtk::Box; type ParentType = gtk::Grid;
} }
impl ObjectImpl for HomePrivate {} impl ObjectImpl for HomePrivate {}
impl WidgetImpl for HomePrivate {} impl WidgetImpl for HomePrivate {}
impl BoxImpl for HomePrivate {} impl GridImpl for HomePrivate {}
glib::wrapper! { glib::wrapper! {
pub struct Home(ObjectSubclass<HomePrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable; pub struct Home(ObjectSubclass<HomePrivate>) @extends gtk::Grid, gtk::Widget;
} }
impl Home { impl Home {
pub fn new(api: CoreApi, view: HomeView) -> Home { pub fn new(api: CoreApi, view: HomeView) -> Home {
let s: Self = Object::builder().build(); 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); let black_player = PlayerDataEntry::new(view.black_player);
players.append(&black_player); s.attach(&black_player, 1, 1, 1, 1);
*s.imp().black_player.borrow_mut() = Some(black_player.clone()); *s.imp().black_player.borrow_mut() = Some(black_player.clone());
let white_player = PlayerDataEntry::new(view.white_player); let white_player = PlayerDataEntry::new(view.white_player);
players.append(&white_player); s.attach(&white_player, 2, 1, 1, 1);
*s.imp().white_player.borrow_mut() = Some(white_player.clone()); *s.imp().white_player.borrow_mut() = Some(white_player.clone());
let new_game_button = gtk::Button::builder() let new_game_button = gtk::Button::builder().label(&view.start_game.label).build();
.css_classes(vec!["suggested-action"]) s.attach(&new_game_button, 2, 2, 1, 1);
.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({ new_game_button.connect_clicked({
move |_| { move |_| {
@ -161,6 +139,12 @@ 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 s
} }
} }

View File

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

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

View File

@ -24,16 +24,6 @@ pub enum Date {
Date(chrono::NaiveDate), 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 { impl TryFrom<&str> for Date {
type Error = String; type Error = String;

View File

@ -105,9 +105,6 @@ impl TryFrom<Tree> for Game {
}; };
let mut info = GameInfo::default(); let mut info = GameInfo::default();
info.app_name = tree.root.find_prop("AP").map(|prop| prop.values[0].clone()); 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_player = tree.root.find_prop("PB").map(|prop| prop.values.join(", "));
info.black_rank = tree info.black_rank = tree
@ -239,7 +236,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 {
@ -254,7 +250,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)),