feat: improve pstates UI (#896)

* move state_row to dedicated component

* remove selectable behavior

* highlight active pstate

* hide checkboxes when not configurable
use monospace for text

* hide pstates table when there is no data

* prevents container width jumps when active row change

* add page_section_expander and wrap PowerStatesFrame into it

* move active indicator to the beginning and use opacity instead of visilibity to keep layout stable

* improve listbox captions styles

* remove states_expanded state from PowerStatesFrame

* remove no longer relevant styles

* improve margins

* move SelectionMode to view, kinda

* clean up

* align expaner label

* organize imports

* hide active p-state indicator if non is active

* use bindings for configurable
update label

* fix PowerStateRow props visibility

* move bindings inits

* fix set_sensitive on enable-pstate-config checkbox
This commit is contained in:
Roman Makarov
2026-02-15 11:55:53 +02:00
committed by GitHub
parent 8ac95192cb
commit b4740fdeca
10 changed files with 303 additions and 96 deletions
+1 -1
View File
@@ -180,7 +180,7 @@ mem-pstate-clock-voltage = VRAM P-State {$pstate} Voltage (mV)
pstates = Power States
gpu-pstates = GPU Power States
vram-pstates = VRAM Power States
pstates-manual-needed = Note: performance level must be set to 'manual' to toggle power states
pstates-manual-needed = Performance level must be set to 'manual' to toggle power states
enable-pstate-config = Enable power state configuration
show-historical-charts = Show Historical Charts
+1
View File
@@ -9,6 +9,7 @@ mod info_row_level;
pub(crate) mod msg;
mod overdrive_dialog;
mod page_section;
mod page_section_expander;
pub(crate) mod pages;
mod process_monitor;
pub(crate) mod styles;
+2 -1
View File
@@ -118,6 +118,8 @@ impl Default for InfoRow {
}
mod imp {
use std::{cell::RefCell, str::FromStr};
use glib::Properties;
use gtk::{
Label, glib,
@@ -126,7 +128,6 @@ mod imp {
subclass::{prelude::*, widget::WidgetImpl},
};
use relm4::{RelmWidgetExt, css, view};
use std::{cell::RefCell, str::FromStr};
#[derive(Default, Properties)]
#[properties(wrapper_type = super::InfoRow)]
+4 -2
View File
@@ -24,11 +24,13 @@ impl Default for InfoRowLevel {
}
mod imp {
use crate::app::info_row::{InfoRow, InfoRowExt};
use std::cell::RefCell;
use glib::Properties;
use gtk::{LevelBar, glib, prelude::*, subclass::prelude::*};
use relm4::view;
use std::cell::RefCell;
use crate::app::info_row::{InfoRow, InfoRowExt};
#[derive(Default, Properties)]
#[properties(wrapper_type = super::InfoRowLevel)]
+2 -1
View File
@@ -32,6 +32,8 @@ impl PageSection {
unsafe impl<T: ObjectSubclass + BoxImpl> IsSubclassable<T> for PageSection {}
mod imp {
use std::cell::RefCell;
use glib::Properties;
use gtk::{
Label,
@@ -40,7 +42,6 @@ mod imp {
subclass::{prelude::*, widget::WidgetImpl},
};
use relm4::{RelmWidgetExt, css, view};
use std::cell::RefCell;
#[derive(Default, Properties)]
#[properties(wrapper_type = super::PageSection)]
+132
View File
@@ -0,0 +1,132 @@
use cairo::glib::subclass::types::ObjectSubclassIsExt;
use gtk::prelude::*;
use gtk::{
glib::{
self, Object,
subclass::types::{IsSubclassable, ObjectSubclass},
},
subclass::box_::BoxImpl,
};
glib::wrapper! {
pub struct PageSectionExpander(ObjectSubclass<imp::PageSectionExpander>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Orientable, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}
impl PageSectionExpander {
pub fn new(name: &str) -> Self {
Object::builder().property("name", name).build()
}
pub fn append_header(&self, widget: &impl IsA<gtk::Widget>) {
self.imp().header_box.append(widget);
}
pub fn append_expandable(&self, widget: &impl IsA<gtk::Widget>) {
self.imp().children_box.append(widget);
}
}
unsafe impl<T: ObjectSubclass + BoxImpl> IsSubclassable<T> for PageSectionExpander {}
mod imp {
use std::cell::RefCell;
use glib::Properties;
use gtk::{
Label,
glib::{self},
prelude::*,
subclass::{prelude::*, widget::WidgetImpl},
};
use relm4::{RelmWidgetExt, view};
#[derive(Default, Properties)]
#[properties(wrapper_type = super::PageSectionExpander)]
pub struct PageSectionExpander {
section_label: Label,
pub(super) header_box: gtk::Box,
pub(super) content_box: gtk::Box,
pub(super) children_box: gtk::Box,
pub(super) expander: gtk::Expander,
#[property(get, set)]
name: RefCell<String>,
#[property(get, set)]
expanded: std::cell::Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for PageSectionExpander {
const NAME: &'static str = "PageSectionExpander";
type Type = super::PageSectionExpander;
type ParentType = gtk::Box;
}
#[glib::derived_properties]
impl ObjectImpl for PageSectionExpander {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
let section_label = &self.section_label;
let header_box = &self.header_box;
let content_box = &self.content_box;
let children_box = &self.children_box;
let expander = &self.expander;
view! {
#[local_ref]
obj {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
#[local_ref]
append = expander {
set_child: Some(content_box),
set_label_widget : Some(header_box),
},
},
#[local_ref]
header_box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 10,
#[local_ref]
append = section_label {
set_use_markup: true,
set_halign: gtk::Align::Start,
set_margin_vertical: 5,
// align label with normal PageSection
set_margin_start: -1,
set_margin_end: 1,
},
},
#[local_ref]
content_box {
set_orientation: gtk::Orientation::Vertical,
#[local_ref]
append = children_box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 10,
set_margin_all: 10,
},
},
}
obj.bind_property("name", &self.section_label, "label")
.transform_to(|_, value: String| {
Some(format!("<span font_desc='13'><b>{value}</b></span>"))
})
.build();
}
}
impl WidgetImpl for PageSectionExpander {}
impl BoxImpl for PageSectionExpander {}
}
@@ -1,2 +1,3 @@
pub mod power_states_frame;
mod power_states_list;
mod power_states_row;
@@ -3,6 +3,7 @@ use crate::{
APP_BROKER, I18N,
app::{
msg::AppMsg,
page_section_expander::PageSectionExpander,
pages::oc_page::power_states::power_states_list::{
PowerStatesListMsg, PowerStatesListOptions,
},
@@ -17,8 +18,9 @@ use i18n_embed_fl::fl;
use indexmap::IndexMap;
use lact_schema::{DeviceStats, PowerStates};
use relm4::{
Component, ComponentController, ComponentParts, ComponentSender, RelmObjectExt, RelmWidgetExt,
binding::BoolBinding,
Component, ComponentController, ComponentParts, ComponentSender, RelmObjectExt,
binding::{Binding, BoolBinding},
css,
};
use std::sync::Arc;
@@ -27,7 +29,6 @@ pub struct PowerStatesFrame {
vram_states_list: relm4::Controller<PowerStatesList>,
states_configurable: BoolBinding,
states_configured: BoolBinding,
states_expanded: BoolBinding,
performance_level: Option<PerformanceLevel>,
configured_signal: SignalHandlerId,
vram_clock_ratio: f64,
@@ -43,6 +44,7 @@ pub enum PowerStatesFrameMsg {
PerformanceLevel(Option<PerformanceLevel>),
VramClockRatio(f64),
Configurable(bool),
InternalConfigurableChanged(bool),
}
#[relm4::component(pub)]
@@ -52,20 +54,14 @@ impl relm4::SimpleComponent for PowerStatesFrame {
type Output = ();
view! {
gtk::Expander {
set_label: Some(&fl!(I18N, "pstates")),
add_binding: (&model.states_expanded, "expanded"),
set_margin_horizontal: 20,
gtk::Box {
PageSectionExpander::new(&fl!(I18N, "pstates")) {
append_expandable = &gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_margin_all: 10,
set_spacing: 5,
add_binding: (&model.states_configurable, "sensitive"),
gtk::Label {
set_label: &fl!(I18N, "pstates-manual-needed"),
set_margin_horizontal: 10,
add_css_class: css::DIM_LABEL,
set_halign: gtk::Align::Start,
#[watch]
set_visible: model.performance_level.is_some_and(|level| level != PerformanceLevel::Manual),
@@ -76,15 +72,25 @@ impl relm4::SimpleComponent for PowerStatesFrame {
add_binding: (&model.states_configured, "active"),
#[watch]
set_visible: model.performance_level.is_some(),
#[watch]
set_sensitive: model.performance_level.is_some_and(|level| level == PerformanceLevel::Manual),
},
gtk::Box {
set_spacing: 10,
set_orientation: gtk::Orientation::Horizontal,
add_binding: (&model.states_configured, "sensitive"),
append = model.core_states_list.widget(),
append = model.vram_states_list.widget(),
gtk::Box {
#[watch]
set_visible: !model.core_states_list.model().is_empty(),
append = model.core_states_list.widget(),
},
gtk::Box {
#[watch]
set_visible: !model.vram_states_list.model().is_empty(),
append = model.vram_states_list.widget(),
},
}
}
}
@@ -93,7 +99,7 @@ impl relm4::SimpleComponent for PowerStatesFrame {
fn init(
_: Self::Init,
root: Self::Root,
_sender: ComponentSender<Self>,
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let core_states_list = PowerStatesList::builder()
.launch(PowerStatesListOptions {
@@ -110,7 +116,10 @@ impl relm4::SimpleComponent for PowerStatesFrame {
let states_configured = BoolBinding::new(false);
let configured_signal = states_configured.connect_value_notify(|_| {
let configured_signal = states_configured.connect_value_notify(move |states_configured| {
sender.input(PowerStatesFrameMsg::InternalConfigurableChanged(
states_configured.get(),
));
APP_BROKER.send(AppMsg::SettingsChanged);
});
@@ -120,7 +129,6 @@ impl relm4::SimpleComponent for PowerStatesFrame {
states_configurable: BoolBinding::new(false),
states_configured,
configured_signal,
states_expanded: BoolBinding::new(false),
performance_level: None,
vram_clock_ratio: 1.0,
};
@@ -163,6 +171,11 @@ impl relm4::SimpleComponent for PowerStatesFrame {
|| !self.vram_states_list.model().is_empty());
self.states_configurable.set_value(value);
self.core_states_list
.emit(PowerStatesListMsg::Configurable(value));
self.vram_states_list
.emit(PowerStatesListMsg::Configurable(value));
if !value {
self.states_configured.block_signal(&self.configured_signal);
self.states_configured.set_value(false);
@@ -173,6 +186,12 @@ impl relm4::SimpleComponent for PowerStatesFrame {
PowerStatesFrameMsg::PerformanceLevel(level) => {
self.performance_level = level;
}
PowerStatesFrameMsg::InternalConfigurableChanged(configurable) => {
self.core_states_list
.emit(PowerStatesListMsg::Configurable(configurable));
self.vram_states_list
.emit(PowerStatesListMsg::Configurable(configurable));
}
}
}
}
@@ -1,14 +1,18 @@
use gtk::prelude::{BoxExt, CheckButtonExt, FrameExt, OrientableExt, WidgetExt};
use crate::app::pages::oc_page::power_states::power_states_row::{
PowerStateRow, PowerStateRowMsg, PowerStateRowOptions,
};
use gtk::prelude::{FrameExt, WidgetExt};
use lact_schema::PowerState;
use relm4::{
ComponentParts, ComponentSender, RelmObjectExt, binding::BoolBinding, prelude::FactoryVecDeque,
ComponentParts, ComponentSender, RelmWidgetExt, binding::BoolBinding, css,
prelude::FactoryVecDeque,
};
use crate::{APP_BROKER, app::msg::AppMsg};
pub struct PowerStatesList {
states: FactoryVecDeque<PowerStateRow>,
value_suffix: String,
is_active_indicator_visible: BoolBinding,
configurable: BoolBinding,
}
pub struct PowerStatesListOptions {
@@ -20,6 +24,7 @@ pub struct PowerStatesListOptions {
pub enum PowerStatesListMsg {
PowerStates(Vec<PowerState>, f64),
ActiveState(Option<usize>),
Configurable(bool),
}
#[relm4::component(pub)]
@@ -31,8 +36,16 @@ impl relm4::SimpleComponent for PowerStatesList {
view! {
gtk::Frame {
set_hexpand: true,
set_label: Some(&opts.title),
set_child: Some(model.states.widget()),
#[wrap(Some)]
set_label_widget = &gtk::Label {
set_label: &opts.title,
set_margin_horizontal: 5,
add_css_class: css::CAPTION_HEADING,
},
#[local_ref]
states_widget -> gtk::ListBox {
set_selection_mode: gtk::SelectionMode::None,
},
}
}
@@ -46,8 +59,12 @@ impl relm4::SimpleComponent for PowerStatesList {
let model = Self {
states,
value_suffix: opts.value_suffix,
is_active_indicator_visible: BoolBinding::new(false),
configurable: BoolBinding::new(true),
};
let states_widget = model.states.widget();
let widgets = view_output!();
ComponentParts { widgets, model }
@@ -65,20 +82,27 @@ impl relm4::SimpleComponent for PowerStatesList {
power_state,
value_suffix: self.value_suffix.clone(),
active: false,
show_active_indicator: self.is_active_indicator_visible.clone(),
configurable: self.configurable.clone(),
};
states.push_back(opts);
}
}
PowerStatesListMsg::ActiveState(active_idx) => {
self.is_active_indicator_visible
.set_value(active_idx.is_some());
for (i, row) in self.states.iter().enumerate() {
let is_active = row
.power_state
.index
.is_some_and(|index| Some(usize::from(index)) == active_idx);
self.states.send(i, is_active);
self.states.send(i, PowerStateRowMsg::Active(is_active));
}
}
PowerStatesListMsg::Configurable(configurable) => {
self.configurable.set_value(configurable);
}
}
}
}
@@ -96,69 +120,3 @@ impl PowerStatesList {
self.states.is_empty()
}
}
struct PowerStateRow {
active: BoolBinding,
enabled: BoolBinding,
power_state: PowerState,
value_suffix: String,
}
pub struct PowerStateRowOptions {
pub power_state: PowerState,
pub value_suffix: String,
pub active: bool,
}
#[relm4::factory]
impl relm4::factory::FactoryComponent for PowerStateRow {
type ParentWidget = gtk::ListBox;
type CommandOutput = ();
type Input = bool;
type Output = ();
type Init = PowerStateRowOptions;
view! {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
append = &gtk::CheckButton {
set_hexpand: true,
add_binding: (&self.enabled, "active"),
set_label: {
let value_text = match self.power_state.min_value {
Some(min) if min != self.power_state.value => format!("{min}-{}", self.power_state.value),
_ => self.power_state.value.to_string(),
};
Some(format!("{}: {value_text} {}", index.current_index(), self.value_suffix))
}.as_deref(),
},
append: image = &gtk::Image {
set_icon_name: Some("pan-start-symbolic"),
add_binding: (&self.active, "visible"),
},
}
}
fn init_model(
opts: Self::Init,
_index: &Self::Index,
_sender: relm4::FactorySender<Self>,
) -> Self {
let enabled = BoolBinding::new(opts.power_state.enabled);
enabled.connect_value_notify(|_| APP_BROKER.send(AppMsg::SettingsChanged));
Self {
enabled,
active: BoolBinding::new(opts.active),
power_state: opts.power_state,
value_suffix: opts.value_suffix,
}
}
fn update(&mut self, active: Self::Input, _: relm4::FactorySender<Self>) {
self.active.set_value(active);
}
}
@@ -0,0 +1,92 @@
use crate::{APP_BROKER, app::msg::AppMsg};
use gtk::prelude::{BoxExt, OrientableExt, WidgetExt};
use lact_schema::PowerState;
use relm4::{RelmObjectExt, RelmWidgetExt, binding::BoolBinding, css};
pub struct PowerStateRow {
active: BoolBinding,
pub(super) enabled: BoolBinding,
pub(super) power_state: PowerState,
value_suffix: String,
configurable: BoolBinding,
show_active_indicator: BoolBinding,
}
pub struct PowerStateRowOptions {
pub power_state: PowerState,
pub value_suffix: String,
pub active: bool,
pub show_active_indicator: BoolBinding,
pub configurable: BoolBinding,
}
#[derive(Clone, Debug)]
pub enum PowerStateRowMsg {
Active(bool),
}
#[relm4::factory(pub)]
impl relm4::factory::FactoryComponent for PowerStateRow {
type ParentWidget = gtk::ListBox;
type CommandOutput = ();
type Input = PowerStateRowMsg;
type Output = ();
type Init = PowerStateRowOptions;
view! {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
set_margin_vertical: 2,
set_margin_horizontal: 5,
append: image = &gtk::Image {
set_icon_name: Some("pan-end-symbolic"),
add_binding: (&self.show_active_indicator, "visible"),
#[watch]
set_opacity: if self.active.value() { 1.0 } else { 0.0 },
},
append = &gtk::CheckButton {
add_binding: (&self.enabled, "active"),
add_binding: (&self.configurable, "visible"),
},
append = &gtk::Label {
add_css_class: css::MONOSPACE,
#[watch]
set_class_active: (css::DIM_LABEL, !self.active.value()),
set_label: &{
let value_text = match self.power_state.min_value {
Some(min) if min != self.power_state.value => format!("{min}-{}", self.power_state.value),
_ => self.power_state.value.to_string(),
};
format!("{}: {value_text} {}", index.current_index(), self.value_suffix)
},
},
}
}
fn init_model(
opts: Self::Init,
_index: &Self::Index,
_sender: relm4::FactorySender<Self>,
) -> Self {
let enabled = BoolBinding::new(opts.power_state.enabled);
enabled.connect_value_notify(|_| APP_BROKER.send(AppMsg::SettingsChanged));
Self {
enabled,
active: BoolBinding::new(opts.active),
power_state: opts.power_state,
value_suffix: opts.value_suffix,
configurable: opts.configurable,
show_active_indicator: opts.show_active_indicator,
}
}
fn update(&mut self, msg: Self::Input, _: relm4::FactorySender<Self>) {
let PowerStateRowMsg::Active(active) = msg;
self.active.set_value(active);
}
}