Set up a configuration UI #66
|
@ -1348,6 +1348,7 @@ dependencies = [
|
||||||
"gtk4",
|
"gtk4",
|
||||||
"image",
|
"image",
|
||||||
"kifu-core",
|
"kifu-core",
|
||||||
|
"libadwaita",
|
||||||
"sgf",
|
"sgf",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,22 +1,34 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
types::{AppState, Config, DatabasePath, 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},
|
||||||
};
|
};
|
||||||
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 {
|
||||||
|
@ -57,13 +69,15 @@ 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>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,25 +88,23 @@ impl CoreApp {
|
||||||
let db_path: DatabasePath = config.get().unwrap();
|
let db_path: DatabasePath = config.get().unwrap();
|
||||||
let state = Arc::new(RwLock::new(AppState::new(db_path)));
|
let state = Arc::new(RwLock::new(AppState::new(db_path)));
|
||||||
|
|
||||||
Self { config, state }
|
Self {
|
||||||
|
config: Arc::new(RwLock::new(config)),
|
||||||
|
state,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
||||||
match request {
|
match request {
|
||||||
/*
|
CoreRequest::ChangeSetting(request) => match request {
|
||||||
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 = {
|
||||||
|
@ -116,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();
|
||||||
|
|
|
@ -39,11 +39,16 @@ 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) {
|
||||||
match sgf {
|
Ok(sgfs) => {
|
||||||
Game::Go(game) => games.push(game),
|
for sgf in sgfs {
|
||||||
Game::Unsupported(_) => {}
|
match sgf {
|
||||||
|
Game::Go(game) => games.push(game),
|
||||||
|
Game::Unsupported(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@ 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;
|
||||||
|
|
|
@ -16,7 +16,7 @@ define_config! {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
|
||||||
pub struct DatabasePath(PathBuf);
|
pub struct DatabasePath(pub PathBuf);
|
||||||
|
|
||||||
impl std::ops::Deref for DatabasePath {
|
impl std::ops::Deref for DatabasePath {
|
||||||
type Target = PathBuf;
|
type Target = PathBuf;
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
|
@ -50,6 +50,7 @@ impl GamePreviewElement {
|
||||||
Some(GameResult::Draw) => "Draw".to_owned(),
|
Some(GameResult::Draw) => "Draw".to_owned(),
|
||||||
Some(GameResult::Black(ref win)) => format!("Black by {}", format_win(win)),
|
Some(GameResult::Black(ref win)) => format!("Black by {}", format_win(win)),
|
||||||
Some(GameResult::White(ref win)) => format!("White by {}", format_win(win)),
|
Some(GameResult::White(ref win)) => format!("White by {}", format_win(win)),
|
||||||
|
Some(GameResult::Unknown(ref text)) => format!("Unknown: {}", text),
|
||||||
None => "".to_owned(),
|
None => "".to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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,5 +1,8 @@
|
||||||
|
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};
|
||||||
|
|
|
@ -9,6 +9,7 @@ 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" }
|
||||||
|
|
|
@ -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::{ConfigurationPage, Home, Layout, 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, layout: Layout, message: CoreResponse) {
|
||||||
let playing_field = Arc::new(RwLock::new(None));
|
let playing_field = Arc::new(RwLock::new(None));
|
||||||
match message {
|
match message {
|
||||||
|
CoreResponse::ConfigurationView(view) => perftrace("ConfigurationView", || {
|
||||||
|
let config_page = ConfigurationPage::new(api, view);
|
||||||
|
|
||||||
|
let window = adw::PreferencesWindow::new();
|
||||||
|
window.add(&config_page);
|
||||||
|
window.set_visible_page(&config_page);
|
||||||
|
window.present();
|
||||||
|
}),
|
||||||
CoreResponse::HomeView(view) => perftrace("HomeView", || {
|
CoreResponse::HomeView(view) => perftrace("HomeView", || {
|
||||||
let api = api.clone();
|
let api = api.clone();
|
||||||
|
|
||||||
let home = Home::new(api, view);
|
let home = Home::new(api, view);
|
||||||
window.set_child(Some(&home));
|
layout.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));
|
layout.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({
|
||||||
|
@ -81,14 +93,31 @@ fn main() {
|
||||||
core: core.clone(),
|
core: core.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let window = gtk::ApplicationWindow::new(app);
|
let action_config = gio::SimpleAction::new("show-config", None);
|
||||||
|
action_config.connect_activate({
|
||||||
|
let api = api.clone();
|
||||||
|
move |_, _| {
|
||||||
|
api.dispatch(CoreRequest::OpenConfiguration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.add_action(&action_config);
|
||||||
|
|
||||||
|
let window = adw::ApplicationWindow::builder()
|
||||||
|
.application(app)
|
||||||
|
.width_request(800)
|
||||||
|
.height_request(500)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let layout = Layout::new();
|
||||||
|
window.set_content(Some(&layout));
|
||||||
|
|
||||||
window.present();
|
window.present();
|
||||||
|
|
||||||
gtk_rx.attach(None, {
|
gtk_rx.attach(None, {
|
||||||
let api = api.clone();
|
let api = api.clone();
|
||||||
move |message| {
|
move |message| {
|
||||||
perftrace("handle_response", || {
|
perftrace("handle_response", || {
|
||||||
handle_response(api.clone(), window.clone(), message)
|
handle_response(api.clone(), layout.clone(), 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ impl GamePreview {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
s.set_orientation(gtk::Orientation::Horizontal);
|
s.set_orientation(gtk::Orientation::Horizontal);
|
||||||
s.set_homogeneous(true);
|
s.set_homogeneous(true);
|
||||||
s.set_hexpand(true);
|
s.set_hexpand(false);
|
||||||
|
|
||||||
s.append(&s.imp().date);
|
s.append(&s.imp().date);
|
||||||
s.append(&s.imp().title);
|
s.append(&s.imp().title);
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
// Deprecated even from day 1. We want to use ToolbarView as soon as it's available in the versions
|
||||||
|
// of Libadwaita available in NixOS.
|
||||||
|
pub struct LayoutPrivate {
|
||||||
|
pub header: adw::HeaderBar,
|
||||||
|
pub content: Rc<RefCell<gtk::Widget>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LayoutPrivate {
|
||||||
|
fn default() -> Self {
|
||||||
|
let header = adw::HeaderBar::builder()
|
||||||
|
.title_widget(>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::StatusPage::builder().title("Nothing here").build();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
header,
|
||||||
|
content: Rc::new(RefCell::new(content.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for LayoutPrivate {
|
||||||
|
const NAME: &'static str = "Layout";
|
||||||
|
type Type = Layout;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for LayoutPrivate {}
|
||||||
|
impl WidgetImpl for LayoutPrivate {}
|
||||||
|
impl BoxImpl for LayoutPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct Layout(ObjectSubclass<LayoutPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Layout {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
s.set_orientation(gtk::Orientation::Vertical);
|
||||||
|
s.set_homogeneous(false);
|
||||||
|
s.append(&s.imp().header);
|
||||||
|
s.append(&*s.imp().content.borrow());
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_content(&self, content: &impl IsA<gtk::Widget>) {
|
||||||
|
let mut widget = self.imp().content.borrow_mut();
|
||||||
|
|
||||||
|
self.remove(&*widget);
|
||||||
|
*widget = content.clone().upcast::<gtk::Widget>();
|
||||||
|
self.append(&*widget);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,18 @@
|
||||||
mod chat;
|
mod chat;
|
||||||
pub use chat::Chat;
|
pub use chat::Chat;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
pub use config::ConfigurationPage;
|
||||||
|
|
||||||
mod game_preview;
|
mod game_preview;
|
||||||
pub use game_preview::GamePreview;
|
pub use game_preview::GamePreview;
|
||||||
|
|
||||||
mod library;
|
mod library;
|
||||||
pub use library::Library;
|
pub use library::Library;
|
||||||
|
|
||||||
|
mod layout;
|
||||||
|
pub use layout::Layout;
|
||||||
|
|
||||||
mod player_card;
|
mod player_card;
|
||||||
pub use player_card::PlayerCard;
|
pub use player_card::PlayerCard;
|
||||||
|
|
||||||
|
|
|
@ -242,6 +242,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 {
|
||||||
|
@ -256,7 +257,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)),
|
||||||
|
|
Loading…
Reference in New Issue