Compare commits

...

3 Commits

Author SHA1 Message Date
Savanni D'Gerinel 56a8133dd5 A lot of work, possibly meaningless, to try to handle state within the main window
A note to self, when I return to this: the overlay modals don't make
sense in this context. The main window should have views, and switching
from one view to the next should involve just replacing the child.
Modals can be put off until later.
2023-12-18 17:49:25 -05:00
Savanni D'Gerinel 29dc81a991 Set up a database selector row that can dispatch operations when a database file gets selected 2023-12-18 17:17:39 -05:00
Savanni D'Gerinel 5668f1d7b2 Style up the modal and add the database file chooser widget 2023-12-18 12:44:12 -05:00
4 changed files with 213 additions and 53 deletions

View File

@ -11,7 +11,7 @@ emseries = { path = "../../emseries" }
ft-core = { path = "../core" } ft-core = { path = "../core" }
gio = { version = "0.18" } gio = { version = "0.18" }
glib = { version = "0.18" } glib = { version = "0.18" }
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] } gtk = { version = "0.7", package = "gtk4", features = [ "v4_10" ] }
tokio = { version = "1.34", features = [ "full" ] } tokio = { version = "1.34", features = [ "full" ] }
[build-dependencies] [build-dependencies]

View File

@ -0,0 +1,21 @@
.modal {
margin: 64px;
background-color: @view_bg_color;
border: 1px solid grey;
border-radius: 10px
}
.modal-title {
font-size: larger;
padding: 8px;
background-color: @headerbar_bg_color;
border-bottom: 1px solid @headerbar_border_color;
border-radius: 10px 10px 0px 0px;
}
.modal-content {
padding: 8px;
}
.modal-footer {
}

View File

