Compare commits
40 Commits
62b8e90c85
...
b9425af234
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | b9425af234 | |
Savanni D'Gerinel | 4bd6388913 | |
Savanni D'Gerinel | 8dad470440 | |
Savanni D'Gerinel | e0fb7714d0 | |
Savanni D'Gerinel | b866249c9d | |
Savanni D'Gerinel | 2a6a5de5e1 | |
Savanni D'Gerinel | 1489121877 | |
Savanni D'Gerinel | 562d4871a1 | |
Savanni D'Gerinel | 16c8dcb682 | |
Savanni D'Gerinel | cc828c417a | |
Savanni D'Gerinel | 784f3ff7f4 | |
Savanni D'Gerinel | 5439e2ac04 | |
Savanni D'Gerinel | 0bf6e079a2 | |
Savanni D'Gerinel | 3998538e88 | |
Savanni D'Gerinel | 793cd67218 | |
Savanni D'Gerinel | ff13ff3c0e | |
Savanni D'Gerinel | cc3ad372e6 | |
Savanni D'Gerinel | 3c063af525 | |
Savanni D'Gerinel | aa64bf4c7e | |
Savanni D'Gerinel | f75e0d4d65 | |
Savanni D'Gerinel | d8534a08eb | |
Savanni D'Gerinel | e5d0b7d20f | |
Savanni D'Gerinel | e9ffab1187 | |
Savanni D'Gerinel | a584fb4de3 | |
Savanni D'Gerinel | e3f4ca246d | |
Savanni D'Gerinel | 07b7351501 | |
Savanni D'Gerinel | 70a295d4b1 | |
Savanni D'Gerinel | 5478d388cb | |
Savanni D'Gerinel | e203b17c8b | |
Savanni D'Gerinel | 69583dfd64 | |
Savanni D'Gerinel | d59c2585db | |
Savanni D'Gerinel | a6fcbfac71 | |
Savanni D'Gerinel | 4f940099da | |
Savanni D'Gerinel | f4735dd16b | |
Savanni D'Gerinel | b662ac519a | |
Savanni D'Gerinel | efec8dfe5a | |
Savanni D'Gerinel | 0765d94a5e | |
Savanni D'Gerinel | 40b33797f3 | |
Savanni D'Gerinel | 24e88da8e2 | |
Savanni D'Gerinel | 456d872b40 |
|
@ -3,3 +3,4 @@ target
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
result
|
result
|
||||||
|
*.tgz
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
|
@ -1,16 +1,21 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
"changeset",
|
||||||
|
"config",
|
||||||
|
"config-derive",
|
||||||
|
"coordinates",
|
||||||
"cyberpunk-splash",
|
"cyberpunk-splash",
|
||||||
"dashboard",
|
"dashboard",
|
||||||
|
"emseries",
|
||||||
|
"flow",
|
||||||
"fluent-ergonomics",
|
"fluent-ergonomics",
|
||||||
"geo-types",
|
"geo-types",
|
||||||
|
"gm-control-panel",
|
||||||
|
"hex-grid",
|
||||||
"ifc",
|
"ifc",
|
||||||
|
"kifu/core",
|
||||||
|
"kifu/gtk",
|
||||||
"memorycache",
|
"memorycache",
|
||||||
"screenplay",
|
"screenplay",
|
||||||
"emseries",
|
|
||||||
"coordinates",
|
|
||||||
"flow",
|
|
||||||
"sgf",
|
"sgf",
|
||||||
"changeset",
|
|
||||||
"hex-grid",
|
|
||||||
]
|
]
|
||||||
|
|
41
build.sh
41
build.sh
|
@ -3,19 +3,24 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
RUST_ALL_TARGETS=(
|
RUST_ALL_TARGETS=(
|
||||||
"dashboard"
|
|
||||||
"ifc"
|
|
||||||
"memorycache"
|
|
||||||
"geo-types"
|
|
||||||
"fluent-ergonomics"
|
|
||||||
"cyberpunk-splash"
|
|
||||||
"screenplay"
|
|
||||||
"emseries"
|
|
||||||
"coordinates"
|
|
||||||
"flow"
|
|
||||||
"sgf"
|
|
||||||
"changeset"
|
"changeset"
|
||||||
|
"config"
|
||||||
|
"config-derive"
|
||||||
|
"coordinates"
|
||||||
|
"cyberpunk-splash"
|
||||||
|
"dashboard"
|
||||||
|
"emseries"
|
||||||
|
"flow"
|
||||||
|
"fluent-ergonomics"
|
||||||
|
"geo-types"
|
||||||
|
"gm-control-panel"
|
||||||
"hex-grid"
|
"hex-grid"
|
||||||
|
"ifc"
|
||||||
|
"kifu-core"
|
||||||
|
"kifu-gtk"
|
||||||
|
"memorycache"
|
||||||
|
"screenplay"
|
||||||
|
"sgf"
|
||||||
)
|
)
|
||||||
|
|
||||||
build_rust_targets() {
|
build_rust_targets() {
|
||||||
|
@ -27,6 +32,16 @@ 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
|
||||||
|
@ -43,7 +58,9 @@ if [ "${CMD}" == "clean" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for cmd in $CMD; do
|
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[*]}
|
build_rust_targets $cmd ${RUST_ALL_TARGETS[*]}
|
||||||
else
|
else
|
||||||
build_rust_targets $cmd $TARGET
|
build_rust_targets $cmd $TARGET
|
||||||
|
|
|
@ -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" ] }
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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" }
|
|
@ -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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Version=1.0
|
||||||
|
Name=dashboard
|
||||||
|
Comment=My personal system dashboard
|
||||||
|
Exec=dashboard
|
|
@ -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/
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
fn main() {
|
||||||
|
glib_build_tools::compile_resources(
|
||||||
|
"resources",
|
||||||
|
"resources/gresources.xml",
|
||||||
|
"com.luminescent-dreams.gm-control-panel.gresource",
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
||||||
|
.playlist-card {
|
||||||
|
margin: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
197
ifc/src/lib.rs
197
ifc/src/lib.rs
|
@ -250,8 +250,12 @@ impl From<chrono::NaiveDate> for IFC {
|
||||||
{
|
{
|
||||||
days = days - 1;
|
days = days - 1;
|
||||||
}
|
}
|
||||||
let month: u8 = (days / 28).try_into().unwrap();
|
let mut month: u8 = (days / 28).try_into().unwrap();
|
||||||
let day: 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 {
|
Self::Day(Day {
|
||||||
year: date.year(),
|
year: date.year(),
|
||||||
month: month + 1,
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -612,10 +455,34 @@ 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()
|
||||||
|
@ -644,6 +511,10 @@ 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()
|
||||||
|
|
|
@ -7,6 +7,8 @@ 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" }
|
||||||
|
|
|
@ -7,8 +7,7 @@
|
||||||
;W[fd]
|
;W[fd]
|
||||||
;B[qf]
|
;B[qf]
|
||||||
;W[qh]
|
;W[qh]
|
||||||
(;B[pf]
|
(;B[of]
|
||||||
)(;B[of]
|
|
||||||
;W[nd]
|
;W[nd]
|
||||||
;B[mf]
|
;B[mf]
|
||||||
;W[pk]
|
;W[pk]
|
||||||
|
@ -221,4 +220,5 @@
|
||||||
;B[ai]
|
;B[ai]
|
||||||
;W[qg]
|
;W[qg]
|
||||||
;B[pf]
|
;B[pf]
|
||||||
|
)(;B[pf]
|
||||||
))
|
))
|
|
@ -1,23 +1,34 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{AppState, GameState, Player, Rank},
|
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank},
|
||||||
ui::{home, playing_field, HomeView, PlayingFieldView},
|
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView},
|
||||||
Config, DatabasePath,
|
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::{
|
||||||
|
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 {
|
||||||
|
@ -58,46 +69,42 @@ 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: Config,
|
config: Arc<RwLock<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();
|
let db_path: DatabasePath = config.get().unwrap();
|
||||||
let state = Arc::new(RwLock::new(AppState::new(db_path)));
|
let state = Arc::new(RwLock::new(AppState::new(db_path)));
|
||||||
|
|
||||||
println!("config: {:?}", config);
|
Self {
|
||||||
println!("games database: {:?}", state.read().unwrap().database.len());
|
config: Arc::new(RwLock::new(config)),
|
||||||
|
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 {
|
||||||
CoreRequest::LaunchScreen => {
|
ChangeSettingRequest::LibraryPath(path) => {
|
||||||
let app_state = self.state.read().unwrap();
|
let mut config = self.config.write().unwrap();
|
||||||
|
config.set(ConfigOption::DatabasePath(DatabasePath(PathBuf::from(
|
||||||
At launch, I want to either show a list of games in progress, the current game, or the game creation screen.
|
path,
|
||||||
- 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.
|
CoreResponse::UpdatedConfigurationView(configuration(&config))
|
||||||
- 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 = {
|
||||||
|
@ -121,6 +128,9 @@ 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();
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use sgf::{go::Game, parse_sgf};
|
||||||
|
|
||||||
use crate::{BoardError, Color, Size};
|
use crate::{BoardError, Color, Size};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
@ -77,6 +79,41 @@ pub struct Coordinate {
|
||||||
pub row: u8,
|
pub row: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Coordinate {
|
||||||
|
fn from_sgf(s: &str) -> Self {
|
||||||
|
fn parse(s: char) -> u8 {
|
||||||
|
match s {
|
||||||
|
'a' => 0,
|
||||||
|
'b' => 1,
|
||||||
|
'c' => 2,
|
||||||
|
'd' => 3,
|
||||||
|
'e' => 4,
|
||||||
|
'f' => 5,
|
||||||
|
'g' => 6,
|
||||||
|
'h' => 7,
|
||||||
|
'i' => 8,
|
||||||
|
'j' => 9,
|
||||||
|
'k' => 10,
|
||||||
|
'l' => 11,
|
||||||
|
'm' => 12,
|
||||||
|
'n' => 13,
|
||||||
|
'o' => 14,
|
||||||
|
'p' => 15,
|
||||||
|
'q' => 16,
|
||||||
|
'r' => 17,
|
||||||
|
's' => 18,
|
||||||
|
_ => panic!("invalid character in the SGF coordinates"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let s = s.chars().collect::<Vec<char>>();
|
||||||
|
Coordinate {
|
||||||
|
column: parse(s[0]),
|
||||||
|
row: parse(s[1]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
||||||
if let Some(_) = self.stone(&coordinate) {
|
if let Some(_) = self.stone(&coordinate) {
|
||||||
|
@ -123,17 +160,17 @@ impl Board {
|
||||||
.map(|g| g.color)
|
.map(|g| g.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
||||||
self.groups
|
self.groups
|
||||||
.iter()
|
.iter()
|
||||||
.find(|g| g.coordinates.contains(coordinate))
|
.find(|g| g.coordinates.contains(coordinate))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_group(&mut self, group: &Group) {
|
fn remove_group(&mut self, group: &Group) {
|
||||||
self.groups.retain(|g| g != group);
|
self.groups.retain(|g| g != group);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
|
fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
|
||||||
let adjacent_spaces = self.group_halo(group).into_iter();
|
let adjacent_spaces = self.group_halo(group).into_iter();
|
||||||
let mut grps: Vec<Group> = Vec::new();
|
let mut grps: Vec<Group> = Vec::new();
|
||||||
|
|
||||||
|
@ -153,7 +190,7 @@ impl Board {
|
||||||
grps
|
grps
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
||||||
group
|
group
|
||||||
.coordinates
|
.coordinates
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -162,14 +199,14 @@ impl Board {
|
||||||
.collect::<HashSet<Coordinate>>()
|
.collect::<HashSet<Coordinate>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn liberties(&self, group: &Group) -> usize {
|
fn liberties(&self, group: &Group) -> usize {
|
||||||
self.group_halo(group)
|
self.group_halo(group)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| self.stone(&c) == None)
|
.filter(|c| self.stone(&c) == None)
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
if coordinate.column > 0 {
|
if coordinate.column > 0 {
|
||||||
v.push(Coordinate {
|
v.push(Coordinate {
|
||||||
|
@ -194,7 +231,7 @@ impl Board {
|
||||||
v.into_iter().filter(|c| self.within_board(c)).collect()
|
v.into_iter().filter(|c| self.within_board(c)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn within_board(&self, coordinate: &Coordinate) -> bool {
|
fn within_board(&self, coordinate: &Coordinate) -> bool {
|
||||||
coordinate.column < self.size.width && coordinate.row < self.size.height
|
coordinate.column < self.size.width && coordinate.row < self.size.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,9 +248,37 @@ impl Group {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&Game> for Board {
|
||||||
|
type Error = BoardError;
|
||||||
|
fn try_from(record: &Game) -> Result<Self, Self::Error> {
|
||||||
|
let mut board = Board::new();
|
||||||
|
let mut root = Some(&record.root);
|
||||||
|
while let Some(node) = root {
|
||||||
|
match (node.find_prop("B"), node.find_prop("W")) {
|
||||||
|
(Some(prop), _) => {
|
||||||
|
let coordinate = Coordinate::from_sgf(prop.values[0].as_ref());
|
||||||
|
board = board.place_stone(coordinate, Color::Black)?;
|
||||||
|
}
|
||||||
|
(None, Some(prop)) => {
|
||||||
|
let coordinate = Coordinate::from_sgf(prop.values[0].as_ref());
|
||||||
|
board = board.place_stone(coordinate, Color::White)?;
|
||||||
|
}
|
||||||
|
(None, None) => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
root = node.next()
|
||||||
|
}
|
||||||
|
Ok(board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use std::{fs::File, io::Read};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use cool_asserts::assert_matches;
|
||||||
|
use sgf::{parse_sgf, Game};
|
||||||
|
|
||||||
/* Two players (Black and White) take turns and Black plays first
|
/* Two players (Black and White) take turns and Black plays first
|
||||||
* Stones are placed on the line intersections and not moved.
|
* Stones are placed on the line intersections and not moved.
|
||||||
|
@ -225,6 +290,25 @@ mod test {
|
||||||
* A stone placed in a suicidal position is legal if it captures other stones first.
|
* A stone placed in a suicidal position is legal if it captures other stones first.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
fn with_text(text: &str, f: impl FnOnce(Vec<sgf::go::Game>)) {
|
||||||
|
let games = parse_sgf(text)
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|game| match game {
|
||||||
|
Game::Go(g) => Some(g),
|
||||||
|
Game::Unsupported(_) => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<sgf::go::Game>>();
|
||||||
|
f(games);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<sgf::go::Game>)) {
|
||||||
|
let mut file = File::open(path).unwrap();
|
||||||
|
let mut text = String::new();
|
||||||
|
let _ = file.read_to_string(&mut text);
|
||||||
|
with_text(&text, f);
|
||||||
|
}
|
||||||
|
|
||||||
fn with_example_board(test: impl FnOnce(Board)) {
|
fn with_example_board(test: impl FnOnce(Board)) {
|
||||||
let board = Board::from_coordinates(
|
let board = Board::from_coordinates(
|
||||||
vec![
|
vec![
|
||||||
|
@ -630,4 +714,46 @@ mod test {
|
||||||
|
|
||||||
assert_eq!(board, b2);
|
assert_eq!(board, b2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn loads_board_from_sgf() {
|
||||||
|
with_file(
|
||||||
|
std::path::Path::new("fixtures/five_games/2022.10.05.sgf"),
|
||||||
|
|trees| {
|
||||||
|
let game = &trees[0];
|
||||||
|
let board = Board::try_from(game).expect("game to be valid");
|
||||||
|
assert_eq!(
|
||||||
|
board.stone(&Coordinate { column: 14, row: 5 }),
|
||||||
|
Some(Color::Black)
|
||||||
|
);
|
||||||
|
|
||||||
|
/* This game has a fork in the game tree here. This block verifies that we have
|
||||||
|
* reached the move right before the fork. */
|
||||||
|
let mut node = &game.root;
|
||||||
|
for _ in 0..8 {
|
||||||
|
node = node.next().unwrap();
|
||||||
|
}
|
||||||
|
let prop = assert_matches!(node.find_prop("W"), Some(prop) => prop);
|
||||||
|
assert_eq!(
|
||||||
|
prop,
|
||||||
|
sgf::Property {
|
||||||
|
ident: "W".to_owned(),
|
||||||
|
values: vec!["qh".to_owned()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/* And now we verify that we have gone down the leftmost side of the fork, which is
|
||||||
|
* traditionally the mainline of the game. */
|
||||||
|
let node = node.next().unwrap();
|
||||||
|
let prop = assert_matches!(node.find_prop("B"), Some(prop) => prop);
|
||||||
|
assert_eq!(
|
||||||
|
prop,
|
||||||
|
sgf::Property {
|
||||||
|
ident: "B".to_owned(),
|
||||||
|
values: vec!["of".to_owned()]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -39,13 +39,18 @@ impl Database {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.read_to_string(&mut buffer)
|
.read_to_string(&mut buffer)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for sgf in parse_sgf(&buffer).unwrap() {
|
match parse_sgf(&buffer) {
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
|
@ -88,7 +93,7 @@ mod test {
|
||||||
assert_eq!(game.info.black_player, Some("Steve".to_owned()));
|
assert_eq!(game.info.black_player, Some("Steve".to_owned()));
|
||||||
assert_eq!(game.info.white_player, Some("Savanni".to_owned()));
|
assert_eq!(game.info.white_player, Some("Savanni".to_owned()));
|
||||||
assert_eq!(game.info.date, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
|
assert_eq!(game.info.date, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
|
||||||
assert_eq!(game.info.komi, Some(6.5));
|
// assert_eq!(game.info.komi, Some(6.5));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate config_derive;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
pub use api::{
|
pub use api::{
|
||||||
CoreApp, CoreRequest, CoreResponse, CreateGameRequest, HotseatPlayerRequest, PlayerInfoRequest,
|
ChangeSettingRequest, CoreApp, CoreRequest, CoreResponse, CreateGameRequest,
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,40 @@
|
||||||
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")]
|
||||||
|
|
|
@ -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: (),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -1,36 +1,70 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sgf::{
|
use sgf::go::{Game, GameResult, Win};
|
||||||
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: Vec<Date>,
|
pub date: String,
|
||||||
|
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 white_rank: Option<Rank>,
|
pub result: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
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.info.date.clone(),
|
date: game
|
||||||
black_player: game
|
|
||||||
.info
|
.info
|
||||||
.black_player
|
.date
|
||||||
.clone()
|
.first()
|
||||||
.unwrap_or("black_player".to_owned()),
|
.map(|dt| dt.to_string())
|
||||||
black_rank: game.info.black_rank.clone(),
|
.unwrap_or("".to_owned()),
|
||||||
white_player: game
|
name,
|
||||||
.info
|
black_player,
|
||||||
.white_player
|
white_player,
|
||||||
.clone()
|
result,
|
||||||
.unwrap_or("white_player".to_owned()),
|
|
||||||
white_rank: game.info.white_rank.clone(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,31 @@
|
||||||
pub mod action;
|
use serde::{Deserialize, Serialize};
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
|
mod configuration;
|
||||||
|
pub use configuration::{configuration, ConfigurationView};
|
||||||
|
|
||||||
mod elements;
|
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;
|
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};
|
||||||
|
|
||||||
|
|
|
@ -9,24 +9,26 @@ 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" }
|
gtk = { version = "0.6", package = "gtk4", features = [ "v4_8" ] }
|
||||||
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"
|
||||||
|
|
||||||
[[bin]]
|
# [[bin]]
|
||||||
name = "kifu-gtk"
|
# name = "kifu-gtk"
|
||||||
path = "src/main.rs"
|
# path = "src/main.rs"
|
||||||
|
|
||||||
[[bin]]
|
# [[bin]]
|
||||||
name = "screenplay"
|
# name = "screenplay"
|
||||||
path = "src/bin/screenplay.rs"
|
# path = "src/bin/screenplay.rs"
|
||||||
required-features = [ "screenplay" ]
|
# required-features = [ "screenplay" ]
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
glib_build_tools::compile_resources(
|
glib_build_tools::compile_resources(
|
||||||
&["resources"],
|
&["resources"],
|
||||||
"resources/resources.gresources.xml",
|
"resources/gresources.xml",
|
||||||
"com.luminescent-dreams.kifu-gtk.gresource",
|
"com.luminescent-dreams.kifu-gtk.gresource",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,8 @@
|
||||||
{"Me":{"name":"Savanni","rank":{"Kyu":10}},"DatabasePath":"../core/fixtures/five_games"}
|
<<<<<<< HEAD
|
||||||
|
{
|
||||||
|
"Me":{
|
||||||
|
"name":"Savanni",
|
||||||
|
"rank":{"Kyu":10}
|
||||||
|
},
|
||||||
|
"DatabasePath": "kifu/core/fixtures/five_games/"
|
||||||
|
}
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
<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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
.content {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
|
@ -1,20 +1,28 @@
|
||||||
use gtk::prelude::*;
|
use adw::prelude::*;
|
||||||
use kifu_core::{CoreApp, CoreRequest, CoreResponse};
|
use kifu_core::{CoreApp, CoreRequest, CoreResponse};
|
||||||
use kifu_gtk::{
|
use kifu_gtk::{
|
||||||
perftrace,
|
perftrace,
|
||||||
ui::{Home, PlayingField},
|
ui::{AppWindow, ConfigurationPage, Home, PlayingField},
|
||||||
CoreApi,
|
CoreApi,
|
||||||
};
|
};
|
||||||
use std::sync::{Arc, RwLock};
|
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));
|
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 new_game = Home::new(api, view);
|
let home = Home::new(api, view);
|
||||||
window.set_child(Some(&new_game));
|
app_window.set_content(&home);
|
||||||
}),
|
}),
|
||||||
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
|
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
|
||||||
let api = api.clone();
|
let api = api.clone();
|
||||||
|
@ -23,13 +31,16 @@ fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreRe
|
||||||
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);
|
||||||
window.set_child(Some(&field));
|
app_window.set_content(&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);
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,8 +76,9 @@ fn main() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let app = gtk::Application::builder()
|
let app = adw::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({
|
||||||
|
@ -75,20 +87,30 @@ 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 window = gtk::ApplicationWindow::new(app);
|
let action_config = gio::SimpleAction::new("show-config", None);
|
||||||
window.present();
|
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, {
|
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(), window.clone(), message)
|
handle_response(api.clone(), &app_window, message)
|
||||||
});
|
});
|
||||||
Continue(true)
|
Continue(true)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,13 @@ 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 {
|
||||||
|
@ -21,22 +27,26 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GamePreview {
|
impl GamePreview {
|
||||||
pub fn new(element: GamePreviewElement) -> GamePreview {
|
pub fn new() -> 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);
|
||||||
|
|
||||||
println!("game_preview: {:?}", element);
|
s.append(&s.imp().date);
|
||||||
let black_player = match element.black_rank {
|
s.append(&s.imp().title);
|
||||||
Some(rank) => format!("{} ({})", element.black_player, rank.to_string()),
|
s.append(&s.imp().black_player);
|
||||||
None => element.black_player,
|
s.append(&s.imp().white_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(>k::Label::new(Some(&black_player)));
|
|
||||||
s.append(>k::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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use crate::ui::GamePreview;
|
use crate::{ui::Library, CoreApi};
|
||||||
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::{
|
||||||
|
@ -101,31 +100,54 @@ 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::Grid;
|
type ParentType = gtk::Box;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectImpl for HomePrivate {}
|
impl ObjectImpl for HomePrivate {}
|
||||||
impl WidgetImpl for HomePrivate {}
|
impl WidgetImpl for HomePrivate {}
|
||||||
impl GridImpl for HomePrivate {}
|
impl BoxImpl for HomePrivate {}
|
||||||
|
|
||||||
glib::wrapper! {
|
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 {
|
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);
|
||||||
s.attach(&black_player, 1, 1, 1, 1);
|
players.append(&black_player);
|
||||||
*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);
|
||||||
s.attach(&white_player, 2, 1, 1, 1);
|
players.append(&white_player);
|
||||||
*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().label(&view.start_game.label).build();
|
let new_game_button = gtk::Button::builder()
|
||||||
s.attach(&new_game_button, 2, 2, 1, 1);
|
.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({
|
new_game_button.connect_clicked({
|
||||||
move |_| {
|
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
|
s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
>k::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(>k::ColumnViewColumn::new(
|
||||||
|
Some("date"),
|
||||||
|
Some(make_factory(|g| g.date)),
|
||||||
|
));
|
||||||
|
list_view.append_column(>k::ColumnViewColumn::new(
|
||||||
|
Some("title"),
|
||||||
|
Some(make_factory(|g| g.name)),
|
||||||
|
));
|
||||||
|
list_view.append_column(>k::ColumnViewColumn::new(
|
||||||
|
Some("black"),
|
||||||
|
Some(make_factory(|g| g.black_player)),
|
||||||
|
));
|
||||||
|
list_view.append_column(>k::ColumnViewColumn::new(
|
||||||
|
Some("white"),
|
||||||
|
Some(make_factory(|g| g.white_player)),
|
||||||
|
));
|
||||||
|
list_view.append_column(>k::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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,20 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ -18,3 +29,72 @@ 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(>k::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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.68.2"
|
channel = "1.71.1"
|
||||||
targets = [ "wasm32-unknown-unknown" ]
|
targets = [ "wasm32-unknown-unknown" ]
|
||||||
|
|
|
@ -11,3 +11,6 @@ nom = { version = "7" }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
thiserror = { version = "1"}
|
thiserror = { version = "1"}
|
||||||
typeshare = { version = "1" }
|
typeshare = { version = "1" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
cool_asserts = { version = "2" }
|
||||||
|
|
|
@ -24,6 +24,16 @@ 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;
|
||||||
|
|
180
sgf/src/go.rs
180
sgf/src/go.rs
|
@ -74,6 +74,7 @@ use crate::{
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::ops::Deref;
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -84,11 +85,18 @@ pub struct Game {
|
||||||
pub tree: Tree,
|
pub tree: Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for Game {
|
||||||
|
type Target = Tree;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.tree
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<Tree> for Game {
|
impl TryFrom<Tree> for Game {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(tree: Tree) -> Result<Self, Self::Error> {
|
fn try_from(tree: Tree) -> Result<Self, Self::Error> {
|
||||||
let board_size = match tree.sequence[0].find_prop("SZ") {
|
let board_size = match tree.root.find_prop("SZ") {
|
||||||
Some(prop) => Size::try_from(prop.values[0].as_str())?,
|
Some(prop) => Size::try_from(prop.values[0].as_str())?,
|
||||||
None => Size {
|
None => Size {
|
||||||
width: 19,
|
width: 19,
|
||||||
|
@ -96,35 +104,37 @@ impl TryFrom<Tree> for Game {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let mut info = GameInfo::default();
|
let mut info = GameInfo::default();
|
||||||
info.app_name = tree.sequence[0]
|
info.app_name = tree.root.find_prop("AP").map(|prop| prop.values[0].clone());
|
||||||
.find_prop("AP")
|
|
||||||
.map(|prop| prop.values[0].clone());
|
|
||||||
info.black_player = tree.sequence[0]
|
|
||||||
.find_prop("PB")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.black_rank = tree.sequence[0]
|
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
|
||||||
|
.root
|
||||||
.find_prop("BR")
|
.find_prop("BR")
|
||||||
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
||||||
|
|
||||||
info.white_player = tree.sequence[0]
|
info.white_player = tree.root.find_prop("PW").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("PW")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.white_rank = tree.sequence[0]
|
info.white_rank = tree
|
||||||
|
.root
|
||||||
.find_prop("WR")
|
.find_prop("WR")
|
||||||
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
.and_then(|prop| Rank::try_from(prop.values[0].as_str()).ok());
|
||||||
|
|
||||||
info.result = tree.sequence[0]
|
info.result = tree
|
||||||
|
.root
|
||||||
.find_prop("RE")
|
.find_prop("RE")
|
||||||
.and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok());
|
.and_then(|prop| GameResult::try_from(prop.values[0].as_str()).ok());
|
||||||
|
|
||||||
info.time_limits = tree.sequence[0]
|
info.time_limits = tree
|
||||||
|
.root
|
||||||
.find_prop("TM")
|
.find_prop("TM")
|
||||||
.and_then(|prop| prop.values[0].parse::<u64>().ok())
|
.and_then(|prop| prop.values[0].parse::<u64>().ok())
|
||||||
.and_then(|seconds| Some(std::time::Duration::from_secs(seconds)));
|
.and_then(|seconds| Some(std::time::Duration::from_secs(seconds)));
|
||||||
|
|
||||||
info.date = tree.sequence[0]
|
info.date = tree
|
||||||
|
.root
|
||||||
.find_prop("DT")
|
.find_prop("DT")
|
||||||
.and_then(|prop| {
|
.and_then(|prop| {
|
||||||
let v = prop
|
let v = prop
|
||||||
|
@ -144,21 +154,13 @@ impl TryFrom<Tree> for Game {
|
||||||
})
|
})
|
||||||
.unwrap_or(vec![]);
|
.unwrap_or(vec![]);
|
||||||
|
|
||||||
info.event = tree.sequence[0]
|
info.event = tree.root.find_prop("EV").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("EV")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.round = tree.sequence[0]
|
info.round = tree.root.find_prop("RO").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("RO")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.source = tree.sequence[0]
|
info.source = tree.root.find_prop("SO").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("SO")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
info.game_keeper = tree.sequence[0]
|
info.game_keeper = tree.root.find_prop("US").map(|prop| prop.values.join(", "));
|
||||||
.find_prop("US")
|
|
||||||
.map(|prop| prop.values.join(", "));
|
|
||||||
|
|
||||||
Ok(Game {
|
Ok(Game {
|
||||||
board_size,
|
board_size,
|
||||||
|
@ -237,6 +239,7 @@ 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 {
|
||||||
|
@ -251,7 +254,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,
|
||||||
_ => panic!("unknown result format"),
|
_ => return Ok(GameResult::Unknown(parts[0].to_owned())),
|
||||||
};
|
};
|
||||||
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)),
|
||||||
|
@ -301,7 +304,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
date::Date,
|
date::Date,
|
||||||
tree::{parse_collection, Size},
|
tree::{parse_collection, Property, Size},
|
||||||
};
|
};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
@ -384,4 +387,123 @@ mod tests {
|
||||||
assert_eq!(tree.info.game_keeper, Some("Arno Hollosi".to_owned()));
|
assert_eq!(tree.info.game_keeper, Some("Arno Hollosi".to_owned()));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_presents_the_mainline_of_game_without_branches() {
|
||||||
|
with_file(
|
||||||
|
std::path::Path::new("test_data/2020 USGO DDK, Round 1.sgf"),
|
||||||
|
|trees| {
|
||||||
|
assert_eq!(trees.len(), 1);
|
||||||
|
let tree = &trees[0];
|
||||||
|
|
||||||
|
let node = &tree.root;
|
||||||
|
assert_eq!(node.properties.len(), 16);
|
||||||
|
let expected_properties = vec![
|
||||||
|
Property {
|
||||||
|
ident: "GM".to_owned(),
|
||||||
|
values: vec!["1".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "FF".to_owned(),
|
||||||
|
values: vec!["4".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "CA".to_owned(),
|
||||||
|
values: vec!["UTF-8".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "AP".to_owned(),
|
||||||
|
values: vec!["CGoban:3".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "ST".to_owned(),
|
||||||
|
values: vec!["2".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "RU".to_owned(),
|
||||||
|
values: vec!["AGA".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "SZ".to_owned(),
|
||||||
|
values: vec!["19".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "KM".to_owned(),
|
||||||
|
values: vec!["7.50".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "TM".to_owned(),
|
||||||
|
values: vec!["1800".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "OT".to_owned(),
|
||||||
|
values: vec!["5x30 byo-yomi".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "PW".to_owned(),
|
||||||
|
values: vec!["Geckoz".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "PB".to_owned(),
|
||||||
|
values: vec!["savanni".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "BR".to_owned(),
|
||||||
|
values: vec!["23k".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "DT".to_owned(),
|
||||||
|
values: vec!["2020-08-05".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "PC".to_owned(),
|
||||||
|
values: vec!["The KGS Go Server at http://www.gokgs.com/".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "RE".to_owned(),
|
||||||
|
values: vec!["W+17.50".to_owned()],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for i in 0..16 {
|
||||||
|
assert_eq!(node.properties[i], expected_properties[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = node.next().unwrap();
|
||||||
|
let expected_properties = vec![
|
||||||
|
Property {
|
||||||
|
ident: "B".to_owned(),
|
||||||
|
values: vec!["pp".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "BL".to_owned(),
|
||||||
|
values: vec!["1795.449".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["Geckoz [?]: Good game\nsavanni [23k?]: There we go! This UI is... tough.\nsavanni [23k?]: Have fun! Talk to you at the end.\nGeckoz [?]: Yeah, OGS is much better; I'm a UX professional\n".to_owned()],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for i in 0..3 {
|
||||||
|
assert_eq!(node.properties[i], expected_properties[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let node = node.next().unwrap();
|
||||||
|
let expected_properties = vec![
|
||||||
|
Property {
|
||||||
|
ident: "W".to_owned(),
|
||||||
|
values: vec!["dp".to_owned()],
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "WL".to_owned(),
|
||||||
|
values: vec!["1765.099".to_owned()],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for i in 0..2 {
|
||||||
|
assert_eq!(node.properties[i], expected_properties[i]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ pub mod go;
|
||||||
|
|
||||||
mod tree;
|
mod tree;
|
||||||
use tree::parse_collection;
|
use tree::parse_collection;
|
||||||
|
pub use tree::Property;
|
||||||
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ pub fn parse_sgf(input: &str) -> Result<Vec<Game>, Error> {
|
||||||
let (_, trees) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
|
let (_, trees) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
|
||||||
Ok(trees
|
Ok(trees
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| match t.sequence[0].find_prop("GM") {
|
.map(|t| match t.root.find_prop("GM") {
|
||||||
Some(prop) if prop.values == vec!["1".to_owned()] => {
|
Some(prop) if prop.values == vec!["1".to_owned()] => {
|
||||||
Game::Go(go::Game::try_from(t).expect("properly structured game tree"))
|
Game::Go(go::Game::try_from(t).expect("properly structured game tree"))
|
||||||
}
|
}
|
||||||
|
|
277
sgf/src/tree.rs
277
sgf/src/tree.rs
|
@ -5,7 +5,6 @@ use nom::{
|
||||||
character::complete::{alpha1, digit1, multispace0, multispace1, none_of},
|
character::complete::{alpha1, digit1, multispace0, multispace1, none_of},
|
||||||
combinator::{opt, value},
|
combinator::{opt, value},
|
||||||
multi::{many0, many1, separated_list1},
|
multi::{many0, many1, separated_list1},
|
||||||
sequence::delimited,
|
|
||||||
IResult,
|
IResult,
|
||||||
};
|
};
|
||||||
use std::num::ParseIntError;
|
use std::num::ParseIntError;
|
||||||
|
@ -54,29 +53,19 @@ impl TryFrom<&str> for Size {
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Tree {
|
pub struct Tree {
|
||||||
pub sequence: Vec<Node>,
|
pub root: Node,
|
||||||
pub sub_sequences: Vec<Tree>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Tree {
|
impl ToString for Tree {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
let sequence = self
|
format!("({})", self.root.to_string())
|
||||||
.sequence
|
|
||||||
.iter()
|
|
||||||
.map(|node| node.to_string())
|
|
||||||
.collect::<String>();
|
|
||||||
let subsequences = self
|
|
||||||
.sub_sequences
|
|
||||||
.iter()
|
|
||||||
.map(|seq| seq.to_string())
|
|
||||||
.collect::<String>();
|
|
||||||
format!("({}{})", sequence, subsequences)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct Node {
|
pub struct Node {
|
||||||
pub properties: Vec<Property>,
|
pub properties: Vec<Property>,
|
||||||
|
pub next: Vec<Node>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Node {
|
impl ToString for Node {
|
||||||
|
@ -86,7 +75,21 @@ impl ToString for Node {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|prop| prop.to_string())
|
.map(|prop| prop.to_string())
|
||||||
.collect::<String>();
|
.collect::<String>();
|
||||||
format!(";{}", props)
|
|
||||||
|
let next = if self.next.len() == 1 {
|
||||||
|
self.next
|
||||||
|
.iter()
|
||||||
|
.map(|node| node.to_string())
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("")
|
||||||
|
} else {
|
||||||
|
self.next
|
||||||
|
.iter()
|
||||||
|
.map(|node| format!("({})", node.to_string()))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("")
|
||||||
|
};
|
||||||
|
format!(";{}{}", props, next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +100,10 @@ impl Node {
|
||||||
.find(|prop| prop.ident == ident)
|
.find(|prop| prop.ident == ident)
|
||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn next<'a>(&'a self) -> Option<&'a Node> {
|
||||||
|
self.next.get(0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
@ -119,40 +126,40 @@ impl ToString for Property {
|
||||||
pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>(
|
pub fn parse_collection<'a, E: nom::error::ParseError<&'a str>>(
|
||||||
input: &'a str,
|
input: &'a str,
|
||||||
) -> IResult<&'a str, Vec<Tree>, E> {
|
) -> IResult<&'a str, Vec<Tree>, E> {
|
||||||
separated_list1(multispace1, parse_tree)(input)
|
let (input, roots) = separated_list1(multispace1, parse_tree)(input)?;
|
||||||
|
let trees = roots
|
||||||
|
.into_iter()
|
||||||
|
.map(|root| Tree { root })
|
||||||
|
.collect::<Vec<Tree>>();
|
||||||
|
|
||||||
|
Ok((input, trees))
|
||||||
}
|
}
|
||||||
|
|
||||||
// note: must preserve unknown properties
|
// note: must preserve unknown properties
|
||||||
// note: must fix or preserve illegally formatted game-info properties
|
// note: must fix or preserve illegally formatted game-info properties
|
||||||
// note: must correct or delete illegally foramtted properties, but display a warning
|
// note: must correct or delete illegally foramtted properties, but display a warning
|
||||||
fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Tree, E> {
|
fn parse_tree<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
|
||||||
let (input, _) = multispace0(input)?;
|
let (input, _) = multispace0(input)?;
|
||||||
delimited(tag("("), parse_sequence, tag(")"))(input)
|
let (input, _) = tag("(")(input)?;
|
||||||
}
|
let (input, node) = parse_node(input)?;
|
||||||
|
let (input, _) = multispace0(input)?;
|
||||||
|
let (input, _) = tag(")")(input)?;
|
||||||
|
|
||||||
fn parse_sequence<'a, E: nom::error::ParseError<&'a str>>(
|
Ok((input, node))
|
||||||
input: &'a str,
|
|
||||||
) -> IResult<&'a str, Tree, E> {
|
|
||||||
let (input, _) = multispace0(input)?;
|
|
||||||
let (input, nodes) = many1(parse_node)(input)?;
|
|
||||||
let (input, _) = multispace0(input)?;
|
|
||||||
let (input, sub_sequences) = many0(parse_tree)(input)?;
|
|
||||||
let (input, _) = multispace0(input)?;
|
|
||||||
|
|
||||||
Ok((
|
|
||||||
input,
|
|
||||||
Tree {
|
|
||||||
sequence: nodes,
|
|
||||||
sub_sequences,
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
|
fn parse_node<'a, E: nom::error::ParseError<&'a str>>(input: &'a str) -> IResult<&'a str, Node, E> {
|
||||||
let (input, _) = multispace0(input)?;
|
let (input, _) = multispace0(input)?;
|
||||||
let (input, _) = tag(";")(input)?;
|
let (input, _) = opt(tag(";"))(input)?;
|
||||||
let (input, properties) = many1(parse_property)(input)?;
|
let (input, properties) = many1(parse_property)(input)?;
|
||||||
Ok((input, Node { properties }))
|
|
||||||
|
let (input, next) = opt(parse_node)(input)?;
|
||||||
|
let (input, mut next_seq) = many0(parse_tree)(input)?;
|
||||||
|
|
||||||
|
let mut next = next.map(|n| vec![n]).unwrap_or(vec![]);
|
||||||
|
next.append(&mut next_seq);
|
||||||
|
|
||||||
|
Ok((input, Node { properties, next }))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_property<'a, E: nom::error::ParseError<&'a str>>(
|
fn parse_property<'a, E: nom::error::ParseError<&'a str>>(
|
||||||
|
@ -219,8 +226,6 @@ pub fn parse_size<'a, E: nom::error::ParseError<&'a str>>(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use std::{fs::File, io::Read};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c])
|
const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c])
|
||||||
|
@ -259,7 +264,8 @@ mod test {
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "B".to_owned(),
|
ident: "B".to_owned(),
|
||||||
values: vec!["ab".to_owned()]
|
values: vec!["ab".to_owned()]
|
||||||
}]
|
}],
|
||||||
|
next: vec![]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -273,6 +279,25 @@ mod test {
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "B".to_owned(),
|
ident: "B".to_owned(),
|
||||||
values: vec!["ab".to_owned()]
|
values: vec!["ab".to_owned()]
|
||||||
|
}],
|
||||||
|
next: vec![Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "W".to_owned(),
|
||||||
|
values: vec!["dp".to_owned()]
|
||||||
|
}],
|
||||||
|
next: vec![Node {
|
||||||
|
properties: vec![
|
||||||
|
Property {
|
||||||
|
ident: "B".to_owned(),
|
||||||
|
values: vec!["pq".to_owned()]
|
||||||
|
},
|
||||||
|
Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["some comments".to_owned()]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
next: vec![],
|
||||||
|
}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -286,21 +311,17 @@ mod test {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
sequence,
|
sequence,
|
||||||
Tree {
|
|
||||||
sequence: vec![
|
|
||||||
Node {
|
Node {
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "B".to_owned(),
|
ident: "B".to_owned(),
|
||||||
values: vec!["ab".to_owned()]
|
values: vec!["ab".to_owned()]
|
||||||
}]
|
}],
|
||||||
},
|
next: vec![Node {
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "W".to_owned(),
|
ident: "W".to_owned(),
|
||||||
values: vec!["dp".to_owned()]
|
values: vec!["dp".to_owned()]
|
||||||
}]
|
}],
|
||||||
},
|
next: vec![Node {
|
||||||
Node {
|
|
||||||
properties: vec![
|
properties: vec![
|
||||||
Property {
|
Property {
|
||||||
ident: "B".to_owned(),
|
ident: "B".to_owned(),
|
||||||
|
@ -310,114 +331,158 @@ mod test {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["some comments".to_owned()]
|
values: vec!["some comments".to_owned()]
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
sub_sequences: vec![],
|
next: vec![],
|
||||||
}
|
}]
|
||||||
|
}],
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_parse_a_sequence_with_subsequences() {
|
fn it_can_parse_a_branching_sequence() {
|
||||||
let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))";
|
let text = "(;C[a];C[b](;C[c])(;C[d];C[e]))";
|
||||||
let (_, sequence) = parse_tree::<nom::error::VerboseError<&str>>(text).unwrap();
|
let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(text).unwrap();
|
||||||
|
|
||||||
let main_sequence = vec![
|
let expected = Node {
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["a".to_owned()],
|
values: vec!["a".to_owned()],
|
||||||
}],
|
}],
|
||||||
},
|
next: vec![Node {
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["b".to_owned()],
|
values: vec!["b".to_owned()],
|
||||||
}],
|
}],
|
||||||
},
|
next: vec![
|
||||||
];
|
Node {
|
||||||
let subsequence_1 = Tree {
|
|
||||||
sequence: vec![Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["c".to_owned()],
|
values: vec!["c".to_owned()],
|
||||||
}],
|
}],
|
||||||
}],
|
next: vec![],
|
||||||
sub_sequences: vec![],
|
},
|
||||||
};
|
|
||||||
let subsequence_2 = Tree {
|
|
||||||
sequence: vec![
|
|
||||||
Node {
|
Node {
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["d".to_owned()],
|
values: vec!["d".to_owned()],
|
||||||
}],
|
}],
|
||||||
},
|
next: vec![Node {
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
properties: vec![Property {
|
||||||
ident: "C".to_owned(),
|
ident: "C".to_owned(),
|
||||||
values: vec!["e".to_owned()],
|
values: vec!["e".to_owned()],
|
||||||
}],
|
}],
|
||||||
|
next: vec![],
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
sub_sequences: vec![],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(tree, expected);
|
||||||
sequence,
|
|
||||||
Tree {
|
|
||||||
sequence: main_sequence,
|
|
||||||
sub_sequences: vec![subsequence_1, subsequence_2],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_parse_example_1() {
|
fn it_can_parse_example_1() {
|
||||||
let (_, ex_tree) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
|
let (_, tree) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
|
||||||
assert_eq!(ex_tree.sequence.len(), 1);
|
|
||||||
|
|
||||||
assert_eq!(ex_tree.sequence[0].properties.len(), 2);
|
let j = Node {
|
||||||
assert_eq!(
|
properties: vec![Property {
|
||||||
ex_tree.sequence[0].properties[0],
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["j".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![],
|
||||||
|
};
|
||||||
|
let i = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["i".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![],
|
||||||
|
};
|
||||||
|
let h = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["h".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![i],
|
||||||
|
};
|
||||||
|
let g = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["g".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![h],
|
||||||
|
};
|
||||||
|
let f = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["f".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![g, j],
|
||||||
|
};
|
||||||
|
let e = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["e".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![],
|
||||||
|
};
|
||||||
|
let d = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["d".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![e],
|
||||||
|
};
|
||||||
|
let c = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["c".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![],
|
||||||
|
};
|
||||||
|
let b = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["b".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![c, d],
|
||||||
|
};
|
||||||
|
let a = Node {
|
||||||
|
properties: vec![Property {
|
||||||
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["a".to_owned()],
|
||||||
|
}],
|
||||||
|
next: vec![b],
|
||||||
|
};
|
||||||
|
let expected = Node {
|
||||||
|
properties: vec![
|
||||||
Property {
|
Property {
|
||||||
ident: "FF".to_owned(),
|
ident: "FF".to_owned(),
|
||||||
values: vec!["4".to_owned()]
|
values: vec!["4".to_owned()],
|
||||||
}
|
},
|
||||||
);
|
Property {
|
||||||
assert_eq!(ex_tree.sub_sequences.len(), 2);
|
ident: "C".to_owned(),
|
||||||
|
values: vec!["root".to_owned()],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
next: vec![a, f],
|
||||||
|
};
|
||||||
|
|
||||||
assert_eq!(ex_tree.sub_sequences[0].sequence.len(), 2);
|
assert_eq!(tree, expected);
|
||||||
assert_eq!(
|
|
||||||
ex_tree.sub_sequences[0].sequence,
|
|
||||||
vec![
|
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
|
||||||
ident: "C".to_owned(),
|
|
||||||
values: vec!["a".to_owned()]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
Node {
|
|
||||||
properties: vec![Property {
|
|
||||||
ident: "C".to_owned(),
|
|
||||||
values: vec!["b".to_owned()]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(ex_tree.sub_sequences[0].sub_sequences.len(), 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_regenerate_the_tree() {
|
fn it_can_regenerate_the_tree() {
|
||||||
let (_, tree1) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
|
let (_, tree1) = parse_tree::<nom::error::VerboseError<&str>>(EXAMPLE).unwrap();
|
||||||
|
let tree1 = Tree { root: tree1 };
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tree1.to_string(),
|
tree1.to_string(),
|
||||||
"(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]))(;C[f](;C[g];C[h];C[i])(;C[j])))"
|
"(;FF[4]C[root](;C[a];C[b](;C[c])(;C[d];C[e]))(;C[f](;C[g];C[h];C[i])(;C[j])))"
|
||||||
);
|
);
|
||||||
let (_, tree2) = parse_tree::<nom::error::VerboseError<&str>>(&tree1.to_string()).unwrap();
|
let (_, tree2) = parse_tree::<nom::error::VerboseError<&str>>(&tree1.to_string()).unwrap();
|
||||||
assert_eq!(tree1, tree2);
|
assert_eq!(tree1, Tree { root: tree2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in New Issue