feat: use relm4 for the UI (only main parts) (#375)

* feat: use relm4 for vulkan window

* feat: WIP relm4 for app

* feat: applying settings

* wip

* feat: implement all the main actions

* feat: avoid full reloads when they are not needed

* fix: don't save vbios dump when the dialog was cancelled

* feat: OC control

* feat: use relm for the software page
This commit is contained in:
Ilya Zlobintsev 2024-09-20 15:46:33 +03:00 committed by GitHub
parent 30df3ee11b
commit e411d155da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1235 additions and 1147 deletions

110
Cargo.lock generated
View File

@ -920,6 +920,18 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d"
[[package]]
name = "flume"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
dependencies = [
"futures-core",
"futures-sink",
"nanorand",
"spin",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -978,6 +990,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "fragile"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
[[package]] [[package]]
name = "freetype-sys" name = "freetype-sys"
version = "0.20.1" version = "0.20.1"
@ -997,6 +1015,7 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-executor",
"futures-io", "futures-io",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
@ -1078,10 +1097,13 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io",
"futures-macro", "futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
@ -1161,8 +1183,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@ -1621,6 +1645,8 @@ dependencies = [
"plotters", "plotters",
"plotters-cairo", "plotters-cairo",
"pretty_assertions", "pretty_assertions",
"relm4",
"relm4-components",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
@ -1835,6 +1861,15 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "nanorand"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [
"getrandom",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.29.0" version = "0.29.0"
@ -2294,6 +2329,52 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "relm4"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf0363f92b6a7eefd985b47f27b7ae168dd2fd5cd4013a338c9b111c33744d1f"
dependencies = [
"flume",
"fragile",
"futures",
"gtk4",
"libadwaita",
"once_cell",
"relm4-css",
"relm4-macros",
"tokio",
"tracing",
]
[[package]]
name = "relm4-components"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb3d67f2982131c5e6047af4278d8fe750266767e57b58bc15f2e11e190eef36"
dependencies = [
"once_cell",
"relm4",
"tracker",
]
[[package]]
name = "relm4-css"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d3b924557df1cddc687b60b313c4b76620fdbf0e463afa4b29f67193ccf37f9"
[[package]]
name = "relm4-macros"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc5885640821d60062497737dd42fd04248d13c7ecccee620caaa4b210fe9905"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "rle-decode-fast" name = "rle-decode-fast"
version = "1.0.3" version = "1.0.3"
@ -2518,6 +2599,15 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@ -2792,6 +2882,26 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "tracker"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5c98457ff700aaeefcd4a4a492096e78a2af1dd8523c66e94a3adb0fdbd415"
dependencies = [
"tracker-macros",
]
[[package]]
name = "tracker-macros"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc19eb2373ccf3d1999967c26c3d44534ff71ae5d8b9dacf78f4b13132229e48"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.20.0" version = "0.20.0"

View File

@ -100,7 +100,7 @@ impl DaemonClient {
}) })
} }
pub fn list_devices<'a>(&self) -> anyhow::Result<ResponseBuffer<Vec<DeviceListEntry<'a>>>> { pub fn list_devices(&self) -> anyhow::Result<ResponseBuffer<Vec<DeviceListEntry>>> {
self.make_request(Request::ListDevices) self.make_request(Request::ListDevices)
} }

View File

@ -277,15 +277,18 @@ impl<'a> Handler {
.context("No controller with such id")?) .context("No controller with such id")?)
} }
pub fn list_devices(&'a self) -> Vec<DeviceListEntry<'a>> { pub fn list_devices(&'a self) -> Vec<DeviceListEntry> {
self.gpu_controllers self.gpu_controllers
.iter() .iter()
.map(|(id, controller)| { .map(|(id, controller)| {
let name = controller let name = controller
.pci_info .pci_info
.as_ref() .as_ref()
.and_then(|pci_info| pci_info.device_pci_info.model.as_deref()); .and_then(|pci_info| pci_info.device_pci_info.model.clone());
DeviceListEntry { id, name } DeviceListEntry {
id: id.to_owned(),
name,
}
}) })
.collect() .collect()
} }

View File

@ -14,13 +14,15 @@ const PP_OVERDRIVE_MASK: u64 = 0x4000;
pub const PP_FEATURE_MASK_PATH: &str = "/sys/module/amdgpu/parameters/ppfeaturemask"; pub const PP_FEATURE_MASK_PATH: &str = "/sys/module/amdgpu/parameters/ppfeaturemask";
pub const MODULE_CONF_PATH: &str = "/etc/modprobe.d/99-amdgpu-overdrive.conf"; pub const MODULE_CONF_PATH: &str = "/etc/modprobe.d/99-amdgpu-overdrive.conf";
pub async fn info() -> anyhow::Result<SystemInfo<'static>> { pub async fn info() -> anyhow::Result<SystemInfo> {
let version = env!("CARGO_PKG_VERSION"); let version = env!("CARGO_PKG_VERSION").to_owned();
let profile = if cfg!(debug_assertions) { let profile = if cfg!(debug_assertions) {
"debug" "debug"
} else { } else {
"release" "release"
}; }
.to_owned();
let kernel_output = Command::new("uname") let kernel_output = Command::new("uname")
.arg("-r") .arg("-r")
.output() .output()
@ -42,7 +44,7 @@ pub async fn info() -> anyhow::Result<SystemInfo<'static>> {
profile, profile,
kernel_version, kernel_version,
amdgpu_overdrive_enabled, amdgpu_overdrive_enabled,
commit: Some(GIT_COMMIT), commit: Some(GIT_COMMIT.to_owned()),
}) })
} }