@ -23,6 +23,7 @@ use gio::resources_lookup_data;
use glib::Object; use glib::Object;
use gtk::{subclass::prelude::*, STYLE_PROVIDER_PRIORITY_USER}; use gtk::{subclass::prelude::*, STYLE_PROVIDER_PRIORITY_USER};
use std::{ use std::{
cell::RefCell,
env, env,
sync::{Arc, RwLock}, sync::{Arc, RwLock},
}; };
@ -136,7 +137,7 @@ enum MainView {
Unconfigured(UnconfiguredView), Unconfigured(UnconfiguredView),
/// The Historical view shows a history of records and whatnot. /// The Historical view shows a history of records and whatnot.
HistoricalView(HistoricalView), Historical(HistoricalView),
} }
/// The application window, or the main window, is the main user interface for the app. /// The application window, or the main window, is the main user interface for the app.
@ -144,7 +145,12 @@ struct AppWindow {
app: App, app: App,
window: adw::ApplicationWindow, window: adw::ApplicationWindow,
overlay: gtk::Overlay, overlay: gtk::Overlay,
current_view: MainView,
current_view: RefCell<MainView>,
// We have to keep around a reference to the modal so that we know what to remove from the
// overlay.
modal: RefCell<Option<gtk::Widget>>,
} }
impl AppWindow { impl AppWindow {
@ -155,6 +161,46 @@ impl AppWindow {
/// ///
/// app is a core [App] object which encapsulates all of the basic logic. /// app is a core [App] object which encapsulates all of the basic logic.
fn new(adw_app: &adw::Application, app: App) -> AppWindow { fn new(adw_app: &adw::Application, app: App) -> AppWindow {
let window = adw::ApplicationWindow::builder()
.application(adw_app)
.width_request(800)
.height_request(600)
.build();
// GTK overlays aren't all that well documented. The Overlay object needs to be the
// content/child of the window. The main content should then be added to the overlay as
// `set_child`. The overlays/modals should be added as `add_overlay` and then removed with
// `remove_overlay`.
let overlay = gtk::Overlay::new();
window.set_content(Some(&overlay));
let current_view = if app.database.read().unwrap().is_none() {
let view = UnconfiguredView::new();
/*
overlay.set_child(Some(&view));
// I have to access the overlay directly here because I haven't fully constructed Self
// yet, and so I don't have access to `open_modal` yet.
*/
MainView::Unconfigured(view)
} else {
let view = HistoricalView::new();
/*
overlay.set_child(Some(&view));
*/
MainView::Historical(view)
};
let s = Self {
app,
window: window.clone(),
overlay,
current_view: RefCell::new(current_view),
modal: RefCell::new(None),
};
let stylesheet = String::from_utf8( let stylesheet = String::from_utf8(
resources_lookup_data( resources_lookup_data(
&format!("{}style.css", RESOURCE_BASE_PATH), &format!("{}style.css", RESOURCE_BASE_PATH),
@ -168,30 +214,28 @@ impl AppWindow {
let provider = gtk::CssProvider::new(); let provider = gtk::CssProvider::new();
provider.load_from_data(&stylesheet); provider.load_from_data(&stylesheet);
let window = adw::ApplicationWindow::builder()
.application(adw_app)
.width_request(800)
.height_request(600)
.build();
let context = window.style_context(); let context = window.style_context();
context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER); context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER);
window.present(); window.present();
// GTK overlays aren't all that well documented. The Overlay object needs to be the s.redraw();
// content/child of the window. The main content should then be added to the overlay as
// `add_overlay`. The overlays/modals should be added as `set_child`.
let overlay = gtk::Overlay::new();
window.set_content(Some(&overlay));
s
/*
let current_view = if app.database.read().unwrap().is_none() { let current_view = if app.database.read().unwrap().is_none() {
let view = UnconfiguredView::new(); let view = UnconfiguredView::new();
overlay.set_child(Some(&view)); overlay.set_child(Some(&view));
// I have to access the overlay directly here because I haven't fully constructed Self // I have to access the overlay directly here because I haven't fully constructed Self
// yet, and so I don't have access to `open_modal` yet. // yet, and so I don't have access to `open_modal` yet.
overlay.add_overlay(&welcome_modal()); overlay.add_overlay(&welcome_modal(|path| {
// When a path gets selected, I want to set the path in the configuration system,
// and I want to open the database. After that, this window should be redrawn with
// its new state. So the view state also needs to change.
println!("path: {}", path.to_str().unwrap())
}));
MainView::Unconfigured(view) MainView::Unconfigured(view)
} else { } else {
@ -200,25 +244,27 @@ impl AppWindow {
MainView::HistoricalView(view) MainView::HistoricalView(view)
}; };
*/
}
Self { fn redraw(&self) {
app, match *self.current_view.borrow() {
window, MainView::Unconfigured(ref view) => {
overlay, self.overlay.set_child(Some(view));
current_view,
let modal = welcome_modal(|path| {
// When a path gets selected, I want to set the path in the configuration system,
// and I want to open the database. After that, this window should be redrawn with
// its new state. So the view state also needs to change.
println!("path: {}", path.to_str().unwrap())
});
self.overlay.add_overlay(&modal);
*self.modal.borrow_mut() = Some(modal.upcast());
}
MainView::Historical(ref view) => self.overlay.set_child(Some(view)),
} }
} }
/// Use [modal] as a modal overlay of the application window.
fn open_modal(&self, modal: Modal) {
self.overlay.set_child(Some(&modal));
}
/// Close the modal by discarding the component.
fn close_modal(&self) {
let none: Option<&gtk::Widget> = None;
self.overlay.set_child(none);
}
} }
fn main() { fn main() {

View File

@ -3,7 +3,11 @@
//! component. //! component.
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use std::cell::RefCell; use std::{
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
};
pub struct ModalPrivate { pub struct ModalPrivate {
title: gtk::Label, title: gtk::Label,
@ -22,14 +26,17 @@ impl ObjectSubclass for ModalPrivate {
type ParentType = gtk::Box; type ParentType = gtk::Box;
fn new() -> Self { fn new() -> Self {
let title = gtk::Label::builder().label("Modal").build(); let title = gtk::Label::builder()
.label("Modal")
.css_classes(["modal-title"])
.build();
let content = gtk::Box::new(gtk::Orientation::Vertical, 0); let content = gtk::Box::new(gtk::Orientation::Vertical, 0);
let actions = gtk::Box::new(gtk::Orientation::Horizontal, 0);
let primary_action = gtk::Button::builder().label("Primary").build(); let primary_action = gtk::Button::builder().label("Primary").build();
let footer = gtk::Box::builder() let footer = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal) .orientation(gtk::Orientation::Horizontal)
.hexpand(true) .hexpand(true)
.css_classes(["modal-footer"])
.build(); .build();
footer.append(&primary_action); footer.append(&primary_action);
@ -56,11 +63,8 @@ glib::wrapper! {
impl Modal { impl Modal {
pub fn new() -> Self { pub fn new() -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
s.set_css_classes(&["modal"]);
s.set_margin_start(100);
s.set_margin_end(100);
s.set_margin_top(100);
s.set_margin_bottom(100);
s.set_orientation(gtk::Orientation::Vertical); s.set_orientation(gtk::Orientation::Vertical);
s.append(&s.imp().title); s.append(&s.imp().title);
@ -76,6 +80,9 @@ impl Modal {
pub fn set_content(&self, content: gtk::Widget) { pub fn set_content(&self, content: gtk::Widget) {
self.remove(&*self.imp().content.borrow()); self.remove(&*self.imp().content.borrow());
content.add_css_class("modal-content");
content.set_hexpand(true);
content.set_vexpand(true);
self.insert_child_after(&content, Some(&self.imp().title)); self.insert_child_after(&content, Some(&self.imp().title));
*self.imp().content.borrow_mut() = content; *self.imp().content.borrow_mut() = content;
} }
@ -95,7 +102,10 @@ impl Modal {
/// database has not been configured yet. /// database has not been configured yet.
/// ///
/// This is a [Modal] component with all of the welcome content. /// This is a [Modal] component with all of the welcome content.
pub fn welcome_modal() -> Modal { pub fn welcome_modal<F>(database_selected: F) -> Modal
where
F: Fn(&Path) + 'static,
{
let modal = Modal::new(); let modal = Modal::new();
modal.set_title("Welcome to FitnessTrax"); modal.set_title("Welcome to FitnessTrax");
@ -105,23 +115,106 @@ pub fn welcome_modal() -> Modal {
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)
.build(); .build();
content.append(&gtk::Label::new(Some("Welcome to FitnessTrax. The application has not yet been configured, so I will walk you through that. Let's start out by selecting your database."))); let instructions = gtk::Label::builder()
.wrap(true)
.label("The application has not yet been configured, so I will walk you through that. Fortunately, it is very easy, and we only need to select the location for your database.").build();
content.append(&instructions);
// The database selection row should be a box that shows a default database path, along with a let db_row = DatabaseFileChooserRow::new(database_selected);
// button that triggers a file chooser dialog. Once the dialog returns, the box should be content.append(&db_row);
// updated to reflect the chosen path.
let db_row = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
db_row.append(
&gtk::Label::builder()
.label("No Path Selected")
.hexpand(true)
.build(),
);
db_row.append(&gtk::Button::builder().label("Select Database").build());
modal.set_content(content.upcast()); modal.set_content(content.upcast());
modal.set_primary_action(gtk::Button::builder().label("Save Settings").build()); modal.set_primary_action(gtk::Button::builder().label("Save Settings").build());
modal modal
} }
pub struct DatabaseFileChooserRowPrivate {
path: RefCell<Option<PathBuf>>,
label: gtk::Label,
on_selected_: RefCell<Box<dyn Fn(&Path)>>,
}
#[glib::object_subclass]
impl ObjectSubclass for DatabaseFileChooserRowPrivate {
const NAME: &'static str = "DatabaseFileChooser";
type Type = DatabaseFileChooserRow;
type ParentType = gtk::Box;
fn new() -> Self {
Self {
path: RefCell::new(None),
label: gtk::Label::builder().hexpand(true).build(),
on_selected_: RefCell::new(Box::new(|_| {})),
}
}
}
impl ObjectImpl for DatabaseFileChooserRowPrivate {}
impl WidgetImpl for DatabaseFileChooserRowPrivate {}
impl BoxImpl for DatabaseFileChooserRowPrivate {}
glib::wrapper! {
pub struct DatabaseFileChooserRow(ObjectSubclass<DatabaseFileChooserRowPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
}
impl DatabaseFileChooserRow {
pub fn new<F>(database_selected: F) -> Self
where
F: Fn(&Path) + 'static,
{
let s: Self = Object::builder().build();
s.set_orientation(gtk::Orientation::Horizontal);
// The database selection row should be a box that shows a default database path, along with a
// button that triggers a file chooser dialog. Once the dialog returns, the box should be
// updated to reflect the chosen path.
s.imp().label.set_text("No database selected");
*s.imp().on_selected_.borrow_mut() = Box::new(database_selected);
let db_file_chooser_button = gtk::Button::builder().label("Select Database").build();
db_file_chooser_button.connect_clicked({
let s = s.clone();
move |_| {
let no_window: Option<&gtk::Window> = None;
let not_cancellable: Option<&gio::Cancellable> = None;
let s = s.clone();
gtk::FileDialog::builder().build().open(
no_window,
not_cancellable,
move |file_id| s.on_selected(file_id),
);
}
});
s.append(&s.imp().label);
s.append(&db_file_chooser_button);
s
}
fn on_selected(&self, m_file_id: Result<gio::File, glib::Error>) {
match m_file_id {
Ok(file_id) => {
println!("The user selected {:?}", file_id.path());
*self.imp().path.borrow_mut() = file_id.path();
match *self.imp().path.borrow() {
Some(ref path) => {
(*self.imp().on_selected_.borrow())(path);
self.redraw();
}
None => {}
}
}
Err(err) => println!("file opening failed: {}", err),
}
}
fn redraw(&self) {
match *self.imp().path.borrow() {
Some(ref path) => self.imp().label.set_text(path.to_str().unwrap()),
None => self.imp().label.set_text("No database selected"),
}
}
}