monorepo/otg/gtk/src/app_window.rs

234 lines
7.9 KiB
Rust

/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of On the Grid.
On the Grid is free software: you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::{CoreApi, ResourceManager};
use adw::prelude::*;
use glib::Propagation;
use gtk::{gdk::Key, EventControllerKey};
use otg_core::{
settings::{SettingsRequest, SettingsResponse},
CoreRequest, CoreResponse, GameReviewViewModel,
};
use sgf::GameRecord;
use std::sync::{Arc, RwLock};
use crate::views::{GameReview, HomeView, SettingsView};
/*
#[derive(Clone)]
enum AppView {
Home,
}
*/
// An application window should generally contain
// - an overlay widget
// - the main content in a stack on the bottom panel of the overlay
// - the settings and the about page in bins atop the overlay
#[derive(Clone)]
pub struct AppWindow {
pub window: adw::ApplicationWindow,
// content is a stack which contains the view models for the application. These are the main
// elements that users want to interact with: the home page, the game library, a review, a game
// itself, perhaps also chat rooms and player lists on other networks. stack contains the
// widgets that need to be rendered. The two of these work together in order to ensure that
// we can maintain the state of previous views. Since the two of these work together, they are
// a candidate for extraction into a new widget or a new struct.
stack: adw::NavigationView,
// view_states: Vec<AppView>,
// Overlays are for transient content, such as about and settings, which can be accessed from
// anywhere but shouldn't be part of the main application flow.
overlay: gtk::Overlay,
core: CoreApi,
// Not liking this, but I have to keep track of the settings view model separately from
// anything else. I'll have to look into this later.
settings_view_model: Arc<RwLock<Option<SettingsView>>>,
resources: ResourceManager,
}
impl AppWindow {
pub fn new(app: &adw::Application, core: CoreApi, resources: ResourceManager) -> Self {
let window = Self::setup_window(app);
let overlay = Self::setup_overlay();
let stack = adw::NavigationView::new();
// let view_states = vec![];
window.set_content(Some(&overlay));
overlay.set_child(Some(&stack));
let s = Self {
window,
stack,
// view_states,
overlay,
core,
settings_view_model: Default::default(),
resources,
};
let home = s.setup_home();
s.stack.push(&home);
s
}
pub fn open_game_review(&self, game_record: GameRecord) {
let header = adw::HeaderBar::new();
let game_review = GameReview::new(
GameReviewViewModel::new(game_record),
self.resources.clone(),
);
let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
layout.append(&header);
layout.append(&game_review.widget());
// This controller ensures that navigational keypresses get sent to the game review so that
// they're not changing the cursor focus in the app.
let keypress_controller = EventControllerKey::new();
keypress_controller.connect_key_pressed({
move |s, key, _, _| {
println!("layout keypress: {}", key);
if s.forward(&game_review.widget()) {
Propagation::Stop
} else {
Propagation::Proceed
}
}
});
layout.add_controller(keypress_controller);
let page = adw::NavigationPage::builder()
.can_pop(true)
.title("Game Review")
.child(&layout)
.build();
self.stack.push(&page);
}
pub fn open_settings(&self) {
// This should return instantly and allow the UI to continue being functional. However,
// some tests indicate that this may not actually be working correctly, and that a
// long-running background thread may delay things.
glib::spawn_future_local({
let s = self.clone();
async move {
if let CoreResponse::Settings(SettingsResponse(settings)) = s
.core
.dispatch(CoreRequest::Settings(SettingsRequest::Get))
.await
{
let view_model = SettingsView::new(
&s.window,
settings,
{
let s = s.clone();
move |config| {
glib::spawn_future_local({
let s = s.clone();
async move {
s.core
.dispatch(CoreRequest::Settings(SettingsRequest::Set(
config,
)))
.await
}
});
s.close_overlay();
}
},
{
let s = s.clone();
move || {
s.close_overlay();
}
},
);
s.overlay.add_overlay(&view_model);
*s.settings_view_model.write().unwrap() = Some(view_model);
}
}
});
}
pub fn close_overlay(&self) {
let mut view = self.settings_view_model.write().unwrap();
if let Some(ref mut settings) = *view {
self.overlay.remove_overlay(settings);
*view = None;
}
}
fn setup_window(app: &adw::Application) -> adw::ApplicationWindow {
adw::ApplicationWindow::builder()
.application(app)
.width_request(800)
.height_request(500)
.build()
}
fn setup_header() -> adw::HeaderBar {
let header = adw::HeaderBar::builder()
.title_widget(&gtk::Label::new(Some("On the Grid")))
.build();
let app_menu = gio::Menu::new();
let menu_item = gio::MenuItem::new(Some("Configuration"), Some("app.show_settings"));
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);
header
}
fn setup_overlay() -> gtk::Overlay {
gtk::Overlay::new()
}
fn setup_home(&self) -> adw::NavigationPage {
let header = Self::setup_header();
let home = HomeView::new(self.core.clone(), {
let s = self.clone();
move |game| s.open_game_review(game)
});
let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
layout.append(&header);
layout.append(&home);
adw::NavigationPage::builder()
.can_pop(false)
.title("Home")
.child(&layout)
.build()
}
}