View File

@ -8,10 +8,12 @@ edition = "2021"
default = ["gtk-tests"] default = ["gtk-tests"]
gtk-tests = [] gtk-tests = []
bench = [] bench = []
adw = ["dep:adw", "relm4/libadwaita"]
[dependencies] [dependencies]
lact-client = { path = "../lact-client" } lact-client = { path = "../lact-client" }
lact-daemon = { path = "../lact-daemon", default-features = false } lact-daemon = { path = "../lact-daemon", default-features = false }
lact-schema = { path = "../lact-schema", features = ["args"] }
amdgpu-sysfs = { workspace = true } amdgpu-sysfs = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
@ -23,6 +25,8 @@ gtk = { version = "0.9", package = "gtk4", features = ["v4_6", "blueprint"] }
adw = { package = "libadwaita", version = "0.7.0", features = [ adw = { package = "libadwaita", version = "0.7.0", features = [
"v1_4", "v1_4",
], optional = true } ], optional = true }
relm4 = "0.9.0"
relm4-components = "0.9.0"
plotters = { version = "0.3.5", default-features = false, features = [ plotters = { version = "0.3.5", default-features = false, features = [
"datetime", "datetime",
@ -38,7 +42,6 @@ itertools = "0.13.0"
criterion = "0.5.1" criterion = "0.5.1"
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
lact-gui = { path = ".", features = ["bench"] } lact-gui = { path = ".", features = ["bench"] }
lact-schema = { path = "../lact-schema", features = ["args"] }
[[bench]] [[bench]]
name = "gui" name = "gui"

View File

@ -1,50 +1,62 @@
use gtk::prelude::*; use gtk::prelude::{BoxExt, ButtonExt, OrientableExt, WidgetExt};
use gtk::*; use relm4::{ComponentParts, ComponentSender, SimpleComponent};
#[derive(Clone)]
pub struct ApplyRevealer { pub struct ApplyRevealer {
pub container: Revealer, shown: bool,
apply_button: Button,
reset_button: Button,
} }
impl ApplyRevealer { #[derive(Debug)]
pub fn new() -> Self { pub enum ApplyRevealerMsg {
let container = Revealer::builder().transition_duration(150).build(); Show,
let vbox = Box::new(Orientation::Horizontal, 5); Hide,
}
let apply_button = Button::builder().label("Apply").hexpand(true).build(); #[relm4::component(pub)]
let reset_button = Button::builder().label("Reset").build(); impl SimpleComponent for ApplyRevealer {
type Init = ();
vbox.append(&apply_button); type Input = ApplyRevealerMsg;
vbox.append(&reset_button); type Output = super::AppMsg;
container.set_child(Some(&vbox)); view! {
gtk::Revealer {
#[watch]
set_reveal_child: model.shown,
Self { gtk::Box {
container, set_orientation: gtk::Orientation::Horizontal,
apply_button, set_spacing: 5,
reset_button,
gtk::Button {
set_label: "Apply",
set_hexpand: true,
connect_clicked[sender] => move |_| { sender.output(super::AppMsg::ApplyChanges).unwrap(); },
},
gtk::Button {
set_label: "Revert",
connect_clicked[sender] => move |_| { sender.output(super::AppMsg::RevertChanges).unwrap(); },
},
}
} }
} }
pub fn show(&self) { fn init(
self.container.set_reveal_child(true); _init: Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self { shown: false };
let widgets = view_output!();
ComponentParts { widgets, model }
} }
pub fn hide(&self) { fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
self.container.set_reveal_child(false); match msg {
} ApplyRevealerMsg::Show => self.shown = true,
ApplyRevealerMsg::Hide => self.shown = false,
pub fn connect_apply_button_clicked<F: Fn() + 'static>(&self, f: F) { }
self.apply_button.connect_clicked(move |_| {
f();
});
}
pub fn connect_reset_button_clicked<F: Fn() + 'static>(&self, f: F) {
self.reset_button.connect_clicked(move |_| {
f();
});
} }
} }

View File

@ -0,0 +1,54 @@
use gtk::prelude::{DialogExt, GtkWindowExt, WidgetExt};
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
#[derive(Clone, Debug)]
pub struct ConfirmationOptions {
pub title: &'static str,
pub message: String,
pub buttons_type: gtk::ButtonsType,
}
pub struct ConfirmationDialog {}
#[relm4::component(pub)]
impl SimpleComponent for ConfirmationDialog {
type Init = (ConfirmationOptions, gtk::ApplicationWindow);
type Input = ();
type Output = gtk::ResponseType;
view! {
gtk::MessageDialog {
set_transient_for: Some(&parent),
set_title: Some(options.title),
set_use_markup: true,
connect_response[sender] => move |diag, response| {
sender.output(response).unwrap();
diag.close();
},
}
}
#[allow(unused_assignments)]
fn init(
(options, parent): Self::Init,
mut root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self {};
root = gtk::MessageDialog::new(
Some(&parent),
gtk::DialogFlags::MODAL,
gtk::MessageType::Question,
options.buttons_type,
options.message,
);
let widgets = view_output!();
root.show();
ComponentParts { model, widgets }
}
}

View File

@ -1,9 +1,96 @@
use super::{AppMsg, DebugSnapshot, DisableOverdrive, DumpVBios, ResetConfig, ShowGraphsWindow};
use gtk::prelude::*; use gtk::prelude::*;
use gtk::*; use gtk::*;
use lact_client::schema::{DeviceListEntry, SystemInfo}; use lact_client::schema::DeviceListEntry;
use pango::EllipsizeMode; use relm4::{
Component, ComponentController, ComponentParts, ComponentSender, Controller, SimpleComponent,
};
use relm4_components::simple_combo_box::SimpleComboBox;
#[derive(Clone)] pub struct Header {
gpu_selector: Controller<SimpleComboBox<DeviceListEntry>>,
}
#[relm4::component(pub)]
impl SimpleComponent for Header {
type Init = (Vec<DeviceListEntry>, gtk::Stack);
type Input = ();
type Output = AppMsg;
view! {
gtk::HeaderBar {
set_show_title_buttons: true,
#[wrap(Some)]
set_title_widget = &StackSwitcher {
set_stack: Some(&stack),
},
#[local_ref]
pack_start = gpu_selector -> ComboBoxText,
pack_end = &gtk::MenuButton {
set_icon_name: "open-menu-symbolic",
set_menu_model: Some(&app_menu),
}
}
}
menu! {
app_menu: {
section! {
"Show historical charts" => ShowGraphsWindow,
},
section! {
"Generate debug snapshot" => DebugSnapshot,
"Dump VBIOS" => DumpVBios,
} ,
section! {
"Disable overclocking support" => DisableOverdrive,
"Reset all configuration" => ResetConfig,
}
}
}
fn init(
(variants, stack): Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let gpu_selector = SimpleComboBox::builder()
.launch(SimpleComboBox {
variants,
active_index: Some(0),
})
.forward(sender.output_sender(), |_| AppMsg::ReloadData {
full: true,
});
// limits the length of gpu names in combobox
for cell in gpu_selector.widget().cells() {
cell.set_property("width-chars", 10);
cell.set_property("ellipsize", pango::EllipsizeMode::End);
}
let model = Self { gpu_selector };
let gpu_selector = model.gpu_selector.widget();
let widgets = view_output!();
ComponentParts { model, widgets }
}
}
impl Header {
pub fn selected_gpu_id(&self) -> Option<String> {
self.gpu_selector
.model()
.get_active_elem()
.map(|model| model.id.clone())
}
}
/*#[derive(Clone)]
pub struct Header { pub struct Header {
pub container: HeaderBar, pub container: HeaderBar,
gpu_selector: ComboBoxText, gpu_selector: ComboBoxText,
@ -80,4 +167,4 @@ impl Header {
} }
}); });
} }
} }*/

View File

@ -13,6 +13,14 @@ impl InfoRow {
.property("value", value) .property("value", value)
.build() .build()
} }
pub fn new_selectable(name: &str, value: &str) -> Self {
Object::builder()
.property("name", name)
.property("value", value)
.property("selectable", true)
.build()
}
} }
mod imp { mod imp {

File diff suppressed because it is too large Load Diff

39
lact-gui/src/app/msg.rs Normal file
View File

@ -0,0 +1,39 @@
use super::confirmation_dialog::ConfirmationOptions;
use lact_schema::DeviceStats;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub enum AppMsg {
Error(Rc<anyhow::Error>),
ReloadData { full: bool },
Stats(DeviceStats),
ApplyChanges,
RevertChanges,
ResetClocks,
ResetPmfw,
ShowGraphsWindow,
DumpVBios,
DebugSnapshot,
EnableOverdrive,
DisableOverdrive,
ResetConfig,
AskConfirmation(ConfirmationOptions, Box<AppMsg>),
}
impl AppMsg {
pub fn ask_confirmation(
inner: AppMsg,
title: &'static str,
message: impl Into<String>,
buttons_type: gtk::ButtonsType,
) -> Self {
Self::AskConfirmation(
ConfirmationOptions {
title,
message: message.into(),
buttons_type,
},
Box::new(inner),
)
}
}

View File

@ -0,0 +1,161 @@
use gtk::{
glib::GString,
prelude::{EditableExt, GtkWindowExt, OrientableExt, WidgetExt},
NoSelection,
};
use relm4::{
typed_view::list::{RelmListItem, TypedListView},
view, ComponentParts, ComponentSender, SimpleComponent,
};
pub struct VulkanFeaturesWindow {
features_view: TypedListView<VulkanFeature, NoSelection>,
}
#[derive(Debug)]
pub enum AppMsg {
FilterChanged(GString),
}
#[relm4::component(pub)]
impl SimpleComponent for VulkanFeaturesWindow {
type Init = (Vec<VulkanFeature>, String);
type Input = AppMsg;
type Output = ();
view! {
gtk::Window {
set_title: Some(&title),
set_default_width: 500,
set_default_height: 700,
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
#[name = "search_entry"]
gtk::SearchEntry {
connect_search_changed[sender] => move |entry| {
sender.input(AppMsg::FilterChanged(entry.text()));
},
connect_stop_search[root] => move |_| {
root.close();
},
},
gtk::ScrolledWindow {
#[local_ref]
features_list -> gtk::ListView {
set_show_separators: true,
},
set_vexpand: true,
}
},
add_controller = gtk::ShortcutController {
set_scope: gtk::ShortcutScope::Global,
add_shortcut = gtk::Shortcut {
set_trigger: Some(gtk::ShortcutTrigger::parse_string("Escape|<Ctrl>w").unwrap()),
set_action: Some(gtk::ShortcutAction::parse_string("action(window.close)").unwrap()),
}
}
}
}
fn init(
(features, title): Self::Init,
root: Self::Root,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let mut features_view: TypedListView<VulkanFeature, NoSelection> = TypedListView::new();
features_view.extend_from_iter(features);
let mut model = VulkanFeaturesWindow { features_view };
let features_list = &model.features_view.view;
let widgets = view_output!();
model.features_view.add_filter({
let search_entry = widgets.search_entry.clone();
move |feature| {
feature
.name
.to_lowercase()
.contains(&search_entry.text().to_lowercase())
}
});
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
match msg {
AppMsg::FilterChanged(filter) => {
self.features_view.set_filter_status(0, false);
if !filter.is_empty() {
self.features_view.set_filter_status(0, true);
}
}
}
}
}
#[derive(Clone)]
pub struct VulkanFeature {
pub name: String,
pub supported: bool,
}
pub struct VulkanFeatureWidgets {
label: gtk::Label,
image: gtk::Image,
}
impl RelmListItem for VulkanFeature {
type Root = gtk::Box;
type Widgets = VulkanFeatureWidgets;
fn setup(_: &gtk::ListItem) -> (Self::Root, Self::Widgets) {
view! {
root_box = gtk::Box {
set_focus_on_click: true,
set_hexpand: true,
set_hexpand_set: true,
set_margin_start: 20,
set_margin_end: 20,
set_margin_top: 10,
set_margin_bottom: 10,
#[name = "label"]
gtk::Label {
set_halign: gtk::Align::Start,
set_hexpand: true,
set_selectable: true,
},
#[name = "image"]
gtk::Image {
set_halign: gtk::Align::End,
},
}
}
let widgets = VulkanFeatureWidgets { label, image };
(root_box, widgets)
}
fn bind(&mut self, widgets: &mut Self::Widgets, _root: &mut Self::Root) {
widgets.label.set_label(&self.name);
let icon = match self.supported {
true => "emblem-ok-symbolic",
false => "action-unavailable-symbolic",
};
widgets.image.set_icon_name(Some(icon));
}
}

View File

@ -1,48 +0,0 @@
use gtk::glib::{self, Object};
glib::wrapper! {
pub struct VulkanFeature(ObjectSubclass<imp::VulkanFeature>);
}
impl VulkanFeature {
pub fn new(name: String, supported: bool) -> Self {
Object::builder()
.property("name", name)
.property("supported", supported)
.build()
}
}
impl Default for VulkanFeature {
fn default() -> Self {
Self::new(String::new(), false)
}
}
mod imp {
use gio::subclass::prelude::*;
use gtk::{
gio,
glib::{self, Properties},
prelude::*,
};
use std::cell::{Cell, RefCell};
#[derive(Debug, Default, Properties)]
#[properties(wrapper_type = super::VulkanFeature)]
pub struct VulkanFeature {
#[property(set, get)]
pub name: RefCell<String>,
#[property(set, get)]
pub supported: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for VulkanFeature {
const NAME: &'static str = "VulkanFeature";
type Type = super::VulkanFeature;
}
#[glib::derived_properties]
impl ObjectImpl for VulkanFeature {}
}

View File

@ -1,130 +0,0 @@
pub mod feature;
mod row;
use glib::Object;
use gtk::{gio, glib};
glib::wrapper! {
pub struct VulkanFeaturesWindow(ObjectSubclass<imp::VulkanFeaturesWindow>)
@extends gtk::Box, gtk::Widget, gtk::Window,
@implements gtk::Orientable, gtk::Accessible, gtk::Buildable;
}
impl VulkanFeaturesWindow {
pub fn new(title: &str, model: gio::ListModel) -> Self {
Object::builder()
.property("title", title)
.property("model", model)
.build()
}
}
mod imp {
use super::{feature::VulkanFeature, row::VulkanFeatureRow};
use glib::Properties;
use gtk::{
gio,
glib::{self, clone, subclass::InitializingObject},
prelude::*,
subclass::{
prelude::*,
widget::{CompositeTemplateClass, WidgetImpl},
},
CompositeTemplate, Expression, FilterListModel, PropertyExpression, SearchEntry,
SignalListItemFactory, StringFilter, TemplateChild,
};
use std::cell::RefCell;
#[derive(CompositeTemplate, Properties, Default)]
#[properties(wrapper_type = super::VulkanFeaturesWindow)]
#[template(file = "ui/vulkan_features_window.blp")]
pub struct VulkanFeaturesWindow {
#[property(get, set)]
model: RefCell<Option<gio::ListModel>>,
#[template_child]
features_factory: TemplateChild<SignalListItemFactory>,
#[template_child]
filter_model: TemplateChild<FilterListModel>,
#[template_child]
search_filter: TemplateChild<StringFilter>,
#[template_child]
search_entry: TemplateChild<SearchEntry>,
}
#[glib::object_subclass]
impl ObjectSubclass for VulkanFeaturesWindow {
const NAME: &'static str = "VulkanFeaturesWindow";
type Type = super::VulkanFeaturesWindow;
type ParentType = gtk::Window;
fn class_init(class: &mut Self::Class) {
class.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for VulkanFeaturesWindow {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.bind_property("model", &self.filter_model.get(), "model")
.sync_create()
.build();
let expression =
PropertyExpression::new(VulkanFeature::static_type(), Expression::NONE, "name");
self.search_filter.set_expression(Some(&expression));
self.search_entry.connect_search_changed(clone!(
#[strong(rename_to = filter)]
self.search_filter,
move |entry| {
if entry.text().is_empty() {
filter.set_search(None);
} else {
filter.set_search(Some(entry.text().as_str()));
}
}
));
self.search_entry.connect_stop_search(clone!(
#[weak(rename_to = win)]
obj,
move |_search| {
win.close();
}
));
self.features_factory.connect_setup(|_, list_item| {
let feature = VulkanFeature::default();
let row = VulkanFeatureRow::new(feature);
list_item.set_child(Some(&row));
});
self.features_factory.connect_bind(|_, list_item| {
let feature = list_item
.item()
.and_downcast::<VulkanFeature>()
.expect("The item has to be a VulkanFeature");
let row = list_item
.child()
.and_downcast::<VulkanFeatureRow>()
.expect("The child has to be a VulkanFeatureRow");
row.set_feature(feature);
});
}
}
impl WidgetImpl for VulkanFeaturesWindow {}
impl WindowImpl for VulkanFeaturesWindow {}
impl ApplicationWindowImpl for VulkanFeaturesWindow {}
}

View File

@ -1,85 +0,0 @@
use super::feature::VulkanFeature;
use glib::Object;
use gtk::glib;
glib::wrapper! {
pub struct VulkanFeatureRow(ObjectSubclass<imp::VulkanFeatureRow>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Orientable, gtk::Accessible, gtk::Buildable;
}
impl VulkanFeatureRow {
pub fn new(feature: VulkanFeature) -> Self {
Object::builder().property("feature", feature).build()
}
}
mod imp {
use crate::app::root_stack::info_page::vulkan_info::feature_window::feature::VulkanFeature;
use glib::Properties;
use gtk::{
glib::{self, subclass::InitializingObject},
prelude::*,
subclass::{
prelude::*,
widget::{CompositeTemplateClass, WidgetImpl},
},
CompositeTemplate, Image, Label, TemplateChild,
};
use std::cell::RefCell;
#[derive(CompositeTemplate, Default, Properties)]
#[properties(wrapper_type = super::VulkanFeatureRow)]
#[template(file = "ui/vulkan_feature_row.blp")]
pub struct VulkanFeatureRow {
#[template_child]
name_label: TemplateChild<Label>,
#[template_child]
available_image: TemplateChild<Image>,
#[property(get, set)]
feature: RefCell<VulkanFeature>,
}
#[glib::object_subclass]
impl ObjectSubclass for VulkanFeatureRow {
const NAME: &'static str = "VulkanFeatureRow";
type Type = super::VulkanFeatureRow;
type ParentType = gtk::Box;
fn class_init(class: &mut Self::Class) {
class.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for VulkanFeatureRow {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.bind_property("feature", &self.name_label.get(), "label")
.transform_to(|_, feature: VulkanFeature| Some(feature.name()))
.sync_create()
.build();
obj.bind_property("feature", &self.available_image.get(), "icon-name")
.transform_to(|_, feature: VulkanFeature| {
if feature.supported() {
Some("emblem-ok-symbolic")
} else {
Some("action-unavailable-symbolic")
}
})
.sync_create()
.build();
}
}
impl WidgetImpl for VulkanFeatureRow {}
impl BoxImpl for VulkanFeatureRow {}
}

View File

@ -1,14 +1,15 @@
mod feature_window; mod feature_window;
use self::feature_window::VulkanFeaturesWindow;
use super::values_grid; use super::values_grid;
use crate::app::root_stack::info_page::vulkan_info::feature_window::feature::VulkanFeature;
use crate::app::root_stack::{label_row, values_row}; use crate::app::root_stack::{label_row, values_row};
use feature_window::{VulkanFeature, VulkanFeaturesWindow};
use glib::clone; use glib::clone;
use gtk::prelude::*; use gtk::prelude::*;
use gtk::*; use gtk::*;
use lact_client::schema::VulkanInfo; use lact_client::schema::VulkanInfo;
use relm4::{Component, ComponentController};
use std::cell::RefCell;
use std::rc::Rc;
use tracing::trace; use tracing::trace;
#[derive(Clone)] #[derive(Clone)]
@ -18,16 +19,16 @@ pub struct VulkanInfoFrame {
version_label: Label, version_label: Label,
driver_name_label: Label, driver_name_label: Label,
driver_version_label: Label, driver_version_label: Label,
features_model: gio::ListStore, features: Rc<RefCell<Vec<VulkanFeature>>>,
extensions_model: gio::ListStore, extensions: Rc<RefCell<Vec<VulkanFeature>>>,
} }
impl VulkanInfoFrame { impl VulkanInfoFrame {
pub fn new() -> Self { pub fn new() -> Self {
let container = Box::new(Orientation::Vertical, 0); let container = Box::new(Orientation::Vertical, 0);
let features_model = gio::ListStore::new::<VulkanFeature>(); let features: Rc<RefCell<Vec<VulkanFeature>>> = Rc::default();
let extensions_model = gio::ListStore::new::<VulkanFeature>(); let extensions: Rc<RefCell<Vec<VulkanFeature>>> = Rc::default();
let grid = values_grid(); let grid = values_grid();
grid.set_margin_start(0); grid.set_margin_start(0);
@ -41,9 +42,9 @@ impl VulkanInfoFrame {
let show_features_button = Button::builder().label("Show").halign(Align::End).build(); let show_features_button = Button::builder().label("Show").halign(Align::End).build();
show_features_button.connect_clicked(clone!( show_features_button.connect_clicked(clone!(
#[strong] #[strong]
features_model, features,
move |_| { move |_| {
show_features_window("Vulkan features", features_model.clone()); show_features_window("Vulkan features", features.clone());
} }
)); ));
values_row("Features:", &grid, &show_features_button, 4, 0); values_row("Features:", &grid, &show_features_button, 4, 0);
@ -51,9 +52,9 @@ impl VulkanInfoFrame {
let show_extensions_button = Button::builder().label("Show").halign(Align::End).build(); let show_extensions_button = Button::builder().label("Show").halign(Align::End).build();
show_extensions_button.connect_clicked(clone!( show_extensions_button.connect_clicked(clone!(
#[strong] #[strong]
extensions_model, extensions,
move |_| { move |_| {
show_features_window("Vulkan extensions", extensions_model.clone()); show_features_window("Vulkan extensions", extensions.clone());
} }
)); ));
values_row("Extensions:", &grid, &show_extensions_button, 5, 0); values_row("Extensions:", &grid, &show_extensions_button, 5, 0);
@ -66,8 +67,8 @@ impl VulkanInfoFrame {
version_label, version_label,
driver_name_label, driver_name_label,
driver_version_label, driver_version_label,
features_model, features,
extensions_model, extensions,
} }
} }
@ -89,21 +90,35 @@ impl VulkanInfoFrame {
vulkan_info.driver.info.as_deref().unwrap_or_default(), vulkan_info.driver.info.as_deref().unwrap_or_default(),
)); ));
self.features_model.remove_all(); let mut features = self.features.borrow_mut();
features.clear();
for (name, supported) in &vulkan_info.features { for (name, supported) in &vulkan_info.features {
let feature = VulkanFeature::new(name.to_string(), *supported); let feature = VulkanFeature {
self.features_model.append(&feature); name: name.to_string(),
supported: *supported,
};
features.push(feature);
} }
self.extensions_model.remove_all(); let mut extensions = self.extensions.borrow_mut();
extensions.clear();
for (name, supported) in &vulkan_info.extensions { for (name, supported) in &vulkan_info.extensions {
let extension = VulkanFeature::new(name.to_string(), *supported); let extension = VulkanFeature {
self.extensions_model.append(&extension); name: name.to_string(),
supported: *supported,
};
extensions.push(extension);
} }
} }
} }
fn show_features_window(title: &str, model: gio::ListStore) { fn show_features_window(title: &str, values: Rc<RefCell<Vec<VulkanFeature>>>) {
let window = VulkanFeaturesWindow::new(title, model.into()); let features = values.borrow().iter().cloned().collect();
window.present();
let mut window_controller = VulkanFeaturesWindow::builder()
.launch((features, title.to_owned()))
.detach();
window_controller.detach_runtime();
window_controller.widget().present();
} }

