feat: inline power states (#1213)

* feat: inline power states

* markup fixes

* polish p-states row

* align the p-state values by right side

* refactor

---------

Co-authored-by: Ilya Zlobintsev <ilya.zl@protonmail.com>
This commit is contained in:
Roman MakarovandIlya Zlobintsev authored and GitHub committed 2026-10-04 12:53:53 +03:00
1 parent 6a2e669a2f
commit 8d7361a4a9
11 files changed
+336 -161

No files matched your search

+1
View File
@@ -217,6 +217,7 @@ pstate-clock-offset = P-State {$pstate} Clock Offset
pstate-clock = P-State {$pstate} Clock
pstate-clock-voltage = P-State {$pstate} Voltage
pstate = Power State
pstates = Power States
gpu-pstates = GPU Power States
vram-pstates = VRAM Power States
@@ -1,15 +1,29 @@
.adjustment-list > row {
padding: 10px;
}
.adjustment-list > row.adjustment-row {
padding: 5px 10px;
}
.adjustment-list > row:first-child {
.adjustment-list > row.adjustment-row:first-child {
padding-top: 10px;
}
.adjustment-list > row:last-child {
.adjustment-list > row.adjustment-row:last-child {
padding-bottom: 10px;
}
/* The card owns row spacing, including Adwaita action rows. */
.adjustment-list > row > box.header {
margin: 0;
min-height: 0;
}
.adjustment-list > row > box.header > box.title {
margin: 0;
}
.adjustment-row scale {
padding: 10px 0;
}
@@ -75,11 +75,10 @@ impl<Key: 'static> FactoryComponent for AdjustmentRow<Key> {
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
add_css_class: "adjustment-row",
gtk::Box {
set_orientation: gtk::Orientation::Vertical,
set_spacing: 5,
add_css_class: "adjustment-row",
gtk::Box {
set_spacing: 12,
+11
View File
@@ -2,3 +2,14 @@
min-width: 400px;
padding: 0;
}
.power-states-dialog-card list:not(.boxed-list) {
/* Embedded lists use the surrounding card's background, not the view background. */
background: none;
border-radius: 12px;
}
.power-states-dialog-card .page-section-children-box,
.power-states-dialog-card .adjustment-card-advanced-features {
margin: 0;
}
+38 -25
View File
@@ -24,7 +24,7 @@ use lact_schema::{ClocksTable, DeviceInfo, PowerStates};
use nvml_wrapper::enums::device::PowerMizerMode;
use performance_frame::PerformanceFrameMsg;
use power_frame::{PowerFrame, PowerFrameMsg};
use power_states::power_states_frame::{PowerStatesFrame, PowerStatesFrameMsg};
use power_states::power_states_dialog::{PowerStatesDialog, PowerStatesDialogMsg};
use relm4::binding::BoolBinding;
use relm4::{ComponentController, ComponentParts, ComponentSender, RelmWidgetExt};
use std::collections::HashSet;
@@ -37,7 +37,7 @@ pub struct OcPage {
device_info: Option<Arc<DeviceInfo>>,
power_frame: relm4::Controller<PowerFrame>,
power_states_frame: relm4::Controller<PowerStatesFrame>,
power_states_dialog: relm4::Controller<PowerStatesDialog>,
gpu_clocks_frame: relm4::Controller<ClocksFrame>,
vram_clocks_frame: relm4::Controller<ClocksFrame>,
@@ -62,6 +62,7 @@ pub enum OcPageMsg {
PerformanceLevelChanged,
SetPerformanceLevel(PerformanceLevel),
EnablePstateConfig,
ShowPowerStates(ClockDomain),
ShowVfCurveEditor,
VfCurveEditingToggled(bool),
}
@@ -87,8 +88,6 @@ impl relm4::Component for OcPage {
ColumnBias::Left, // Core
ColumnBias::Right, // VRAM
ColumnBias::Left, // Power
ColumnBias::Right, // Power States
])),
set_valign: gtk::Align::Start,
@@ -103,10 +102,6 @@ impl relm4::Component for OcPage {
model.power_frame.widget() {
add_css_class: "oc-page-section",
},
model.power_states_frame.widget() {
add_css_class: "oc-page-section",
},
},
},
}
@@ -145,8 +140,8 @@ impl relm4::Component for OcPage {
show_all_pstates: BoolBinding::new(false),
})
.forward(sender.input_sender(), |msg| msg);
let power_states_frame =
PowerStatesFrame::launch_default().forward(sender.input_sender(), |msg| msg);
let power_states_dialog =
PowerStatesDialog::launch_default().forward(sender.input_sender(), |msg| msg);
let power_frame = PowerFrame::launch_default().forward(sender.input_sender(), |msg| msg);
let vf_curve_editor = VfCurveEditor::detach(VfCurveEditorInit {
@@ -158,7 +153,7 @@ impl relm4::Component for OcPage {
stats_section,
device_info: None,
power_frame,
power_states_frame,
power_states_dialog,
gpu_clocks_frame,
vram_clocks_frame,
vf_curve_editor,
@@ -174,13 +169,17 @@ impl relm4::Component for OcPage {
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
_root: &Self::Root,
root: &Self::Root,
) {
match msg {
OcPageMsg::Update { update, initial } => match &update {
PageUpdate::Stats(stats) => {
self.power_states_frame
.emit(PowerStatesFrameMsg::Stats(stats.clone()));
self.power_states_dialog
.emit(PowerStatesDialogMsg::Stats(stats.clone()));
self.gpu_clocks_frame
.emit(ClocksFrameMsg::Stats(stats.clone()));
self.vram_clocks_frame
.emit(ClocksFrameMsg::Stats(stats.clone()));
self.stats_section
.emit(GpuStatsSectionMsg::Stats(stats.clone()));
@@ -208,8 +207,8 @@ impl relm4::Component for OcPage {
self.device_info = Some(info.clone());
self.stats_section
.emit(GpuStatsSectionMsg::Info(info.clone()));
self.power_states_frame
.emit(PowerStatesFrameMsg::VramClockRatio(vram_clock_ratio));
self.power_states_dialog
.emit(PowerStatesDialogMsg::VramClockRatio(vram_clock_ratio));
self.vram_clocks_frame
.emit(ClocksFrameMsg::VramRatio(vram_clock_ratio));
}
@@ -240,11 +239,19 @@ impl relm4::Component for OcPage {
pstates,
configured,
} => {
self.power_states_frame
.emit(PowerStatesFrameMsg::PowerStates {
self.power_states_dialog
.emit(PowerStatesDialogMsg::PowerStates {
pstates: pstates.clone(),
configured,
});
self.gpu_clocks_frame
.emit(ClocksFrameMsg::PowerStatesAvailable(
!pstates.core.is_empty(),
));
self.vram_clocks_frame
.emit(ClocksFrameMsg::PowerStatesAvailable(
!pstates.vram.is_empty(),
));
self.stats_section
.emit(GpuStatsSectionMsg::PowerStates(Arc::new(pstates)));
sender.input(OcPageMsg::PerformanceLevelChanged);
@@ -252,13 +259,13 @@ impl relm4::Component for OcPage {
OcPageMsg::PerformanceLevelChanged => {
let custom_pstates_configurable =
self.get_performance_level() == Some(PerformanceLevel::Manual);
self.power_states_frame
.emit(PowerStatesFrameMsg::Configurable(
self.power_states_dialog
.emit(PowerStatesDialogMsg::Configurable(
custom_pstates_configurable,
));
self.power_states_frame
.emit(PowerStatesFrameMsg::PerformanceLevel(
self.power_states_dialog
.emit(PowerStatesDialogMsg::PerformanceLevel(
self.get_performance_level(),
));
}
@@ -269,8 +276,14 @@ impl relm4::Component for OcPage {
APP_BROKER.send(AppMsg::SettingsChanged);
}
OcPageMsg::EnablePstateConfig => {
self.power_states_frame
.emit(PowerStatesFrameMsg::EnableWithManualPerformanceLevel);
self.power_states_dialog
.emit(PowerStatesDialogMsg::EnableWithManualPerformanceLevel);
}
OcPageMsg::ShowPowerStates(domain) => {
self.power_states_dialog.emit(PowerStatesDialogMsg::Show {
domain,
parent: root.clone().upcast(),
});
}
OcPageMsg::ShowVfCurveEditor => {
self.vf_curve_editor.emit(VfCurveEditorMsg::Show);
@@ -329,7 +342,7 @@ impl OcPage {
pub fn get_enabled_power_states(&self) -> IndexMap<PowerLevelKind, Vec<u8>> {
if self.get_performance_level() == Some(PerformanceLevel::Manual) {
self.power_states_frame.model().get_enabled_power_states()
self.power_states_dialog.model().get_enabled_power_states()
} else {
IndexMap::new()
}
@@ -0,0 +1,4 @@
.adjustment-list > row.pstates {
padding-top: 15px;
padding-bottom: 15px;
}
@@ -11,10 +11,10 @@ use crate::{
},
};
use adw::prelude::*;
use amdgpu_sysfs::gpu_handle::overdrive::ClocksTableGen as AmdClocksTable;
use amdgpu_sysfs::gpu_handle::{PowerLevelId, overdrive::ClocksTableGen as AmdClocksTable};
use i18n_embed_fl::fl;
use lact_schema::{
ClocksTable, IntelClocksTable, NvidiaClockOffset, NvidiaClocksTable,
ClocksTable, DeviceStats, IntelClocksTable, NvidiaClockOffset, NvidiaClocksTable,
request::{ClockspeedType, SetClocksCommand},
};
use relm4::{
@@ -35,6 +35,9 @@ pub struct ClocksFrame {
show_all_pstates: BoolBinding,
vf_curve_editing: BoolBinding,
enable_locked_clocks: BoolBinding,
has_power_states: bool,
active_power_state: Option<PowerLevelId>,
current_clock: Option<u64>,
}
#[derive(Default)]
@@ -99,6 +102,8 @@ pub enum ClocksFrameMsg {
vf_curve_is_configured: bool,
},
VramRatio(f64),
PowerStatesAvailable(bool),
Stats(Arc<DeviceStats>),
TogglePStatesVisibility,
ResetGpuClockOffsets,
}
@@ -119,7 +124,9 @@ impl relm4::Component for ClocksFrame {
ClockDomain::Vram => fl!(I18N, "vram-section"),
},
#[watch]
set_visible: model.domain == ClockDomain::Gpu || model.has_any_clocks(),
set_visible: model.domain == ClockDomain::Gpu
|| model.has_any_clocks()
|| model.has_power_states,
append_header = &gtk::Box {
set_spacing: 10,
@@ -245,6 +252,43 @@ impl relm4::Component for ClocksFrame {
set_halign: gtk::Align::Start,
},
},
adw::ActionRow {
set_title: &fl!(I18N, "pstate"),
set_activatable: true,
add_css_class: "pstates",
#[watch]
set_visible: model.has_power_states,
connect_activated[sender, domain = model.domain] => move |_| {
sender.output(OcPageMsg::ShowPowerStates(domain)).unwrap();
},
add_suffix = &gtk::Box {
set_spacing: 4,
set_valign: gtk::Align::Center,
gtk::Label {
add_css_class: css::HEADING,
#[watch]
set_label: &match model.active_power_state {
Some(PowerLevelId::Index(index)) => format!("P{index}"),
Some(PowerLevelId::Sleep) => "S".to_owned(),
None => "N/A".to_owned(),
},
},
gtk::Label {
add_css_class: css::DIM_LABEL,
#[watch]
set_label: &model.power_state_clock_label(),
},
},
add_suffix = &gtk::Image {
set_icon_name: Some("go-next-symbolic"),
add_css_class: css::DIM_LABEL,
},
},
},
},
}
@@ -272,6 +316,9 @@ impl relm4::Component for ClocksFrame {
show_all_pstates,
vf_curve_editing,
enable_locked_clocks: BoolBinding::new(false),
has_power_states: false,
active_power_state: None,
current_clock: None,
};
for binding in [
@@ -287,6 +334,13 @@ impl relm4::Component for ClocksFrame {
let widgets = view_output!();
model.adjustments.widget().set_sort_func(|left, right| {
// Keep the pstates row after dynamically inserted clock controls.
left.has_css_class("pstates")
.cmp(&right.has_css_class("pstates"))
.into()
});
ComponentParts { model, widgets }
}
@@ -363,6 +417,16 @@ impl relm4::Component for ClocksFrame {
self.vram_clock_ratio = vram_ratio;
self.update_vram_clock_ratio();
}
ClocksFrameMsg::PowerStatesAvailable(available) => {
self.has_power_states = available;
}
ClocksFrameMsg::Stats(stats) => {
let active_states = stats.active_power_states.unwrap_or_default();
(self.active_power_state, self.current_clock) = match self.domain {
ClockDomain::Gpu => (active_states.core, stats.clockspeed.gpu_clockspeed),
ClockDomain::Vram => (active_states.memory, stats.clockspeed.vram_clockspeed),
};
}
ClocksFrameMsg::TogglePStatesVisibility => {
if !self.secondary_p_states_enabled() && self.show_all_pstates.value() {
self.show_all_pstates.set_value(false);
@@ -399,6 +463,19 @@ impl relm4::Component for ClocksFrame {
}
impl ClocksFrame {
fn power_state_clock_label(&self) -> String {
match self.current_clock {
Some(clock) => {
let ratio = match self.domain {
ClockDomain::Gpu => 1.0,
ClockDomain::Vram => self.vram_clock_ratio,
};
format!("{:.0} {}", clock as f64 * ratio, fl!(I18N, "mhz"))
}
None => "N/A".to_owned(),
}
}
fn secondary_p_states_enabled(&self) -> bool {
!self.show_nvidia_options || CONFIG.read().experimental_nvidia_pstate_offsets
}
@@ -1,3 +1,3 @@
pub mod power_states_frame;
pub mod power_states_dialog;
mod power_states_list;
mod power_states_row;
@@ -5,7 +5,7 @@ use crate::{
app::{
components::{adjustment_card::AdjustmentCard, page_section::PageSection},
msg::AppMsg,
pages::oc_page::OcPageMsg,
pages::oc_page::{OcPageMsg, clocks_frame::ClockDomain},
utils::ext::RelmLaunchable as _,
},
};
@@ -16,12 +16,13 @@ use i18n_embed_fl::fl;
use indexmap::IndexMap;
use lact_schema::{DeviceStats, PowerStates};
use relm4::{
ComponentController, ComponentParts, ComponentSender,
ComponentController, ComponentParts, ComponentSender, RelmWidgetExt,
binding::{Binding, BoolBinding},
};
use std::sync::Arc;
pub struct PowerStatesFrame {
pub struct PowerStatesDialog {
domain: ClockDomain,
core_states_list: relm4::Controller<PowerStatesList>,
vram_states_list: relm4::Controller<PowerStatesList>,
states_configurable: BoolBinding,
@@ -34,7 +35,11 @@ pub struct PowerStatesFrame {
}
#[derive(Debug)]
pub enum PowerStatesFrameMsg {
pub enum PowerStatesDialogMsg {
Show {
domain: ClockDomain,
parent: gtk::Widget,
},
PowerStates {
pstates: PowerStates,
configured: bool,
@@ -51,69 +56,83 @@ pub enum PowerStatesFrameMsg {
}
#[relm4::component(pub)]
impl relm4::SimpleComponent for PowerStatesFrame {
impl relm4::Component for PowerStatesDialog {
type Init = ();
type Input = PowerStatesFrameMsg;
type Input = PowerStatesDialogMsg;
type Output = OcPageMsg;
type CommandOutput = ();
view! {
PageSection {
set_name: fl!(I18N, "pstates"),
set_hide_visible_container: true,
#[template]
append_child = &AdjustmentCard {
#[template_child]
advanced_features {
#[watch]
set_visible: model.performance_level.is_some(),
},
adw::Dialog {
set_content_width: 420,
set_follows_content_size: true,
#[watch]
set_title: &match model.domain {
ClockDomain::Gpu => fl!(I18N, "gpu-pstates"),
ClockDomain::Vram => fl!(I18N, "vram-pstates"),
},
#[template_child]
controls {
gtk::ToggleButton {
set_halign: gtk::Align::Start,
add_css_class: "adjustment-card-option-toggle",
#[wrap(Some)]
set_child = &adw::ToolbarView {
add_top_bar = &adw::HeaderBar {},
#[watch]
#[block_signal(configured_toggled_handler)]
set_active: model.states_configuration_enabled.value(),
#[wrap(Some)]
set_content = &gtk::ScrolledWindow {
set_hscrollbar_policy: gtk::PolicyType::Never,
set_propagate_natural_height: true,
set_max_content_height: 500,
connect_toggled[sender] => move |button| {
sender.input(PowerStatesFrameMsg::ConfiguredToggled {
configured: button.is_active(),
});
} @ configured_toggled_handler,
PageSection {
set_hide_visible_container: true,
set_margin_all: 15,
add_css_class: "power-states-dialog-card",
#[wrap(Some)]
set_child = &gtk::Box {
gtk::Label {
set_label: &fl!(I18N, "enable-pstate-config"),
},
},
},
},
#[template_child]
content {
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
gtk::Box {
set_spacing: 10,
set_orientation: gtk::Orientation::Horizontal,
set_homogeneous: true,
gtk::Box {
#[template]
append_child = &AdjustmentCard {
#[template_child]
advanced_features {
#[watch]
set_visible: model.has_core_states,
append = model.core_states_list.widget(),
set_visible: model.performance_level.is_some(),
},
gtk::Box {
#[watch]
set_visible: model.has_vram_states,
append = model.vram_states_list.widget(),
#[template_child]
controls {
gtk::ToggleButton {
set_halign: gtk::Align::Start,
add_css_class: "adjustment-card-option-toggle",
set_label: &fl!(I18N, "enable-pstate-config"),
#[watch]
#[block_signal(configured_toggled_handler)]
set_active: model.states_configuration_enabled.value(),
connect_toggled[sender] => move |button| {
sender.input(PowerStatesDialogMsg::ConfiguredToggled {
configured: button.is_active(),
});
} @ configured_toggled_handler,
},
},
#[template_child]
content {
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
#[watch]
set_visible: model.domain == ClockDomain::Gpu && model.has_core_states,
#[wrap(Some)]
set_child = model.core_states_list.widget(),
},
gtk::ListBoxRow {
set_activatable: false,
set_selectable: false,
#[watch]
set_visible: model.domain == ClockDomain::Vram && model.has_vram_states,
#[wrap(Some)]
set_child = model.vram_states_list.widget(),
},
},
},
},
@@ -128,11 +147,9 @@ impl relm4::SimpleComponent for PowerStatesFrame {
sender: ComponentSender<Self>,
) -> ComponentParts<Self> {
let core_states_list = PowerStatesList::detach(PowerStatesListOptions {
title: fl!(I18N, "gpu-pstates"),
value_suffix: fl!(I18N, "mhz"),
});
let vram_states_list = PowerStatesList::detach(PowerStatesListOptions {
title: fl!(I18N, "vram-pstates"),
value_suffix: fl!(I18N, "mhz"),
});
@@ -141,13 +158,14 @@ impl relm4::SimpleComponent for PowerStatesFrame {
let configured_sender = sender.clone();
let configured_signal =
states_configuration_enabled.connect_value_notify(move |states_configured| {
configured_sender.input(PowerStatesFrameMsg::InternalConfigurableChanged(
configured_sender.input(PowerStatesDialogMsg::InternalConfigurableChanged(
states_configured.get(),
));
APP_BROKER.send(AppMsg::SettingsChanged);
});
let model = Self {
domain: ClockDomain::Gpu,
core_states_list,
vram_states_list,
states_configurable: BoolBinding::new(false),
@@ -164,9 +182,20 @@ impl relm4::SimpleComponent for PowerStatesFrame {
ComponentParts { model, widgets }
}
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
fn update_with_view(
&mut self,
widgets: &mut Self::Widgets,
msg: Self::Input,
sender: ComponentSender<Self>,
root: &Self::Root,
) {
match msg {
PowerStatesFrameMsg::PowerStates {
PowerStatesDialogMsg::Show { domain, parent } => {
self.domain = domain;
self.update_view(widgets, sender.clone());
root.present(Some(&parent));
}
PowerStatesDialogMsg::PowerStates {
pstates,
configured,
} => {
@@ -186,7 +215,7 @@ impl relm4::SimpleComponent for PowerStatesFrame {
self.vram_clock_ratio,
));
}
PowerStatesFrameMsg::Stats(stats) => {
PowerStatesDialogMsg::Stats(stats) => {
self.core_states_list.emit(PowerStatesListMsg::ActiveState(
stats.active_power_states.and_then(|states| states.core),
));
@@ -194,10 +223,12 @@ impl relm4::SimpleComponent for PowerStatesFrame {
stats.active_power_states.and_then(|states| states.memory),
));
}
PowerStatesFrameMsg::VramClockRatio(ratio) => {
PowerStatesDialogMsg::VramClockRatio(ratio) => {
self.vram_clock_ratio = ratio;
self.vram_states_list
.emit(PowerStatesListMsg::ValueRatio(ratio));
}
PowerStatesFrameMsg::Configurable(is_plvl_manual) => {
PowerStatesDialogMsg::Configurable(is_plvl_manual) => {
let configurable = is_plvl_manual && (self.has_core_states || self.has_vram_states);
self.states_configurable.set_value(configurable);
@@ -216,33 +247,34 @@ impl relm4::SimpleComponent for PowerStatesFrame {
configurable && self.states_configuration_enabled.value(),
));
}
PowerStatesFrameMsg::PerformanceLevel(level) => {
PowerStatesDialogMsg::PerformanceLevel(level) => {
self.performance_level = level;
}
PowerStatesFrameMsg::ConfiguredToggled { configured } => {
PowerStatesDialogMsg::ConfiguredToggled { configured } => {
if !configured || self.performance_level == Some(PerformanceLevel::Manual) {
self.states_configuration_enabled.set_value(configured);
} else {
APP_BROKER.send(AppMsg::EnablePstateConfig);
}
}
PowerStatesFrameMsg::EnableWithManualPerformanceLevel => {
PowerStatesDialogMsg::EnableWithManualPerformanceLevel => {
sender
.output(OcPageMsg::SetPerformanceLevel(PerformanceLevel::Manual))
.unwrap();
self.states_configuration_enabled.set_value(true);
}
PowerStatesFrameMsg::InternalConfigurableChanged(configurable) => {
PowerStatesDialogMsg::InternalConfigurableChanged(configurable) => {
self.core_states_list
.emit(PowerStatesListMsg::Configurable(configurable));
self.vram_states_list
.emit(PowerStatesListMsg::Configurable(configurable));
}
}
self.update_view(widgets, sender);
}
}
impl PowerStatesFrame {
impl PowerStatesDialog {
pub fn get_enabled_power_states(&self) -> IndexMap<PowerLevelKind, Vec<u8>> {
if self.states_configuration_enabled.value() {
let state_types = [
@@ -279,7 +311,7 @@ mod tests {
adw::init().unwrap();
let context = gtk::glib::MainContext::default();
let _guard = context.acquire().unwrap();
let frame = PowerStatesFrame::detach(());
let dialog = PowerStatesDialog::detach(());
let states = PowerStates {
core: vec![],
vram: (0..4)
@@ -293,30 +325,30 @@ mod tests {
};
// Queue the startup messages together, before child components can update
frame.emit(PowerStatesFrameMsg::PowerStates {
dialog.emit(PowerStatesDialogMsg::PowerStates {
pstates: states,
configured: true,
});
frame.emit(PowerStatesFrameMsg::Configurable(true));
dialog.emit(PowerStatesDialogMsg::Configurable(true));
while context.pending() {
context.iteration(false);
}
assert!(frame.model().states_configuration_enabled.value());
assert!(dialog.model().states_configuration_enabled.value());
assert_eq!(
frame.model().get_enabled_power_states()[&PowerLevelKind::MemoryClock],
dialog.model().get_enabled_power_states()[&PowerLevelKind::MemoryClock],
vec![3],
);
// Changing to a GPU without power states must clear the previous capability
frame.emit(PowerStatesFrameMsg::PowerStates {
dialog.emit(PowerStatesDialogMsg::PowerStates {
pstates: PowerStates::default(),
configured: false,
});
frame.emit(PowerStatesFrameMsg::Configurable(true));
dialog.emit(PowerStatesDialogMsg::Configurable(true));
while context.pending() {
context.iteration(false);
}
assert!(!frame.model().states_configurable.value());
assert!(frame.model().get_enabled_power_states().is_empty());
assert!(!dialog.model().states_configurable.value());
assert!(dialog.model().get_enabled_power_states().is_empty());
}
}
@@ -1,21 +1,18 @@
use super::power_states_row::{PowerStateRow, PowerStateRowMsg, PowerStateRowOptions};
use amdgpu_sysfs::gpu_handle::PowerLevelId;
use gtk::prelude::{FrameExt, WidgetExt};
use gtk::prelude::WidgetExt;
use lact_schema::PowerState;
use relm4::{
ComponentParts, ComponentSender, RelmWidgetExt, binding::BoolBinding, css,
prelude::FactoryVecDeque,
};
use relm4::{ComponentParts, ComponentSender, binding::BoolBinding, prelude::FactoryVecDeque};
pub struct PowerStatesList {
states: FactoryVecDeque<PowerStateRow>,
value_suffix: String,
is_active_indicator_visible: BoolBinding,
configurable: BoolBinding,
active_state: Option<PowerLevelId>,
}
pub struct PowerStatesListOptions {
pub title: String,
pub value_suffix: String,
}
@@ -24,6 +21,7 @@ pub enum PowerStatesListMsg {
PowerStates(Vec<PowerState>, f64),
ActiveState(Option<PowerLevelId>),
Configurable(bool),
ValueRatio(f64),
}
#[relm4::component(pub)]
@@ -33,16 +31,11 @@ impl relm4::SimpleComponent for PowerStatesList {
type Output = ();
view! {
gtk::Frame {
gtk::Box {
set_hexpand: true,
#[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_hexpand: true,
set_selection_mode: gtk::SelectionMode::None,
},
}
@@ -60,6 +53,7 @@ impl relm4::SimpleComponent for PowerStatesList {
value_suffix: opts.value_suffix,
is_active_indicator_visible: BoolBinding::new(false),
configurable: BoolBinding::new(true),
active_state: None,
};
let states_widget = model.states.widget();
@@ -75,12 +69,12 @@ impl relm4::SimpleComponent for PowerStatesList {
let mut states = self.states.guard();
states.clear();
for mut power_state in new_pstates {
power_state.value = (power_state.value as f64 * value_ratio) as u64;
for power_state in new_pstates {
let opts = PowerStateRowOptions {
power_state,
value_suffix: self.value_suffix.clone(),
active: false,
value_ratio,
active: self.active_state.is_some() && power_state.id == self.active_state,
show_active_indicator: self.is_active_indicator_visible.clone(),
configurable: self.configurable.clone(),
};
@@ -88,10 +82,11 @@ impl relm4::SimpleComponent for PowerStatesList {
}
}
PowerStatesListMsg::ActiveState(active_idx) => {
self.active_state = 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.id == active_idx;
let is_active = active_idx.is_some() && row.power_state.id == active_idx;
self.states.send(i, PowerStateRowMsg::Active(is_active));
}
@@ -99,6 +94,9 @@ impl relm4::SimpleComponent for PowerStatesList {
PowerStatesListMsg::Configurable(configurable) => {
self.configurable.set_value(configurable);
}
PowerStatesListMsg::ValueRatio(ratio) => {
self.states.broadcast(PowerStateRowMsg::ValueRatio(ratio));
}
}
}
}
@@ -1,6 +1,6 @@
use crate::{APP_BROKER, app::msg::AppMsg};
use amdgpu_sysfs::gpu_handle::PowerLevelId;
use gtk::prelude::{BoxExt, OrientableExt, WidgetExt};
use gtk::prelude::{BoxExt, ListBoxRowExt, OrientableExt, WidgetExt};
use lact_schema::PowerState;
use relm4::{RelmObjectExt, RelmWidgetExt, binding::BoolBinding, css};
@@ -9,6 +9,7 @@ pub struct PowerStateRow {
pub(super) enabled: BoolBinding,
pub(super) power_state: PowerState,
value_suffix: String,
value_ratio: f64,
configurable: BoolBinding,
show_active_indicator: BoolBinding,
}
@@ -16,6 +17,7 @@ pub struct PowerStateRow {
pub struct PowerStateRowOptions {
pub power_state: PowerState,
pub value_suffix: String,
pub value_ratio: f64,
pub active: bool,
pub show_active_indicator: BoolBinding,
pub configurable: BoolBinding,
@@ -24,6 +26,7 @@ pub struct PowerStateRowOptions {
#[derive(Clone, Debug)]
pub enum PowerStateRowMsg {
Active(bool),
ValueRatio(f64),
}
#[relm4::factory(pub)]
@@ -35,40 +38,60 @@ impl relm4::factory::FactoryComponent for PowerStateRow {
type Init = PowerStateRowOptions;
view! {
gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
set_margin_vertical: 2,
set_margin_horizontal: 5,
gtk::ListBoxRow {
set_selectable: false,
set_activatable: false,
set_focusable: false,
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 },
},
#[wrap(Some)]
set_child = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
set_spacing: 5,
set_margin_vertical: 2,
set_margin_horizontal: 5,
append = &gtk::CheckButton {
add_binding: (&self.enabled, "active"),
add_binding: (&self.configurable, "visible"),
set_sensitive: matches!(self.power_state.id, Some(PowerLevelId::Index(_))),
},
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::Label {
add_css_class: css::MONOSPACE,
#[watch]
set_class_active: (css::DIM_LABEL, !self.active.value()),
set_label: &{
let index_text = match self.power_state.id {
Some(PowerLevelId::Index(index)) => index.to_string(),
Some(PowerLevelId::Sleep) => "S".to_owned(),
append = &gtk::CheckButton {
add_binding: (&self.enabled, "active"),
add_binding: (&self.configurable, "visible"),
set_sensitive: matches!(self.power_state.id, Some(PowerLevelId::Index(_))),
},
append = &gtk::Label {
add_css_class: css::MONOSPACE,
#[watch]
set_class_active: (css::DIM_LABEL, !self.active.value()),
set_label: &match self.power_state.id {
Some(PowerLevelId::Index(index)) => format!("{index}:"),
Some(PowerLevelId::Sleep) => "S:".to_owned(),
None => String::new(),
};
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!("{index_text}: {value_text} {}", self.value_suffix)
},
},
append = &gtk::Label {
add_css_class: css::MONOSPACE,
set_hexpand: true,
set_xalign: 1.0,
#[watch]
set_class_active: (css::DIM_LABEL, !self.active.value()),
#[watch]
set_label: &{
let value = (self.power_state.value as f64 * self.value_ratio) as u64;
let value_text = match self.power_state.min_value {
Some(min) if min != self.power_state.value => {
let min = (min as f64 * self.value_ratio) as u64;
format!("{min}-{value}")
}
_ => value.to_string(),
};
format!("{value_text} {}", self.value_suffix)
},
},
},
}
@@ -87,13 +110,16 @@ impl relm4::factory::FactoryComponent for PowerStateRow {
active: BoolBinding::new(opts.active),
power_state: opts.power_state,
value_suffix: opts.value_suffix,
value_ratio: opts.value_ratio,
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);
match msg {
PowerStateRowMsg::Active(active) => self.active.set_value(active),
PowerStateRowMsg::ValueRatio(ratio) => self.value_ratio = ratio,
}
}
}