mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2026-11-03 12:50:31 -06:00
feat: detachable pages (#1203)
* wip * refactor * markup fixes * use static sidebar min-width * upd spacing * move detachable page * toggle detach on sidebar's elm icon click * simplify wording * fix * small cleanup * clippy * small ref * use enum for pageId * replace set_transient_for with set_application * show detach button for selected page * shrink icon
This commit is contained in:
1 parent
38874d1301
commit
a2323301c5
7 files changed
+417
-43
No files matched your search
@@ -7,6 +7,10 @@ oc-page = Overclocking
|
||||
thermals-page = Thermals
|
||||
software-page = Software Info
|
||||
displays-page = Display Info
|
||||
detach-page = Detach {$page}
|
||||
page-detached = This page is open in another window.
|
||||
show-page-window = Focus Window
|
||||
reattach-page = Return to Main Window
|
||||
|
||||
hardware-info = Hardware Information
|
||||
|
||||
|
||||
@@ -13,3 +13,18 @@
|
||||
.main-sidebar-container .sidebar-section {
|
||||
margin: 4px 8px 8px 8px;
|
||||
}
|
||||
|
||||
.page-detach-button {
|
||||
-gtk-icon-size: 12px;
|
||||
min-width: 26px;
|
||||
min-height: 26px;
|
||||
padding: 0;
|
||||
margin: 4px 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.navigation-sidebar row:hover .page-detach-button,
|
||||
.navigation-sidebar row:selected .page-detach-button,
|
||||
.page-detach-button:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
+63
-42
@@ -1,10 +1,12 @@
|
||||
mod about_dialog;
|
||||
pub(crate) mod components;
|
||||
mod detachable_page;
|
||||
mod gpu_selector;
|
||||
pub mod graphs_window;
|
||||
mod info_dialog;
|
||||
pub(crate) mod msg;
|
||||
mod overdrive_dialog;
|
||||
mod page_navigation;
|
||||
pub(crate) mod pages;
|
||||
mod preferences_dialog;
|
||||
mod process_monitor;
|
||||
@@ -49,8 +51,9 @@ use lact_schema::{
|
||||
request::{ConfirmCommand, ProfileBase, SetClocksCommand},
|
||||
};
|
||||
use msg::AppMsg;
|
||||
use page_navigation::{PageNavigation, PageNavigationInit, PageNavigationMsg};
|
||||
use pages::{
|
||||
PageUpdate,
|
||||
PageId, PageUpdate,
|
||||
crash_page::CrashPage,
|
||||
info_page::InformationPage,
|
||||
oc_page::{OcPage, OcPageMsg},
|
||||
@@ -85,7 +88,6 @@ pub(crate) static APP_BROKER: MessageBroker<AppMsg> = MessageBroker::new();
|
||||
|
||||
const PROCESS_POLL_INTERVAL_MS: u64 = 1500;
|
||||
const NVIDIA_RECOMMENDED_MIN_VERSION: u32 = 560;
|
||||
const CONTENT_MAXIMUM_WIDTH: i32 = 1200;
|
||||
const DEFAULT_WINDOW_WIDTH: i32 = 1100;
|
||||
const DEFAULT_WINDOW_HEIGHT: i32 = 750;
|
||||
const CONFIRM_RESPONSE_APPLY: &str = "confirm";
|
||||
@@ -117,6 +119,7 @@ pub struct AppModel {
|
||||
software_page: relm4::Controller<SoftwarePage>,
|
||||
displays_page: relm4::Controller<DisplaysPage>,
|
||||
crash_page: relm4::Controller<CrashPage>,
|
||||
page_navigation: relm4::Controller<PageNavigation>,
|
||||
service_setup_dialog: Option<AsyncController<ServiceSetupDialog>>,
|
||||
|
||||
gpu_selector: relm4::Controller<GpuSelector>,
|
||||
@@ -183,6 +186,7 @@ impl AsyncComponent for AppModel {
|
||||
#[name = "navbar"]
|
||||
adw::NavigationSplitView {
|
||||
set_expand: true,
|
||||
set_min_sidebar_width: 230.0,
|
||||
set_max_sidebar_width: 230.0,
|
||||
|
||||
#[wrap(Some)]
|
||||
@@ -209,11 +213,7 @@ impl AsyncComponent for AppModel {
|
||||
|
||||
gtk::Separator {},
|
||||
|
||||
gtk::StackSidebar {
|
||||
set_margin_vertical: 1,
|
||||
set_stack: &root_stack,
|
||||
set_vexpand: true,
|
||||
},
|
||||
model.page_navigation.widget(),
|
||||
|
||||
gtk::Separator {},
|
||||
|
||||
@@ -281,43 +281,26 @@ impl AsyncComponent for AppModel {
|
||||
},
|
||||
|
||||
#[wrap(Some)]
|
||||
set_content = >k::ScrolledWindow {
|
||||
set_hscrollbar_policy: gtk::PolicyType::Never,
|
||||
#[name = "root_stack"]
|
||||
set_content = &model.page_navigation.widgets().stack.clone() -> gtk::Stack {
|
||||
add_binding: (&model.ui_sensitive, "sensitive"),
|
||||
|
||||
adw::Clamp {
|
||||
set_maximum_size: CONTENT_MAXIMUM_WIDTH,
|
||||
set_tightening_threshold: CONTENT_MAXIMUM_WIDTH,
|
||||
add_named[Some(PageId::Crash.as_str())] = model.crash_page.widget(),
|
||||
|
||||
#[name = "root_stack"]
|
||||
gtk::Stack {
|
||||
set_vexpand: false,
|
||||
set_vhomogeneous: false,
|
||||
set_visible_child_name: &CONFIG.read().selected_tab,
|
||||
connect_visible_child_name_notify[content_page] => move |stack| {
|
||||
if let Some(child) = stack.visible_child() {
|
||||
let page = stack.page(&child);
|
||||
content_page.set_title(&page.title().unwrap_or_default());
|
||||
|
||||
add_binding: (&model.ui_sensitive, "sensitive"),
|
||||
|
||||
add_titled[Some("info_page"), &fl!(I18N, "info-page")] = model.info_page.widget(),
|
||||
add_titled[Some("oc_page"), &fl!(I18N, "oc-page")] = model.oc_page.widget(),
|
||||
add_titled[Some("thermals_page"), &fl!(I18N, "thermals-page")] = model.thermals_page.widget(),
|
||||
add_titled[Some("software_page"), &fl!(I18N, "software-page")] = model.software_page.widget(),
|
||||
add_titled[Some("displays_page"), &fl!(I18N, "displays-page")] = model.displays_page.widget(),
|
||||
add_named[Some("crash_page")] = model.crash_page.widget(),
|
||||
|
||||
set_visible_child_name: &CONFIG.read().selected_tab,
|
||||
connect_visible_child_name_notify[content_page] => move |stack| {
|
||||
if let Some(child) = stack.visible_child() {
|
||||
let page = stack.page(&child);
|
||||
content_page.set_title(&page.title().unwrap_or_default());
|
||||
|
||||
let name = stack.visible_child_name().unwrap().to_string();
|
||||
if name != "crash_page" {
|
||||
CONFIG.write().edit(|config| {
|
||||
config.selected_tab = name;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
let name = stack.visible_child_name().unwrap().to_string();
|
||||
if name != PageId::Crash.as_str() {
|
||||
CONFIG.write().edit(|config| {
|
||||
config.selected_tab = name;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -428,6 +411,39 @@ impl AsyncComponent for AppModel {
|
||||
|
||||
let displays_page = DisplaysPage::detach_default();
|
||||
|
||||
let ui_sensitive = BoolBinding::new(false);
|
||||
let page_navigation = PageNavigation::detach(PageNavigationInit {
|
||||
pages: vec![
|
||||
(
|
||||
PageId::Info,
|
||||
fl!(I18N, "info-page"),
|
||||
info_page.widget().clone().upcast(),
|
||||
),
|
||||
(
|
||||
PageId::Oc,
|
||||
fl!(I18N, "oc-page"),
|
||||
oc_page.widget().clone().upcast(),
|
||||
),
|
||||
(
|
||||
PageId::Thermals,
|
||||
fl!(I18N, "thermals-page"),
|
||||
thermals_page.widget().clone().upcast(),
|
||||
),
|
||||
(
|
||||
PageId::Software,
|
||||
fl!(I18N, "software-page"),
|
||||
software_page.widget().clone().upcast(),
|
||||
),
|
||||
(
|
||||
PageId::Displays,
|
||||
fl!(I18N, "displays-page"),
|
||||
displays_page.widget().clone().upcast(),
|
||||
),
|
||||
],
|
||||
parent: root.clone(),
|
||||
sensitive: ui_sensitive.clone(),
|
||||
});
|
||||
|
||||
let crash_page = CrashPage::launch_default().forward(sender.input_sender(), |msg| msg);
|
||||
|
||||
let overdrive_dialog =
|
||||
@@ -493,10 +509,11 @@ impl AsyncComponent for AppModel {
|
||||
software_page,
|
||||
crash_page,
|
||||
displays_page,
|
||||
page_navigation,
|
||||
gpu_selector,
|
||||
profile_selector,
|
||||
service_setup_dialog: None,
|
||||
ui_sensitive: BoolBinding::new(false),
|
||||
ui_sensitive,
|
||||
is_reconnecting: BoolBinding::new(false),
|
||||
stats_task_handle: None,
|
||||
settings_changed,
|
||||
@@ -845,6 +862,7 @@ impl AppModel {
|
||||
result?;
|
||||
}
|
||||
AppMsg::EnablePstateConfig => {
|
||||
root.present();
|
||||
self.info_dialog
|
||||
.emit(InfoDialogMsg::Show(Box::new(InfoDialogData {
|
||||
id: InfoDialogId::EnablePstateConfigConfirmation,
|
||||
@@ -923,6 +941,7 @@ impl AppModel {
|
||||
self.reload_profiles(None).await?;
|
||||
}
|
||||
AppMsg::Crash(message) => {
|
||||
self.page_navigation.emit(PageNavigationMsg::CloseWindows);
|
||||
// we cannot be sure that the application is fully functional after a crash
|
||||
// even though the main loop is restored via crash handler, we want user to restart
|
||||
// this is why navigation controls are disabled
|
||||
@@ -931,7 +950,9 @@ impl AppModel {
|
||||
self.settings_changed.set_value(false);
|
||||
|
||||
self.ui_sensitive.set_value(true);
|
||||
widgets.root_stack.set_visible_child_name("crash_page");
|
||||
widgets
|
||||
.root_stack
|
||||
.set_visible_child_name(PageId::Crash.as_str());
|
||||
self.crash_page.emit(message);
|
||||
|
||||
if let Some(handle) = self.stats_task_handle.take() {
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
use crate::{
|
||||
I18N,
|
||||
app::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, pages::PageId},
|
||||
};
|
||||
use adw::prelude::*;
|
||||
use gtk::glib;
|
||||
use i18n_embed_fl::fl;
|
||||
use relm4::{
|
||||
ComponentParts, ComponentSender, RelmObjectExt, RelmWidgetExt, binding::BoolBinding, css,
|
||||
};
|
||||
|
||||
const CONTENT_MAXIMUM_WIDTH: i32 = 1200;
|
||||
|
||||
pub struct DetachablePage {
|
||||
pub init: DetachablePageInit,
|
||||
detached: bool,
|
||||
}
|
||||
|
||||
pub struct DetachablePageInit {
|
||||
pub id: PageId,
|
||||
pub title: String,
|
||||
pub content: gtk::Widget,
|
||||
pub parent: adw::ApplicationWindow,
|
||||
pub sensitive: BoolBinding,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DetachablePageMsg {
|
||||
ToggleDetached,
|
||||
Detach,
|
||||
Attach,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl relm4::Component for DetachablePage {
|
||||
type Init = DetachablePageInit;
|
||||
type Input = DetachablePageMsg;
|
||||
type Output = ();
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
#[root]
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
|
||||
#[name = "content"]
|
||||
gtk::ScrolledWindow {
|
||||
set_hscrollbar_policy: gtk::PolicyType::Never,
|
||||
set_vexpand: true,
|
||||
add_binding: (&model.init.sensitive, "sensitive"),
|
||||
|
||||
adw::Clamp {
|
||||
set_maximum_size: CONTENT_MAXIMUM_WIDTH,
|
||||
set_tightening_threshold: CONTENT_MAXIMUM_WIDTH,
|
||||
set_child: Some(&model.init.content),
|
||||
},
|
||||
},
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 15,
|
||||
set_margin_all: 15,
|
||||
set_align: gtk::Align::Center,
|
||||
set_vexpand: true,
|
||||
#[watch]
|
||||
set_visible: model.detached,
|
||||
|
||||
gtk::Label {
|
||||
set_label: &fl!(I18N, "page-detached"),
|
||||
},
|
||||
gtk::Button {
|
||||
set_label: &fl!(I18N, "show-page-window"),
|
||||
connect_clicked => DetachablePageMsg::Detach,
|
||||
},
|
||||
gtk::Button {
|
||||
set_label: &fl!(I18N, "reattach-page"),
|
||||
connect_clicked => DetachablePageMsg::Attach,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
#[name = "row"]
|
||||
gtk::ListBoxRow {
|
||||
update_property: &[gtk::accessible::Property::Label(&model.init.title)],
|
||||
#[wrap(Some)]
|
||||
set_child = >k::Box {
|
||||
set_spacing: 5,
|
||||
gtk::Label {
|
||||
set_label: &model.init.title,
|
||||
set_halign: gtk::Align::Start,
|
||||
set_hexpand: true,
|
||||
},
|
||||
gtk::Button {
|
||||
set_icon_name: "window-new-symbolic",
|
||||
#[watch]
|
||||
set_tooltip_text: Some(&if model.detached {
|
||||
fl!(I18N, "reattach-page")
|
||||
} else {
|
||||
fl!(I18N, "detach-page", page = model.init.title.as_str())
|
||||
}),
|
||||
add_css_class: css::FLAT,
|
||||
add_css_class: "page-detach-button",
|
||||
connect_clicked => DetachablePageMsg::ToggleDetached,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
#[name = "window"]
|
||||
adw::Window {
|
||||
set_title: Some(&model.init.title),
|
||||
set_default_width: DEFAULT_WINDOW_WIDTH,
|
||||
set_default_height: DEFAULT_WINDOW_HEIGHT,
|
||||
set_application: model.init.parent.application().as_ref(),
|
||||
#[watch]
|
||||
set_visible: model.detached,
|
||||
connect_close_request[sender] => move |_| {
|
||||
sender.input(DetachablePageMsg::Attach);
|
||||
glib::Propagation::Stop
|
||||
},
|
||||
|
||||
#[name = "toolbar"]
|
||||
adw::ToolbarView {
|
||||
add_top_bar = &adw::HeaderBar {},
|
||||
},
|
||||
},
|
||||
|
||||
#[local_ref]
|
||||
parent -> adw::ApplicationWindow {
|
||||
connect_close_request[sender, window] => move |_| {
|
||||
// Hide immediately, before the application's main loop can stop.
|
||||
window.set_visible(false);
|
||||
window.set_application(gtk::Application::NONE);
|
||||
sender.input(DetachablePageMsg::Attach);
|
||||
glib::Propagation::Proceed
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let model = Self {
|
||||
init,
|
||||
detached: false,
|
||||
};
|
||||
let parent = &model.init.parent;
|
||||
let widgets = view_output!();
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update_with_view(
|
||||
&mut self,
|
||||
widgets: &mut Self::Widgets,
|
||||
msg: Self::Input,
|
||||
sender: ComponentSender<Self>,
|
||||
root: &Self::Root,
|
||||
) {
|
||||
match msg {
|
||||
DetachablePageMsg::ToggleDetached => {
|
||||
sender.input(if self.detached {
|
||||
DetachablePageMsg::Attach
|
||||
} else {
|
||||
DetachablePageMsg::Detach
|
||||
});
|
||||
}
|
||||
DetachablePageMsg::Detach => {
|
||||
if !self.detached {
|
||||
root.remove(&widgets.content);
|
||||
widgets.toolbar.set_content(Some(&widgets.content));
|
||||
// Focusing the first input would scroll the page back to that input.
|
||||
GtkWindowExt::set_focus(&widgets.window, Some(&widgets.content));
|
||||
self.detached = true;
|
||||
}
|
||||
widgets.window.present();
|
||||
}
|
||||
DetachablePageMsg::Attach => {
|
||||
if self.detached {
|
||||
widgets.toolbar.set_content(gtk::Widget::NONE);
|
||||
root.append(&widgets.content);
|
||||
self.detached = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.update_view(widgets, sender);
|
||||
}
|
||||
|
||||
fn shutdown(&mut self, widgets: &mut Self::Widgets, _output: relm4::Sender<Self::Output>) {
|
||||
widgets.window.destroy();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
use super::{
|
||||
detachable_page::{DetachablePage, DetachablePageInit, DetachablePageMsg},
|
||||
pages::PageId,
|
||||
utils::ext::RelmLaunchable,
|
||||
};
|
||||
use adw::prelude::*;
|
||||
use relm4::{ComponentController, ComponentParts, ComponentSender, binding::BoolBinding};
|
||||
|
||||
pub struct PageNavigation {
|
||||
pages: Vec<relm4::Controller<DetachablePage>>,
|
||||
}
|
||||
|
||||
pub struct PageNavigationInit {
|
||||
pub pages: Vec<(PageId, String, gtk::Widget)>,
|
||||
pub parent: adw::ApplicationWindow,
|
||||
pub sensitive: BoolBinding,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PageNavigationMsg {
|
||||
Select(usize),
|
||||
SyncSelection,
|
||||
CloseWindows,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl relm4::Component for PageNavigation {
|
||||
type Init = PageNavigationInit;
|
||||
type Input = PageNavigationMsg;
|
||||
type Output = ();
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
#[root]
|
||||
gtk::ListBox {
|
||||
add_css_class: "navigation-sidebar",
|
||||
set_vexpand: true,
|
||||
connect_row_selected[sender] => move |_, row| {
|
||||
if let Some(row) = row {
|
||||
sender.input(PageNavigationMsg::Select(row.index() as usize));
|
||||
}
|
||||
} @ selection_signal,
|
||||
},
|
||||
|
||||
#[name = "stack"]
|
||||
gtk::Stack {
|
||||
set_vhomogeneous: false,
|
||||
connect_visible_child_name_notify => PageNavigationMsg::SyncSelection,
|
||||
},
|
||||
}
|
||||
|
||||
fn init(
|
||||
init: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let pages = init
|
||||
.pages
|
||||
.into_iter()
|
||||
.map(|(id, title, content)| {
|
||||
DetachablePage::detach(DetachablePageInit {
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
parent: init.parent.clone(),
|
||||
sensitive: init.sensitive.clone(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let model = Self { pages };
|
||||
let widgets = view_output!();
|
||||
|
||||
for page in &model.pages {
|
||||
root.append(&page.widgets().row);
|
||||
widgets.stack.add_titled(
|
||||
page.widget(),
|
||||
Some(page.model().init.id.as_str()),
|
||||
&page.model().init.title,
|
||||
);
|
||||
}
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update_with_view(
|
||||
&mut self,
|
||||
widgets: &mut Self::Widgets,
|
||||
msg: Self::Input,
|
||||
_sender: ComponentSender<Self>,
|
||||
root: &Self::Root,
|
||||
) {
|
||||
match msg {
|
||||
PageNavigationMsg::Select(index) => {
|
||||
widgets
|
||||
.stack
|
||||
.set_visible_child_name(self.pages[index].model().init.id.as_str());
|
||||
}
|
||||
PageNavigationMsg::SyncSelection => {
|
||||
let index = self.pages.iter().position(|page| {
|
||||
Some(page.model().init.id.as_str())
|
||||
== widgets.stack.visible_child_name().as_deref()
|
||||
});
|
||||
root.block_signal(&widgets.selection_signal);
|
||||
root.select_row(
|
||||
index
|
||||
.and_then(|index| root.row_at_index(index as i32))
|
||||
.as_ref(),
|
||||
);
|
||||
root.unblock_signal(&widgets.selection_signal);
|
||||
}
|
||||
PageNavigationMsg::CloseWindows => {
|
||||
for page in &self.pages {
|
||||
page.emit(DetachablePageMsg::Attach);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,29 @@ pub mod thermals_page;
|
||||
use lact_schema::{DeviceInfo, DeviceStats};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PageId {
|
||||
Info,
|
||||
Oc,
|
||||
Thermals,
|
||||
Software,
|
||||
Displays,
|
||||
Crash,
|
||||
}
|
||||
|
||||
impl PageId {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Info => "info_page",
|
||||
Self::Oc => "oc_page",
|
||||
Self::Thermals => "thermals_page",
|
||||
Self::Software => "software_page",
|
||||
Self::Displays => "displays_page",
|
||||
Self::Crash => "crash_page",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PageUpdate {
|
||||
Info(Arc<DeviceInfo>),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::app::{
|
||||
graphs_window::stat::StatType,
|
||||
pages::PageId,
|
||||
utils::{color_scheme::AppColorScheme, styles::AppTheme},
|
||||
};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
@@ -125,5 +126,5 @@ fn deserialize_poll_interval<'de, D: Deserializer<'de>>(deserializer: D) -> Resu
|
||||
}
|
||||
|
||||
fn default_tab() -> String {
|
||||
"info_page".to_owned()
|
||||
PageId::Info.as_str().to_owned()
|
||||
}
|
||||
Reference in new issue
Block a user