View File

@ -5,6 +5,7 @@ mod software_page;
mod thermals_page; mod thermals_page;
use gtk::{prelude::*, *}; use gtk::{prelude::*, *};
use relm4::{Component, ComponentController};
use self::software_page::SoftwarePage; use self::software_page::SoftwarePage;
use info_page::InformationPage; use info_page::InformationPage;
@ -41,8 +42,11 @@ impl RootStack {
container.add_titled(&thermals_page.container, Some("thermals_page"), "Thermals"); container.add_titled(&thermals_page.container, Some("thermals_page"), "Thermals");
let software_page = SoftwarePage::new(system_info, embedded_daemon); let mut software_page = SoftwarePage::builder()
container.add_titled(&software_page, Some("software_page"), "Software"); .launch((system_info, embedded_daemon))
.detach();
container.add_titled(software_page.widget(), Some("software_page"), "Software");
software_page.detach_runtime();
Self { Self {
container, container,

View File

@ -1,16 +1,39 @@
use crate::GUI_VERSION; use crate::{app::info_row::InfoRow, GUI_VERSION};
use gtk::glib::{self, Object}; use gtk::prelude::*;
use lact_client::schema::{SystemInfo, GIT_COMMIT}; use lact_client::schema::{SystemInfo, GIT_COMMIT};
use relm4::{ComponentParts, ComponentSender, SimpleComponent};
use std::fmt::Write; use std::fmt::Write;
glib::wrapper! { pub struct SoftwarePage {}
pub struct SoftwarePage(ObjectSubclass<imp::SoftwarePage>)
@extends gtk::Box, gtk::Widget, #[relm4::component(pub)]
@implements gtk::Orientable, gtk::Accessible, gtk::Buildable; impl SimpleComponent for SoftwarePage {
} type Init = (SystemInfo, bool);
type Input = ();
type Output = ();
view! {
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
set_margin_start: 5,
set_margin_end: 5,
set_margin_top: 5,
set_margin_bottom: 5,
append = &InfoRow::new_selectable("LACT Daemon:", &daemon_version),
append = &InfoRow::new_selectable("LACT GUI:", &gui_version),
append = &InfoRow::new_selectable("Kernel Version:", &system_info.kernel_version),
}
}
fn init(
(system_info, embedded): Self::Init,
root: Self::Root,
_sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let model = Self {};
impl SoftwarePage {
pub fn new(system_info: SystemInfo, embedded: bool) -> Self {
let mut daemon_version = format!("{}-{}", system_info.version, system_info.profile); let mut daemon_version = format!("{}-{}", system_info.version, system_info.profile);
if embedded { if embedded {
daemon_version.push_str("-embedded"); daemon_version.push_str("-embedded");
@ -26,62 +49,8 @@ impl SoftwarePage {
}; };
let gui_version = format!("{GUI_VERSION}-{gui_profile} (commit {GIT_COMMIT})"); let gui_version = format!("{GUI_VERSION}-{gui_profile} (commit {GIT_COMMIT})");
Object::builder() let widgets = view_output!();
.property("daemon-version", daemon_version)
.property("gui-version", gui_version) ComponentParts { model, widgets }
.property("kernel-version", system_info.kernel_version)
.build()
} }
} }
mod imp {
#![allow(clippy::enum_variant_names)]
use crate::app::{info_row::InfoRow, page_section::PageSection};
use glib::Properties;
use gtk::{
glib::{self, subclass::InitializingObject},
prelude::*,
subclass::{
prelude::*,
widget::{CompositeTemplateClass, WidgetImpl},
},
CompositeTemplate,
};
use std::cell::RefCell;
#[derive(CompositeTemplate, Default, Properties)]
#[properties(wrapper_type = super::SoftwarePage)]
#[template(file = "ui/software_page.blp")]
pub struct SoftwarePage {
#[property(get, set)]
daemon_version: RefCell<String>,
#[property(get, set)]
gui_version: RefCell<String>,
#[property(get, set)]
kernel_version: RefCell<String>,
}
#[glib::object_subclass]
impl ObjectSubclass for SoftwarePage {
const NAME: &'static str = "SoftwarePage";
type Type = super::SoftwarePage;
type ParentType = gtk::Box;
fn class_init(class: &mut Self::Class) {
InfoRow::ensure_type();
PageSection::ensure_type();
class.bind_template();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for SoftwarePage {}
impl WidgetImpl for SoftwarePage {}
impl BoxImpl for SoftwarePage {}
}

View File

@ -1,8 +1,10 @@
pub mod app; pub mod app;
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use app::App; use app::AppModel;
use lact_client::{schema::args::GuiArgs, DaemonClient}; use lact_client::DaemonClient;
use lact_schema::args::GuiArgs;
use relm4::RelmApp;
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use tracing::{debug, error, info, metadata::LevelFilter}; use tracing::{debug, error, info, metadata::LevelFilter};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
@ -22,9 +24,10 @@ pub fn run(args: GuiArgs) -> anyhow::Result<()> {
} }
let (connection, connection_err) = create_connection()?; let (connection, connection_err) = create_connection()?;
let app = App::new(connection);
app.run(connection_err) let app = RelmApp::new(APP_ID).with_args(vec![]);
app.run::<AppModel>((connection, connection_err));
Ok(())
} }
fn create_connection() -> anyhow::Result<(DaemonClient, Option<anyhow::Error>)> { fn create_connection() -> anyhow::Result<(DaemonClient, Option<anyhow::Error>)> {

View File

@ -1,28 +0,0 @@
using Gtk 4.0;
template $SoftwarePage: Box {
orientation: vertical;
spacing: 10;
margin-start: 5;
margin-end: 5;
margin-top: 5;
margin-bottom: 5;
$InfoRow {
name: "LACT Daemon:";
value: bind template.daemon_version;
selectable: true;
}
$InfoRow {
name: "LACT GUI:";
value: bind template.gui_version;
selectable: true;
}
$InfoRow {
name: "Kernel Version:";
value: bind template.kernel_version;
selectable: true;
}
}

View File

@ -1,23 +0,0 @@
using Gtk 4.0;
template $VulkanFeatureRow: Box {
focus-on-click: false;
hexpand: true;
hexpand-set: true;
margin-bottom: 10;
margin-end: 20;
margin-start: 20;
margin-top: 10;
Label name_label {
halign: start;
hexpand: true;
label: 'feature name';
selectable: true;
}
Image available_image {
halign: end;
icon-name: 'action-unavailable-symbolic';
}
}

View File

@ -1,44 +0,0 @@
using Gtk 4.0;
template $VulkanFeaturesWindow: Window {
default-height: 700;
default-width: 500;
Box {
orientation: vertical;
SearchEntry search_entry {}
ScrolledWindow {
vexpand: true;
ListView {
factory: features_factory;
model: selection_model;
show-separators: true;
}
}
}
ShortcutController {
scope: global;
Shortcut {
trigger: "Escape|<Ctrl>w";
action: "action(window.close)";
}
}
}
SignalListItemFactory features_factory {}
NoSelection selection_model {
model: filter_model;
}
StringFilter search_filter {}
FilterListModel filter_model {
filter: search_filter;
incremental: true;
}

View File

@ -23,6 +23,7 @@ use serde_with::skip_serializing_none;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
fmt,
str::FromStr, str::FromStr,
}; };
@ -58,18 +59,27 @@ pub fn default_fan_curve() -> FanCurveMap {
pub struct Pong; pub struct Pong;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct SystemInfo<'a> { pub struct SystemInfo {
pub version: &'a str, pub version: String,
pub commit: Option<&'a str>, pub commit: Option<String>,
pub profile: &'a str, pub profile: String,
pub kernel_version: String, pub kernel_version: String,
pub amdgpu_overdrive_enabled: Option<bool>, pub amdgpu_overdrive_enabled: Option<bool>,
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DeviceListEntry<'a> { pub struct DeviceListEntry {
pub id: &'a str, pub id: String,
pub name: Option<&'a str>, pub name: Option<String>,
}
impl fmt::Display for DeviceListEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.name {
Some(name) => name.fmt(f),
None => self.id.fmt(f),
}
}
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]