diff --git a/lact-gui/i18n/en/lact_gui.ftl b/lact-gui/i18n/en/lact_gui.ftl index f0ce7e08..8438453d 100644 --- a/lact-gui/i18n/en/lact_gui.ftl +++ b/lact-gui/i18n/en/lact_gui.ftl @@ -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 diff --git a/lact-gui/src/app/components/adjustment_card.css b/lact-gui/src/app/components/adjustment_card.css index 1bae05c5..f3468bf2 100644 --- a/lact-gui/src/app/components/adjustment_card.css +++ b/lact-gui/src/app/components/adjustment_card.css @@ -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; } diff --git a/lact-gui/src/app/components/adjustment_row.rs b/lact-gui/src/app/components/adjustment_row.rs index 453b93ca..33807130 100644 --- a/lact-gui/src/app/components/adjustment_row.rs +++ b/lact-gui/src/app/components/adjustment_row.rs @@ -75,11 +75,10 @@ impl FactoryComponent for AdjustmentRow { 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, diff --git a/lact-gui/src/app/pages/oc_page.css b/lact-gui/src/app/pages/oc_page.css index 9050ea4c..5d6fa0fb 100644 --- a/lact-gui/src/app/pages/oc_page.css +++ b/lact-gui/src/app/pages/oc_page.css @@ -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; +} diff --git a/lact-gui/src/app/pages/oc_page.rs b/lact-gui/src/app/pages/oc_page.rs index 6e6f750b..97c2871c 100644 --- a/lact-gui/src/app/pages/oc_page.rs +++ b/lact-gui/src/app/pages/oc_page.rs @@ -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>, power_frame: relm4::Controller, - power_states_frame: relm4::Controller, + power_states_dialog: relm4::Controller, gpu_clocks_frame: relm4::Controller, vram_clocks_frame: relm4::Controller, @@ -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, - _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> { 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() } diff --git a/lact-gui/src/app/pages/oc_page/clocks_frame/clocks_frame.css b/lact-gui/src/app/pages/oc_page/clocks_frame/clocks_frame.css new file mode 100644 index 00000000..fef49afb --- /dev/null +++ b/lact-gui/src/app/pages/oc_page/clocks_frame/clocks_frame.css @@ -0,0 +1,4 @@ +.adjustment-list > row.pstates { + padding-top: 15px; + padding-bottom: 15px; +} diff --git a/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs b/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs index b3c9cbd9..bf4cc8af 100644 --- a/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs +++ b/lact-gui/src/app/pages/oc_page/clocks_frame/mod.rs @@ -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, + current_clock: Option, } #[derive(Default)] @@ -99,6 +102,8 @@ pub enum ClocksFrameMsg { vf_curve_is_configured: bool, }, VramRatio(f64), + PowerStatesAvailable(bool), + Stats(Arc), 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 = >k::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 = >k::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 = >k::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 } diff --git a/lact-gui/src/app/pages/oc_page/power_states/mod.rs b/lact-gui/src/app/pages/oc_page/power_states/mod.rs index 7b492d3c..b1065c71 100644 --- a/lact-gui/src/app/pages/oc_page/power_states/mod.rs +++ b/lact-gui/src/app/pages/oc_page/power_states/mod.rs @@ -1,3 +1,3 @@ -pub mod power_states_frame; +pub mod power_states_dialog; mod power_states_list; mod power_states_row; diff --git a/lact-gui/src/app/pages/oc_page/power_states/power_states_frame.rs b/lact-gui/src/app/pages/oc_page/power_states/power_states_dialog.rs similarity index 60% rename from lact-gui/src/app/pages/oc_page/power_states/power_states_frame.rs rename to lact-gui/src/app/pages/oc_page/power_states/power_states_dialog.rs index c2b5733b..e53b0496 100644 --- a/lact-gui/src/app/pages/oc_page/power_states/power_states_frame.rs +++ b/lact-gui/src/app/pages/oc_page/power_states/power_states_dialog.rs @@ -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, vram_states_list: relm4::Controller, 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 = >k::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 = >k::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, ) -> ComponentParts { 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) { + fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + msg: Self::Input, + sender: ComponentSender, + 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> { 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()); } } diff --git a/lact-gui/src/app/pages/oc_page/power_states/power_states_list.rs b/lact-gui/src/app/pages/oc_page/power_states/power_states_list.rs index 3088f529..dea7ace3 100644 --- a/lact-gui/src/app/pages/oc_page/power_states/power_states_list.rs +++ b/lact-gui/src/app/pages/oc_page/power_states/power_states_list.rs @@ -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, value_suffix: String, is_active_indicator_visible: BoolBinding, configurable: BoolBinding, + active_state: Option, } pub struct PowerStatesListOptions { - pub title: String, pub value_suffix: String, } @@ -24,6 +21,7 @@ pub enum PowerStatesListMsg { PowerStates(Vec, f64), ActiveState(Option), 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 = >k::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)); + } } } } diff --git a/lact-gui/src/app/pages/oc_page/power_states/power_states_row.rs b/lact-gui/src/app/pages/oc_page/power_states/power_states_row.rs index 12368f1c..a969b62f 100644 --- a/lact-gui/src/app/pages/oc_page/power_states/power_states_row.rs +++ b/lact-gui/src/app/pages/oc_page/power_states/power_states_row.rs @@ -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 = >k::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 = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + set_margin_vertical: 2, + set_margin_horizontal: 5, - append = >k::CheckButton { - add_binding: (&self.enabled, "active"), - add_binding: (&self.configurable, "visible"), - set_sensitive: matches!(self.power_state.id, Some(PowerLevelId::Index(_))), - }, + append: image = >k::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 = >k::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 = >k::CheckButton { + add_binding: (&self.enabled, "active"), + add_binding: (&self.configurable, "visible"), + set_sensitive: matches!(self.power_state.id, Some(PowerLevelId::Index(_))), + }, + + append = >k::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 = >k::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) { - 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, + } } }