diff --git a/Cargo.lock b/Cargo.lock index 8a81a4547..8091504ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,24 +600,6 @@ dependencies = [ "unty", ] -[[package]] -name = "bindgen" -version = "0.71.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" -dependencies = [ - "bitflags 2.13.1", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.3", - "shlex 1.3.0", - "syn 2.0.119", -] - [[package]] name = "bindgen" version = "0.72.1" @@ -1559,26 +1541,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "femtovg" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43d05da42e81724c16d34a150fb7fda53d3f786e5a94673ee526ff8602f0f6a" -dependencies = [ - "bitflags 2.13.1", - "bytemuck", - "fnv", - "glow", - "imgref", - "itertools 0.14.0", - "log", - "rgb", - "slotmap", - "swash", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "field-offset" version = "0.3.6" @@ -2279,8 +2241,9 @@ dependencies = [ "glutin", "i-slint-common", "i-slint-core", - "i-slint-renderer-femtovg", + "i-slint-renderer-skia", "input", + "memmap2", "nix", "raw-window-handle", "xkbcommon", @@ -2295,11 +2258,25 @@ dependencies = [ "cfg-if", "cfg_aliases", "i-slint-backend-linuxkms", + "i-slint-backend-testing", "i-slint-backend-winit", "i-slint-common", "i-slint-core", "i-slint-core-macros", - "i-slint-renderer-femtovg", + "i-slint-renderer-skia", +] + +[[package]] +name = "i-slint-backend-testing" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e901e3d47ab829c0ef500c63155776208707cd93259e6a7803ed627fa2786" +dependencies = [ + "cfg_aliases", + "i-slint-common", + "i-slint-core", + "i-slint-renderer-skia", + "vtable", ] [[package]] @@ -2319,7 +2296,6 @@ dependencies = [ "i-slint-common", "i-slint-core", "i-slint-core-macros", - "i-slint-renderer-femtovg", "i-slint-renderer-skia", "lyon_path", "muda", @@ -2399,6 +2375,7 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e407dc2bd5385dcbdd9c1cf927213d2e4bacda667545abf8839c60f8290f754" dependencies = [ + "async-channel", "auto_enums", "bitflags 2.13.1", "cfg-if", @@ -2412,6 +2389,7 @@ dependencies = [ "i-slint-core-macros", "icu_normalizer", "image", + "ksni", "lyon_algorithms", "lyon_extra", "lyon_geom", @@ -2456,28 +2434,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "i-slint-renderer-femtovg" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78d0d99a18d66738b684758fe8460743a0aa0f64a96e05940942231a340085b1" -dependencies = [ - "cfg-if", - "const-field-offset", - "derive_more", - "femtovg", - "glow", - "i-slint-common", - "i-slint-core", - "i-slint-core-macros", - "imgref", - "lyon_path", - "pin-weak", - "rgb", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "i-slint-renderer-skia" version = "1.17.1" @@ -3178,9 +3134,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "logind-zbus" @@ -4727,7 +4683,6 @@ dependencies = [ "env_logger", "futures-util", "image", - "ksni", "log", "notify-rust", "rand 0.10.2", @@ -4828,16 +4783,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "rog_simulators" -version = "6.4.0" -dependencies = [ - "log", - "rog_anime", - "sdl2", - "uhid-virt", -] - [[package]] name = "rog_slash" version = "6.4.0" @@ -5017,29 +4962,6 @@ dependencies = [ "tiny-skia 0.11.4", ] -[[package]] -name = "sdl2" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" -dependencies = [ - "bitflags 1.3.2", - "lazy_static", - "libc", - "sdl2-sys", -] - -[[package]] -name = "sdl2-sys" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" -dependencies = [ - "cfg-if", - "libc", - "version-compare", -] - [[package]] name = "semver" version = "1.0.28" @@ -5193,7 +5115,7 @@ version = "0.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2d1c3ebd697c0cbded0145e9204a38fa6b268446051b7196d0a096414ea7f3" dependencies = [ - "bindgen 0.72.1", + "bindgen", "cc", "flate2", "heck", @@ -5252,7 +5174,7 @@ dependencies = [ "i-slint-common", "i-slint-core", "i-slint-core-macros", - "i-slint-renderer-femtovg", + "i-slint-renderer-skia", "i-slint-renderer-software", "num-traits", "once_cell", @@ -5423,6 +5345,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "as-raw-xcb-connection", "bytemuck", + "drm", "fastrand", "js-sys", "memmap2", @@ -6105,26 +6028,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "uhid-virt" -version = "0.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad50862a51d693d984b07f6f6263a571554d415f5d1e16f4cbad2c266448e1b" -dependencies = [ - "enumflags2", - "libc", - "uhidrs-sys", -] - -[[package]] -name = "uhidrs-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec7103e0167479f230f6baae49dbf714074d91b2ff141983524d7417c0b98a8" -dependencies = [ - "bindgen 0.71.1", -] - [[package]] name = "unicase" version = "2.9.0" @@ -6258,9 +6161,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.1" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -6291,12 +6194,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version-compare" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" - [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index 52ffca737..ece868983 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ members = [ "rog-profiles", "rog-control-center", "rog-slash", - "simulators", "rog-scsi", ] @@ -47,7 +46,6 @@ gif = "^0.14.2" glam = { version = "^0.33.5", features = ["serde"] } image = "=0.25.10" inotify = "^0.11.5" -ksni = { version = "^0.3.6", default-features = false, features = ["async-io"] } libc = "^0.2.189" log = "^0.4.33" logind-zbus = { version = "^5.3.2" } @@ -68,7 +66,8 @@ slint = { version = "^1.17.1", default-features = false, features = [ "compat-1-2", "gettext", "backend-winit-wayland", - "renderer-femtovg", + "renderer-skia", + "system-tray", ] } slint-build = "=1.17.1" thiserror = "^2.0.20" @@ -80,7 +79,6 @@ tokio = { version = "^1.53.1", default-features = false, features = [ "rt-multi-thread", ] } udev = { version = "^0.9.3", features = ["mio"] } -uhid-virt = "^0.0.8" zbus = "^5.19.0" [profile.release] diff --git a/flake.nix b/flake.nix index eb0139191..c7ce27cd4 100644 --- a/flake.nix +++ b/flake.nix @@ -19,8 +19,8 @@ system: let tc = (fenixpkgs system).toolchainOf { - channel = "1.85.0"; - sha256 = "sha256-AJ6LX/Q/Er9kS15bn9iflkUwcgYqRQxiOIL2ToVAXaU="; + channel = "1.93.0"; + sha256 = "sha256-vra6TkHITpwRyA5oBKAHSX0Mi6CBDNQD+ryPSpxFsfg="; }; in (fenixpkgs system).combine [ @@ -43,7 +43,9 @@ (pkgs system).clang (pkgs system).SDL2 (pkgs system).cargo-cranky - (pkgs system).eslint + (pkgs system).fontconfig + (pkgs system).freetype + (pkgs system).slint-lsp ]; LD_LIBRARY_PATH = (pkgs system).lib.makeLibraryPath [ (pkgs system).wayland @@ -56,6 +58,10 @@ (pkgs system).egl-wayland (pkgs system).egl-x11 (pkgs system).libglvnd + (pkgs system).llvmPackages.libclang.lib + (pkgs system).stdenv.cc.cc.lib + (pkgs system).fontconfig + (pkgs system).freetype ]; LIBCLANG_PATH = "${(pkgs system).llvmPackages.libclang.lib}/lib"; RUST_SRC_PATH = "${toolchainFor system}/lib/rustlib/src/rust/library"; diff --git a/rog-control-center/Cargo.toml b/rog-control-center/Cargo.toml index 7854f3c7c..a4ca532b3 100644 --- a/rog-control-center/Cargo.toml +++ b/rog-control-center/Cargo.toml @@ -18,7 +18,6 @@ rog_ally = [] [dependencies] console-subscriber = { workspace = true, optional = true } -ksni.workspace = true image.workspace = true ashpd.workspace = true diff --git a/rog-control-center/src/ui/helpers/hardware.rs b/rog-control-center/src/helpers/hardware.rs similarity index 81% rename from rog-control-center/src/ui/helpers/hardware.rs rename to rog-control-center/src/helpers/hardware.rs index 410a18ebd..e5493a5f3 100644 --- a/rog-control-center/src/ui/helpers/hardware.rs +++ b/rog-control-center/src/helpers/hardware.rs @@ -1,6 +1,6 @@ use std::{fs, io, path::Path}; -use crate::ui::helpers::types::BatteryInfo; +use crate::helpers::types::{BatteryInfo, CpuTelemetry}; use anyhow::Result; pub fn get_dmi_product_name() -> io::Result { @@ -49,3 +49,15 @@ pub fn calculate_cpu_usage( } 0.0 } + +pub fn get_current_ram() -> f32 { + rog_platform::cpu::get_ram_usage_pct() +} + +pub fn get_cpu_telemetry() -> CpuTelemetry { + CpuTelemetry { + temp: rog_platform::cpu::get_cpu_temp(), + freq_mhz: rog_platform::cpu::get_cpu_frequency_mhz(), + usage_pct: 99.99, + } +} diff --git a/rog-control-center/src/helpers/mod.rs b/rog-control-center/src/helpers/mod.rs new file mode 100644 index 000000000..e1b8e1b92 --- /dev/null +++ b/rog-control-center/src/helpers/mod.rs @@ -0,0 +1,4 @@ +pub mod hardware; +pub mod startup; +pub mod types; +pub mod zbus_proxies; diff --git a/rog-control-center/src/helpers/startup.rs b/rog-control-center/src/helpers/startup.rs new file mode 100644 index 000000000..310a24f5d --- /dev/null +++ b/rog-control-center/src/helpers/startup.rs @@ -0,0 +1,62 @@ +//! Used to get the current values at startup + +use std::{ + rc::Rc, + sync::{Arc, OnceLock}, +}; + +use log::warn; + +use rog_platform::platform::PlatformProfile; +use slint::{ComponentHandle, ModelRc, VecModel}; + +use crate::{AsusArmouryData, MainWindow, PowerData, helpers::zbus_proxies::AsusdInterface}; + +/// Get asusd attribute and update the UI to reflect them +pub async fn populate_slint_properties( + ui_weak: slint::Weak, + asusd: Arc>, +) { + if let Some(asusd_proxy) = asusd.get() { + // Fetch armoury attributes if not empty + if asusd_proxy.is_armoury_loaded() { + // Set asus_armoury_loaded to true + { + let _ = ui_weak.upgrade_in_event_loop(move |ui| { + let armoury = ui.global::(); + armoury.set_asus_armoury_loaded(true); + }); + } + } else { + warn!("asus-armoury module may not be loader"); + let _ = ui_weak.upgrade_in_event_loop(move |ui| { + let armoury = ui.global::(); + armoury.set_asus_armoury_loaded(false); + }); + } + + // Platform profile, populate index, the current selected mode is handled by the platform subscription + if let Some(platform_proxy) = &asusd_proxy.platform { + let available_platform_profiles = platform_proxy + .platform_profile_choices() + .await + .unwrap_or_default(); + let mut indexes: Vec = Vec::new(); + for profile in [ + PlatformProfile::LowPower, + PlatformProfile::Quiet, + PlatformProfile::Balanced, + PlatformProfile::Performance, + ] { + if available_platform_profiles.contains(&profile) { + indexes.push(profile as i32); + } + } + let _ = ui_weak.upgrade_in_event_loop(move |ui| { + let power_data = ui.global::(); + power_data + .set_platform_profile_indexes(ModelRc::from(Rc::new(VecModel::from(indexes)))); + }); + } + } +} diff --git a/rog-control-center/src/ui/helpers/types.rs b/rog-control-center/src/helpers/types.rs similarity index 69% rename from rog-control-center/src/ui/helpers/types.rs rename to rog-control-center/src/helpers/types.rs index 6035bcda7..af3d2d2c3 100644 --- a/rog-control-center/src/ui/helpers/types.rs +++ b/rog-control-center/src/helpers/types.rs @@ -1,3 +1,14 @@ +//! Types used to represent data + +use crate::AttrMinMax; + +pub const MINMAX: AttrMinMax = AttrMinMax { + min: 0, + max: 0, + current: -1.0, + supported: false, +}; + #[derive(Debug, Clone, PartialEq, Default)] pub struct BatteryInfo { pub health: u8, @@ -29,12 +40,13 @@ pub struct SystemTelemetry { pub igpu_temp: f32, pub igpu_usage: f32, pub ram_usage_pct: f32, - pub fan_rpms: (i32, i32, i32), // (CPU, GPU, Mid) + pub fan_rpms: FanTelemetry, } #[derive(Debug, Clone, PartialEq, Default)] -pub struct AttrMinMaxData { - pub min: i32, - pub max: i32, - pub current: f32, +pub struct FanTelemetry { + pub cpu: i32, + pub gpu: i32, + // Some laptops does not have the mid fan + pub mid: Option, } diff --git a/rog-control-center/src/helpers/zbus_proxies.rs b/rog-control-center/src/helpers/zbus_proxies.rs new file mode 100644 index 000000000..356920af6 --- /dev/null +++ b/rog-control-center/src/helpers/zbus_proxies.rs @@ -0,0 +1,284 @@ +use std::collections::HashMap; + +use anyhow::Result; +use log::info; +use zbus::blocking::proxy::ProxyImpl; +use zbus::blocking::{Connection, fdo}; + +use rog_dbus::asus_armoury::AsusArmouryProxy; +use rog_dbus::scsi_aura::ScsiAuraProxy; +use rog_dbus::zbus_anime::AnimeProxy; +use rog_dbus::zbus_aura::AuraProxy; +use rog_dbus::zbus_backlight::BacklightProxy; +use rog_dbus::zbus_fan_curves::FanCurvesProxy; +use rog_dbus::zbus_platform::PlatformProxy; +use rog_dbus::zbus_slash::SlashProxy; +use rog_dbus::zbus_xgm_led::XgmLedProxy; +use rog_platform::asus_armoury::FirmwareAttribute; +use zbus::names::OwnedInterfaceName; +use zbus::zvariant::{OwnedObjectPath, OwnedValue}; + +use crate::AttrMinMax; + +pub const ZBUS_PATH: &str = "/xyz/ljones/rogcc"; +pub const ZBUS_IFACE: &str = "xyz.ljones.rogcc"; + +pub fn find_iface(iface_name: &str) -> Result, Box> +where + T: ProxyImpl<'static> + From>, +{ + let conn = Connection::system()?; + let f = fdo::ObjectManagerProxy::new(&conn, "xyz.ljones.Asusd", "/")?; + let interfaces = f.get_managed_objects()?; + let mut paths = Vec::new(); + for v in interfaces.iter() { + // let o: Vec = v.1.keys().map(|e| + // e.to_owned()).collect(); println!("{}, {:?}", v.0, o); + for k in v.1.keys() { + if k.as_str() == iface_name { + // println!("Found {iface_name} device at {}, {}", v.0, k); + paths.push(v.0.clone()); + } + } + } + if paths.len() > 1 { + info!("Multiple asusd interfaces devices found"); + } + if !paths.is_empty() { + let mut ctrl = Vec::new(); + paths.sort_by(|a, b| a.cmp(b)); + for path in paths { + ctrl.push( + T::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build()?, + ); + } + return Ok(ctrl); + } + + Err("No Aura interface".into()) +} + +pub async fn find_iface_async(iface_name: &str) -> Result, Box> +where + T: zbus::proxy::ProxyImpl<'static> + From>, +{ + let conn = zbus::Connection::system().await?; + let f = zbus::fdo::ObjectManagerProxy::new(&conn, "xyz.ljones.Asusd", "/").await?; + let interfaces = f.get_managed_objects().await?; + let mut paths = Vec::new(); + for v in interfaces.iter() { + // let o: Vec = v.1.keys().map(|e| + // e.to_owned()).collect(); println!("{}, {:?}", v.0, o); + for k in v.1.keys() { + if k.as_str() == iface_name { + // println!("Found {iface_name} device at {}, {}", v.0, k); + paths.push(v.0.clone()); + } + } + } + if paths.len() > 1 { + info!("Multiple asusd interfaces devices found"); + } + if !paths.is_empty() { + let mut ctrl = Vec::new(); + paths.sort_by(|a, b| a.cmp(b)); + for path in paths { + ctrl.push( + T::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + return Ok(ctrl); + } + + Err("No interface".into()) +} + +pub struct AsusdInterface { + pub conn: zbus::Connection, + // One armory attribute object per firmware attribute + pub armoury: HashMap>, + pub scsi_aura: Option>, + pub anime: Option>, + pub aura: Option>, + pub backlight: Option>, + pub fan_curves: Option>, + pub platform: Option>, + pub slash: Option>, + pub xgm_led: Option>, +} +impl AsusdInterface { + pub async fn build() -> Result { + let conn = zbus::Connection::system().await?; + + let proxy = zbus::fdo::ObjectManagerProxy::new(&conn, "xyz.ljones.Asusd", "/").await?; + + let objects: HashMap< + OwnedObjectPath, + HashMap>, + > = proxy.get_managed_objects().await?; + + let mut armoury: HashMap> = HashMap::new(); + let mut scsi_aura: Option> = None; + let mut anime: Option> = None; + let mut aura: Option> = None; + let mut backlight: Option> = None; + let mut fan_curves: Option> = None; + let mut platform: Option> = None; + let mut slash: Option> = None; + let mut xgm_led: Option> = None; + + for (path, interfaces) in objects { + for iface in interfaces.keys() { + match iface.as_str() { + "xyz.ljones.Platform" => { + platform = Some( + PlatformProxy::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + "xyz.ljones.FanCurves" => { + fan_curves = Some( + FanCurvesProxy::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + "xyz.ljones.Backlight" => { + backlight = Some( + BacklightProxy::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + "xyz.ljones.Slash" => { + slash = Some( + SlashProxy::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + "xyz.ljones.XgmLed" => { + xgm_led = Some( + XgmLedProxy::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + "xyz.ljones.ScsiAura" => { + scsi_aura = Some( + ScsiAuraProxy::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + "xyz.ljones.Aura" => { + aura = Some( + AuraProxy::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + "xyz.ljones.Anime" => { + anime = Some( + AnimeProxy::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + "xyz.ljones.AsusArmoury" => { + // One attribute object per firmware attribute + let leaf = path.as_str().rsplit('/').next().unwrap_or(""); + let attr = FirmwareAttribute::from(leaf); + if attr != FirmwareAttribute::None { + armoury.insert( + attr, + AsusArmouryProxy::builder(&conn) + .path(path.clone())? + .destination("xyz.ljones.Asusd")? + .build() + .await?, + ); + } + } + _ => {} + } + } + } + + Ok(Self { + conn, + armoury, + scsi_aura, + anime, + aura, + backlight, + fan_curves, + platform, + slash, + xgm_led, + }) + } + + /// True when at least one asusd interface was discovered + pub fn present(&self) -> bool { + self.platform.is_some() + || self.fan_curves.is_some() + || self.backlight.is_some() + || self.slash.is_some() + || self.xgm_led.is_some() + || self.scsi_aura.is_some() + || self.aura.is_some() + || self.anime.is_some() + || !self.armoury.is_empty() + } + + /// Proxy for a specific armory firmware attribute, if the board exposes it + pub fn attribute(&self, attr: FirmwareAttribute) -> Option<&AsusArmouryProxy<'static>> { + self.armoury.get(&attr) + } + + pub fn is_armoury_loaded(&self) -> bool { + !self.armoury.is_empty() + } +} + +pub async fn get_min_max_current(proxy: &AsusArmouryProxy<'_>) -> Option { + if let (Ok(min), Ok(max), Ok(current)) = ( + proxy.min_value().await, + proxy.max_value().await, + proxy.current_value().await, + ) { + Some(AttrMinMax { + min, + max, + current: current as f32, + supported: true, + }) + } else { + None + } +} diff --git a/rog-control-center/src/lib.rs b/rog-control-center/src/lib.rs index 8f5ce1dba..6e3cbb3e7 100644 --- a/rog-control-center/src/lib.rs +++ b/rog-control-center/src/lib.rs @@ -9,10 +9,10 @@ pub use slint; pub mod cli_options; pub mod config; pub mod error; +pub mod helpers; pub mod state; pub mod tray; pub mod ui; -pub mod zbus_proxies; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const APP_ID: &str = "org.opengamingcollective.rog-control-center"; diff --git a/rog-control-center/src/main.rs b/rog-control-center/src/main.rs index 28a8e9ae3..0261c66f8 100644 --- a/rog-control-center/src/main.rs +++ b/rog-control-center/src/main.rs @@ -1,5 +1,5 @@ use std::env; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; use config_traits::{StdConfig, StdConfigLoad1}; use dmi_id::DMIID; @@ -8,19 +8,27 @@ use log::{error, info, warn}; use rog_control_center::cli_options::CliStart; use rog_control_center::config::Config; -use rog_control_center::error::Result; use rog_control_center::print_versions; +use rog_control_center::tray::setup_tray; +use rog_control_center::helpers::startup::populate_slint_properties; +use rog_control_center::helpers::zbus_proxies::AsusdInterface; use rog_control_center::state::Event; use rog_control_center::ui::actions::ActionHandler; +use rog_control_center::ui::subscriptions::{ + subscribe_armoury, subscribe_ppd, subscribe_telemetry, +}; +use rog_control_center::ui::window::setup_window; use slint::ComponentHandle; use tokio::runtime::Runtime; use tokio::sync::mpsc; +use anyhow::Result; + fn main() -> Result<()> { // Setup Logging env_logger::Builder::from_env( - Env::default().default_filter_or("warn,tracing=error,zbus=error"), + Env::default().default_filter_or("info,tracing=error,zbus=error"), ) .format_timestamp(None) .init(); @@ -36,10 +44,10 @@ fn main() -> Result<()> { if let Ok(gamescope) = env::var("GAMESCOPE_WAYLAND_DISPLAY") { if !gamescope.is_empty() { unsafe { env::set_var("WAYLAND_DISPLAY", gamescope) }; - } else if let Ok(wayland) = env::var("WAYLAND_DISPLAY") { - if wayland.is_empty() { - unsafe { env::set_var("WAYLAND_DISPLAY", "gamescope-0") }; - } + } else if let Ok(wayland) = env::var("WAYLAND_DISPLAY") + && wayland.is_empty() + { + unsafe { env::set_var("WAYLAND_DISPLAY", "gamescope-0") }; } } @@ -96,36 +104,70 @@ fn main() -> Result<()> { } // Create the Slint UI Window - let ui = rog_control_center::ui::window::setup_window(config.clone()); + let ui = setup_window(); // Create the Central Event Channel let (event_tx, mut event_rx) = mpsc::unbounded_channel::(); + // Create System Tray + let _tray = setup_tray(event_tx.clone()); + // Send the dmi product name to the UI let _ = event_tx.send(Event::DmiLoaded(dmi.product_name.clone())); - // Start System Tray - let (tray_tx, tray_rx) = tokio::sync::watch::channel(config.lock().unwrap().enable_tray_icon); - rog_control_center::tray::init_tray(tray_rx, event_tx.clone()); - // Bind UI Inputs to the Channel rog_control_center::ui::callbacks::bind_ui_events(&ui, event_tx.clone()); - // Start Hardware Subscriptions - rt.spawn(rog_control_center::ui::subscriptions::subscribe_battery( - event_tx.clone(), - )); // subscribe_telemetry(event_tx.clone(), true); + // Detect asusd and send the result to the GUI + let asusd = Arc::new(OnceLock::new()); + let asusd_set = asusd.clone(); + let asusd_tx = event_tx.clone(); + // Block here, we want to wait for the asusd interface connection + rt.block_on(async move { + match AsusdInterface::build().await { + Ok(int) if int.present() => { + let _ = asusd_set.set(int); + let _ = asusd_tx.send(Event::AsusdState(true)); + } + Ok(_) => { + warn!("asusd reachable but no known interfaces found"); + let _ = asusd_tx.send(Event::AsusdState(false)); + } + Err(err) => { + warn!("asusd is not available: {err}"); + let _ = asusd_tx.send(Event::AsusdState(false)); + } + } + }); + + // Start Hardware Subscriptions + rt.spawn(subscribe_telemetry(event_tx.clone())); + rt.spawn(subscribe_ppd(event_tx.clone(), asusd.clone())); + rt.spawn(subscribe_armoury(event_tx.clone(), asusd.clone())); + + // Show the window after the pre-startup is done + let background_startup = match config.try_lock() { + Ok(c) => c.startup_in_background, + Err(_) => false, + }; + + if !background_startup && let Err(e) = ui.window().show() { + warn!("Couldn't show main window: {e:?}"); + } + let mut action_handler = ActionHandler { - tray_tx: tray_tx.clone(), config: config.clone(), + asusd: asusd.clone(), + event_tx: event_tx.clone(), }; - // Start Event Loop let ui_weak = ui.as_weak(); rt.spawn(async move { let mut state = rog_control_center::state::AppState::new(); + // Get the current values from asusd + populate_slint_properties(ui_weak.clone(), asusd.clone()).await; while let Some(event) = event_rx.recv().await { let (actions, ui_updates) = state.update(event); @@ -165,7 +207,6 @@ fn main() -> Result<()> { slint::CloseRequestResponse::HideWindow } }); - if let Err(e) = slint::run_event_loop_until_quit() { error!("Slint event loop error: {e:?}"); } diff --git a/rog-control-center/src/state.rs b/rog-control-center/src/state.rs index d880db415..b3e6aa641 100644 --- a/rog-control-center/src/state.rs +++ b/rog-control-center/src/state.rs @@ -1,7 +1,11 @@ //! AppState, contains all the datas and the Events -use crate::ui::helpers::types::{BatteryInfo, SystemTelemetry}; - +use crate::{ + AttrBool, AttrMinMax, + helpers::types::{BatteryInfo, SystemTelemetry}, +}; +use log::info; +use rog_platform::asus_armoury::FirmwareAttribute; #[derive(Debug, Clone)] pub enum Event { // Hardware update events @@ -12,8 +16,48 @@ pub enum Event { // Dbus signals PlatformProfileSignalled(i32), - // User Action - UserRequestedProfile(i32), + // asusd reachability: false → grey-out + permanent toast + AsusdState(bool), + // User pressed the Retry button + RetryAsusd, + + // System User Action + UserRequestedPowerProfile(i32), + UserRequestedPanelOD(AttrBool), + UserRequestedBootSound(AttrBool), + UserRequestedScreenAutoBrightness(AttrBool), + UserRequestedMCUPowerSave(AttrBool), + + UpdatedPowerProfile(i32), + // asus-armoury update + UpdatedApuMem(AttrMinMax), + UpdatedCorePerf(AttrMinMax), + UpdatedCoreEff(AttrMinMax), + UpdatedPptPl1Spl(AttrMinMax), + UpdatedPptPl2Sppt(AttrMinMax), + UpdatedPptPl3Fppt(AttrMinMax), + UpdatedPptFppt(AttrMinMax), + UpdatedPptApuSppt(AttrMinMax), + UpdatedPptPlatformSppt(AttrMinMax), + UpdatedNvDynamicBoost(AttrMinMax), + UpdatedNvTempTarget(AttrMinMax), + UpdatedDgpuBaseTgp(AttrMinMax), + UpdatedDgpuTgp(AttrMinMax), + UpdatedChargeMode(AttrMinMax), + UpdatedBootSound(AttrBool), + UpdatedMCUPowerSave(AttrBool), + UpdatedPanelOD(AttrBool), + UpdatedPanelHdMode(AttrMinMax), + UpdatedEgpuConnected(AttrBool), + UpdatedEgpuEnable(AttrBool), + UpdatedDgpuDisable(AttrBool), + UpdatedGpuMuxMode(AttrBool), + UpdatedMiniLedMode(AttrMinMax), + UpdatedPendingRebbot(AttrBool), + // 30 + UpdatedScreenAutoBrightness(AttrBool), + + // System User Action Per Profile UserRequestedBatteryLimit(u8), // Settings @@ -24,34 +68,122 @@ pub enum Event { ShowWindow, HideWindow, Quit, + // Nothing + None, +} + +impl Event { + /// Convert an asus-armoury firmware attribute into an event + pub fn firmware_attr_into_event(attr: &FirmwareAttribute, val: AttrMinMax) -> Event { + match attr { + FirmwareAttribute::ApuMem => Event::UpdatedApuMem(val), + FirmwareAttribute::CoresPerformance => Event::UpdatedCorePerf(val), + FirmwareAttribute::CoresEfficiency => Event::UpdatedCoreEff(val), + FirmwareAttribute::PptPl1Spl => Event::UpdatedPptPl1Spl(val), + FirmwareAttribute::PptPl2Sppt => Event::UpdatedPptPl2Sppt(val), + FirmwareAttribute::PptPl3Fppt => Event::UpdatedPptPl3Fppt(val), + FirmwareAttribute::PptFppt => Event::UpdatedPptFppt(val), + FirmwareAttribute::PptApuSppt => Event::UpdatedPptApuSppt(val), + FirmwareAttribute::PptPlatformSppt => Event::UpdatedPptPlatformSppt(val), + FirmwareAttribute::NvDynamicBoost => Event::UpdatedNvDynamicBoost(val), + FirmwareAttribute::NvTempTarget => Event::UpdatedNvTempTarget(val), + FirmwareAttribute::DgpuBaseTgp => Event::UpdatedDgpuBaseTgp(val), + FirmwareAttribute::DgpuTgp => Event::UpdatedDgpuTgp(val), + FirmwareAttribute::ChargeMode => Event::UpdatedChargeMode(val), + FirmwareAttribute::BootSound => Event::UpdatedBootSound(attr_i32_into_bool(val)), + + FirmwareAttribute::McuPowersave => Event::UpdatedMCUPowerSave(attr_i32_into_bool(val)), + FirmwareAttribute::PanelOverdrive => Event::UpdatedPanelOD(attr_i32_into_bool(val)), + + FirmwareAttribute::PanelHdMode => Event::UpdatedPanelHdMode(val), + FirmwareAttribute::EgpuConnected => { + Event::UpdatedEgpuConnected(attr_i32_into_bool(val)) + } + FirmwareAttribute::EgpuEnable => Event::UpdatedEgpuEnable(attr_i32_into_bool(val)), + FirmwareAttribute::DgpuDisable => Event::UpdatedDgpuDisable(attr_i32_into_bool(val)), + FirmwareAttribute::GpuMuxMode => Event::UpdatedGpuMuxMode(attr_i32_into_bool(val)), + FirmwareAttribute::MiniLedMode => Event::UpdatedMiniLedMode(val), + FirmwareAttribute::PendingReboot => { + Event::UpdatedPendingRebbot(attr_i32_into_bool(val)) + } + FirmwareAttribute::ScreenAutoBrightness => { + Event::UpdatedScreenAutoBrightness(attr_i32_into_bool(val)) + } + // Unknown + FirmwareAttribute::None => Event::None, + } + } +} + +pub fn attr_i32_into_bool(val: AttrMinMax) -> AttrBool { + AttrBool { + current: val.current == 1.0, + supported: val.supported, + } } #[derive(Debug, Clone)] pub enum Action { + // System/Home Page SetPlatformProfile(i32), + SetPanelOD(AttrBool), + SetBootSound(AttrBool), + SetScreenAutoBrightness(AttrBool), + SetMCUPowerSave(AttrBool), + SetBatteryLimit(u8), + // Re-probe asusd + RetryAsusd, + // Settings SetTray(bool), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ToastType { + Info = 0, + Warn = 1, + Error = 2, +} + #[derive(Debug, Clone)] pub enum UiUpdate { Telemetry(SystemTelemetry), Battery(BatteryInfo), ProductName(String), PlatformProfile(i32), - ShowToast { message: String, is_error: bool }, + ShowToast { + message: String, + toast_type: ToastType, + }, + // asusd gone/back + AsusdState(bool), + // Non-closable toast + ShowPermanentToast { + message: String, + toast_type: ToastType, + }, + + // Home + BootSound(AttrBool), + PanelOD(AttrBool), + ScreenAutoBrightness(bool), + MCUPowerSave(bool), // Window Management ToggleWindow, ShowWindow, HideWindow, Quit, + + // Unknown attribute / no event + None, } #[derive(Default)] pub struct AppState { + pub asusd_running: bool, pub battery: Option, pub telemetry: SystemTelemetry, pub product_name: String, @@ -59,7 +191,11 @@ pub struct AppState { } impl AppState { pub fn new() -> Self { - Self::default() + // Assume asusd is running, changed at boot + Self { + asusd_running: true, + ..Self::default() + } } /// Takes an event, update the state if needed and return what should happen @@ -91,13 +227,53 @@ impl AppState { ui_updates.push(UiUpdate::PlatformProfile(new_profile)); } } - Event::UserRequestedProfile(requested_profile) => { - // This one does not directly change the profile + Event::AsusdState(running) => { + // Only react on a transition, error toast once when it goes down + if self.asusd_running != running { + self.asusd_running = running; + ui_updates.push(UiUpdate::AsusdState(running)); + if !running { + ui_updates.push(UiUpdate::ShowPermanentToast { + message: "asusd is not running, please retry again".to_string(), + toast_type: ToastType::Error, + }); + } + } + } + Event::RetryAsusd => { + actions.push(Action::RetryAsusd); + } + + // System User Action + Event::UserRequestedPowerProfile(requested_profile) => { actions.push(Action::SetPlatformProfile(requested_profile)); } + + Event::UserRequestedPanelOD(b) => { + actions.push(Action::SetPanelOD(b)); + } + + Event::UserRequestedBootSound(b) => { + actions.push(Action::SetBootSound(b)); + } + + Event::UserRequestedScreenAutoBrightness(b) => { + actions.push(Action::SetScreenAutoBrightness(b)); + } + + Event::UserRequestedMCUPowerSave(b) => { + actions.push(Action::SetMCUPowerSave(b)); + } + Event::UserRequestedBatteryLimit(requested_limit) => { actions.push(Action::SetBatteryLimit(requested_limit)); } + Event::UpdatedBootSound(b) => { + ui_updates.push(UiUpdate::BootSound(b)); + } + Event::UpdatedPanelOD(b) => { + ui_updates.push(UiUpdate::PanelOD(b)); + } // Config Event::UserToggledTray(b) => { actions.push(Action::SetTray(b)); @@ -109,6 +285,9 @@ impl AppState { Event::Quit => { ui_updates.push(UiUpdate::Quit); } + _ => { + info!("Event not implemented yet: {:?}", event); + } } (actions, ui_updates) diff --git a/rog-control-center/src/tray.rs b/rog-control-center/src/tray.rs index b9aaebaef..512680751 100644 --- a/rog-control-center/src/tray.rs +++ b/rog-control-center/src/tray.rs @@ -1,151 +1,22 @@ -//! A self-contained tray icon with menus. -//! -//! The tray icon color reflects the GPU power status, published by the -//! dGPU status monitor in `notify.rs` (the same source as the -//! "dGPU status changed" notifications). - -use std::path::{Path, PathBuf}; - -use crate::state::Event; -use ksni::{Icon, TrayMethods}; -use log::info; +use crate::{AsusTray, state::Event}; +use log::warn; use tokio::sync::mpsc::UnboundedSender; -use tokio::sync::watch; - -const TRAY_LABEL: &str = "ROG Control Center"; -//const TRAY_ICON_PATH: &str = "/usr/share/icons/hicolor/512x512/apps/"; -const TRAY_ICON_PATH: &str = "/home/luytan/Projects/asusctl/rog-control-center/data"; - -fn read_icon(file: &Path) -> Icon { - let mut path = PathBuf::from(TRAY_ICON_PATH); - path.push(file); - let bytes = match std::fs::read(&path) { - Ok(b) => b, - Err(e) => { - log::warn!("Could not read icon {:?}: {e}, using fallback", path); - return Icon { - width: 16, - height: 16, - data: vec![255; 16 * 16 * 4], - }; - } - }; - - let mut img = match image::load_from_memory_with_format(&bytes, image::ImageFormat::Png) { - Ok(i) => i.to_rgba8(), - Err(e) => { - log::warn!("Could not decode icon {:?}: {e}, using fallback", path); - return Icon { - width: 16, - height: 16, - data: vec![255; 16 * 16 * 4], - }; - } - }; - - for image::Rgba(pixel) in img.pixels_mut() { - // (╯°□°)╯︵ ┻━┻ - *pixel = u32::from_be_bytes(*pixel).rotate_right(8).to_be_bytes(); - } - - let (width, height) = img.dimensions(); - Icon { - width: width as i32, - height: height as i32, - data: img.into_raw(), - } -} - -struct AsusTray { - current_title: String, - current_icon: Icon, - tx: UnboundedSender, - pub status: ksni::Status, -} - -impl ksni::Tray for AsusTray { - fn id(&self) -> String { - TRAY_LABEL.into() - } - - fn icon_pixmap(&self) -> Vec { - vec![self.current_icon.clone()] - } - fn title(&self) -> String { - self.current_title.clone() - } - - fn status(&self) -> ksni::Status { - self.status - } - - fn menu(&self) -> Vec> { - use ksni::menu::*; - vec![ - StandardItem { - label: "Open ROGCC".into(), - icon_name: "rog-control-center".into(), - activate: Box::new(move |s: &mut AsusTray| { - let _ = s.tx.send(Event::ShowWindow); - }), - ..Default::default() - } - .into(), - MenuItem::Separator, - StandardItem { - label: "Quit ROGCC".into(), - icon_name: "application-exit".into(), - activate: Box::new(|s: &mut AsusTray| { - let _ = s.tx.send(Event::Quit); - }), - ..Default::default() - } - .into(), - ] - } -} +pub fn setup_tray(tx: UnboundedSender) -> AsusTray { + let tray = AsusTray::new().expect("Couldn't create tray"); -/// Start the tray and route its window actions through `WindowController`. -pub fn init_tray(mut enable_tray_rx: watch::Receiver, tx: UnboundedSender) { - tokio::spawn(async move { - let tray_init = AsusTray { - current_title: TRAY_LABEL.to_string(), - current_icon: read_icon(Path::new("rog-sidebar-logo.png")), - tx, - status: if *enable_tray_rx.borrow() { - ksni::Status::Active - } else { - ksni::Status::Passive - }, - }; - - let tray = match tray_init.disable_dbus_name(true).spawn().await { - Ok(t) => t, - Err(e) => { - log::error!( - "Tray unable to be initialised: {e:?}. Do you have a system tray enabled?" - ); - return; - } - }; - - info!("Tray started"); + let tx_clone = tx.clone(); + tray.on_show_window(move || { + let _ = tx_clone.send(Event::ShowWindow); + }); - loop { - if enable_tray_rx.changed().await.is_err() { - break; // sender dropped - } - let enabled = *enable_tray_rx.borrow_and_update(); - let _ = tray - .update(move |t: &mut AsusTray| { - t.status = if enabled { - ksni::Status::Active - } else { - ksni::Status::Passive - }; - }) - .await; - } + let tx_clone = tx.clone(); + tray.on_quit_window(move || { + let _ = tx_clone.send(Event::Quit); }); + + if let Err(err) = tray.show() { + warn!("Couldn't show tray: {:?}", err); + }; + tray } diff --git a/rog-control-center/src/ui/actions.rs b/rog-control-center/src/ui/actions.rs index b4e56d24e..e97bce486 100644 --- a/rog-control-center/src/ui/actions.rs +++ b/rog-control-center/src/ui/actions.rs @@ -1,31 +1,91 @@ -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, OnceLock}; -use config_traits::StdConfig; -use log::error; -use tokio::sync::watch::Sender; +use log::{debug, warn}; +use tokio::sync::mpsc::UnboundedSender; -use crate::{config::Config, state::Action}; +use crate::{ + config::Config, + helpers::zbus_proxies::AsusdInterface, + state::{Action, Event}, +}; +use rog_platform::asus_armoury::FirmwareAttribute; pub struct ActionHandler { - pub tray_tx: Sender, pub config: Arc>, + pub asusd: Arc>, + pub event_tx: UnboundedSender, } impl ActionHandler { pub async fn handle_action(&mut self, action: Action) { + debug!("handling action: {:?}", action); match action { - Action::SetBatteryLimit(_) => {} - Action::SetPlatformProfile(_) => {} - Action::SetTray(b) => { - match self.config.lock() { - Ok(mut c) => { - // We got the lock, update the config and update the tray - c.enable_tray_icon = b; - c.write(); - let _ = self.tray_tx.send(b); - } - Err(err) => { - error!("Couldn't get config lock: {}", err); - } + // Re-probe asusd (Retry button) + Action::RetryAsusd => match AsusdInterface::build().await { + Ok(int) if int.present() => { + let _ = self.asusd.set(int); + let _ = self.event_tx.send(Event::AsusdState(true)); } + Ok(_) => { + warn!("asusd reachable but no known interfaces found"); + let _ = self.event_tx.send(Event::AsusdState(false)); + } + Err(err) => { + warn!("asusd retry failed: {err}"); + let _ = self.event_tx.send(Event::AsusdState(false)); + } + }, + // asusd is down + _ if self.asusd.get().is_none() => { + warn!("asusd unavailable, ignoring action {action:?}"); + } + // System + Action::SetPlatformProfile(ppd) => { + if let Some(asusd_proxy) = self.asusd.get() + && let Some(platform_proxy) = &asusd_proxy.platform + && let Err(err) = platform_proxy.set_platform_profile(ppd.into()).await + { + warn!("failed to set platform profile: {}", err); + }; + } + Action::SetPanelOD(b) => { + self.set_attribute(FirmwareAttribute::PanelOverdrive, b.current as i32) + .await; + let _ = self.event_tx.send(Event::UpdatedPanelOD(b)); + } + Action::SetBootSound(b) => { + self.set_attribute(FirmwareAttribute::BootSound, b.current as i32) + .await; + let _ = self.event_tx.send(Event::UpdatedBootSound(b)); + } + Action::SetScreenAutoBrightness(b) => { + self.set_attribute(FirmwareAttribute::ScreenAutoBrightness, b.current as i32) + .await; + } + Action::SetMCUPowerSave(b) => { + self.set_attribute(FirmwareAttribute::McuPowersave, b.current as i32) + .await; + } + _ => {} + } + } + + /// Write a single armory firmware attribute via D-Bus (panel OD, boot + /// sound, PPT, …). Skips with a warning when the attribute is unsupported. + async fn set_attribute(&self, attr: FirmwareAttribute, value: i32) { + let proxy = self.asusd.get().and_then(|i| i.attribute(attr)); + match proxy { + Some(p) => { + if let Err(e) = p.set_current_value(value).await { + warn!( + "could not set {value} on attribute {}: {e}", + <&str>::from(attr) + ); + } + } + None => { + warn!( + "attribute {} not supported by this device", + <&str>::from(attr) + ); } } } diff --git a/rog-control-center/src/ui/callbacks.rs b/rog-control-center/src/ui/callbacks.rs index 856808d9d..b3e0ce14c 100644 --- a/rog-control-center/src/ui/callbacks.rs +++ b/rog-control-center/src/ui/callbacks.rs @@ -1,23 +1,22 @@ -use crate::{MainWindow, SystemPageData, state::Event}; -use log::warn; +use crate::{AsusArmouryData, MainWindow, PowerData, state::Event}; use slint::ComponentHandle; use tokio::sync::mpsc::UnboundedSender; /// A simple macro used to bind an user action to an event, also handle the copy of tx macro_rules! bind { // Standard binding, to use when type is i32 - ($ui:ident, $tx:ident, $slint_callback:ident, $event_variant:expr) => { + ($ui:ident, $tx:ident, $global:ident, $slint_callback:ident, $event_variant:expr) => { let tx_clone = $tx.clone(); - $ui.global::().$slint_callback(move |val| { + $ui.global::<$global>().$slint_callback(move |val| { let _ = tx_clone.send($event_variant(val)); }); }; // Binding with a type cast - ($ui:ident, $tx:ident, $slint_callback:ident, $event_variant:expr, $cast_type:ty) => { + ($ui:ident, $tx:ident, $global:ident, $slint_callback:ident, $event_variant:expr, $cast_type:ty) => { let tx_clone = $tx.clone(); - $ui.global::().$slint_callback(move |val| { - match <$cast_type>::try_from(val) { + $ui.global::<$global>() + .$slint_callback(move |val| match <$cast_type>::try_from(val) { Ok(v) => { let _ = tx_clone.send($event_variant(v)); } @@ -30,13 +29,12 @@ macro_rules! bind { val ); } - } - }); + }); }; // Binding with no value, can be used for "Restore to Default" - ($ui:ident, $tx:ident, $slint_callback:ident => $event:expr) => { + ($ui:ident, $tx:ident, $global:ident, $slint_callback:ident => $event:expr) => { let tx_clone = $tx.clone(); - $ui.global::().$slint_callback(move || { + $ui.global::<$global>().$slint_callback(move || { let _ = tx_clone.send($event); }); }; @@ -45,13 +43,33 @@ macro_rules! bind { pub fn bind_ui_events(ui: &MainWindow, tx: UnboundedSender) { // Platform Profile - bind!(ui, tx, on_cb_platform_profile, Event::UserRequestedProfile); + bind!( + ui, + tx, + PowerData, + on_cb_platform_profile, + Event::UserRequestedPowerProfile + ); + // Home Page bind!( ui, tx, - on_cb_charge_control_end_threshold, - Event::UserRequestedBatteryLimit, - u8 + AsusArmouryData, + on_cb_boot_sound, + Event::UserRequestedBootSound ); + + bind!( + ui, + tx, + AsusArmouryData, + on_cb_panel_overdrive, + Event::UserRequestedPanelOD + ); + + // Retry asusd + ui.on_retry_asusd(move || { + let _ = tx.send(Event::RetryAsusd); + }); } diff --git a/rog-control-center/src/ui/helpers/mod.rs b/rog-control-center/src/ui/helpers/mod.rs deleted file mode 100644 index 3859a9bcf..000000000 --- a/rog-control-center/src/ui/helpers/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod hardware; -pub mod types; diff --git a/rog-control-center/src/ui/mod.rs b/rog-control-center/src/ui/mod.rs index e8634c145..3a316bb45 100644 --- a/rog-control-center/src/ui/mod.rs +++ b/rog-control-center/src/ui/mod.rs @@ -1,6 +1,5 @@ pub mod actions; pub mod callbacks; -pub mod helpers; pub mod subscriptions; pub mod toast; pub mod update; diff --git a/rog-control-center/src/ui/subscriptions.rs b/rog-control-center/src/ui/subscriptions.rs index 33b751206..19e639af7 100644 --- a/rog-control-center/src/ui/subscriptions.rs +++ b/rog-control-center/src/ui/subscriptions.rs @@ -1,10 +1,23 @@ //! Here are defined the subscriptions, they must be minimal, most of the lifting should be in a helper function +use crate::{ + helpers::{ + hardware::get_cpu_telemetry, + types::{BatteryInfo, FanTelemetry, GpuTelemetry, SystemTelemetry}, + zbus_proxies::{AsusdInterface, get_min_max_current}, + }, + state::Event, +}; +use futures_util::{Stream, StreamExt, stream::SelectAll}; +use rog_dbus::asus_armoury::AsusArmouryProxy; +use rog_platform::{asus_armoury::FirmwareAttribute, cpu::get_ram_usage_pct}; +use std::{ + collections::HashMap, + sync::{Arc, OnceLock}, + time::Duration, +}; use tokio::sync::mpsc::UnboundedSender; -use crate::{state::Event, ui::helpers::types::BatteryInfo}; -use std::time::Duration; - pub async fn subscribe_battery(tx: UnboundedSender) { loop { let charge = (rand::random::() % 101) as u8; @@ -19,3 +32,81 @@ pub async fn subscribe_battery(tx: UnboundedSender) { tokio::time::sleep(Duration::from_secs(1)).await; } } + +/// Loop that retrieve system telemetry every 1 sec +pub async fn subscribe_telemetry(tx: UnboundedSender) { + loop { + // CPU + let cpu = get_cpu_telemetry(); + + let ram = get_ram_usage_pct(); + + let telemetry = SystemTelemetry { + cpu, + dgpu: GpuTelemetry::default(), + igpu_temp: 0.0, + igpu_usage: 0.0, + ram_usage_pct: ram, + fan_rpms: FanTelemetry::default(), + }; + + if tx.send(Event::TelemetryUpdated(telemetry)).is_err() { + return; + } + + tokio::time::sleep(Duration::from_secs(1)).await + } +} + +/// Loop that waits for ppd dbus event +pub async fn subscribe_ppd(tx: UnboundedSender, asusd: Arc>) { + loop { + if let Some(asusd_proxy) = asusd.get() + && let Some(platform_proxy) = &asusd_proxy.platform + { + let mut ppd_stream = platform_proxy.receive_platform_profile_changed().await; + while let Some(msg) = ppd_stream.next().await { + if let Ok(new_ppd) = msg.get().await { + let _ = tx.send(Event::PlatformProfileSignalled(new_ppd.into())); + } + } + } else { + tokio::time::sleep(Duration::from_secs(1)).await + } + } +} + +/// Stream for all armoury attr +async fn armoury_changes( + armoury: &HashMap>, +) -> impl Stream)> { + let mut select_all = SelectAll::new(); + for (attr, iface) in armoury { + let stream: zbus::proxy::PropertyStream<'_, i32> = + iface.receive_current_value_changed().await; + select_all.push(stream.map(move |_| (*attr, iface.clone()))); + } + select_all +} + +pub async fn subscribe_armoury(tx: UnboundedSender, asusd: Arc>) { + loop { + let Some(asusd_proxy) = asusd.get() else { + tokio::time::sleep(Duration::from_secs(1)).await; + continue; + }; + + if asusd_proxy.armoury.is_empty() { + tokio::time::sleep(Duration::from_secs(1)).await; + continue; + } + + let mut changes = armoury_changes(&asusd_proxy.armoury).await; + + while let Some((attr, iface)) = changes.next().await { + if let Some(mm) = get_min_max_current(&iface).await { + let _ = tx.send(Event::firmware_attr_into_event(&attr, mm)); + } + } + } +} diff --git a/rog-control-center/src/ui/toast.rs b/rog-control-center/src/ui/toast.rs index 8f8161ab1..a63e74c34 100644 --- a/rog-control-center/src/ui/toast.rs +++ b/rog-control-center/src/ui/toast.rs @@ -4,6 +4,7 @@ use std::{ }; use crate::MainWindow; +use crate::state::ToastType; use log::error; use slint::{SharedString, Weak}; use tokio::runtime::Handle; @@ -11,10 +12,25 @@ use tokio::runtime::Handle; // A counter of toast that appears static TOAST_SEQ: AtomicU64 = AtomicU64::new(0); +/// Show a persistent toast with a retry button +pub fn show_permanent_toast( + message: SharedString, + toast_type: ToastType, + handle: Weak, +) { + let code = toast_type as i32; + let _ = slint::invoke_from_event_loop(move || { + if let Some(h) = handle.upgrade() { + h.invoke_show_permanent_toast(message, code); + } + }); +} + /// Show a toast on the user screen -pub fn show_toast(message: SharedString, handle: Weak) { +pub fn show_toast(message: SharedString, toast_type: ToastType, handle: Weak) { // Increase the counter let current_seq = TOAST_SEQ.fetch_add(1, Ordering::SeqCst) + 1; + let code = toast_type as i32; // Copy the handle to be able to use them in a later move let delayed_handle = handle.clone(); @@ -23,7 +39,7 @@ pub fn show_toast(message: SharedString, handle: Weak) { // Display the toast on the screen let _ = slint::invoke_from_event_loop(move || { if let Some(h) = handle.upgrade() { - h.invoke_show_toast(message); + h.invoke_show_toast(message, code); } }); diff --git a/rog-control-center/src/ui/update.rs b/rog-control-center/src/ui/update.rs index 22404bd47..8df95bada 100644 --- a/rog-control-center/src/ui/update.rs +++ b/rog-control-center/src/ui/update.rs @@ -1,32 +1,59 @@ //! Handle UiUpdates +use log::info; use slint::{ComponentHandle, SharedString}; -use crate::{MainWindow, SystemPageData, state::UiUpdate}; +use crate::{AsusArmouryData, MainWindow, PowerData, SystemInfo, TelemetryData, state::UiUpdate}; pub fn apply_ui_update(ui: &MainWindow, update: UiUpdate) { match update { UiUpdate::Telemetry(t) => { - let sys_data = ui.global::(); + let sys_data = ui.global::(); + // CPU sys_data.set_cpu_temp_val(t.cpu.temp); + sys_data.set_cpu_freq_mhz(t.cpu.freq_mhz); + sys_data.set_cpu_usage_val(t.cpu.usage_pct); + // RAM + sys_data.set_ram_usage_val(t.ram_usage_pct); } UiUpdate::ProductName(n) => { - let sys_data = ui.global::(); + let sys_data = ui.global::(); sys_data.set_product_name(SharedString::from(n)); } - UiUpdate::Battery(b) => { - let sys_data = ui.global::(); - sys_data.set_battery_health(b.health as i32); - } UiUpdate::PlatformProfile(p) => { - let sys_data = ui.global::(); + let sys_data = ui.global::(); sys_data.set_platform_profile(p); } + UiUpdate::BootSound(b) => { + let dev_data = ui.global::(); + dev_data.set_boot_sound(b); + } + UiUpdate::PanelOD(b) => { + let dev_data = ui.global::(); + dev_data.set_panel_overdrive(b); + } UiUpdate::ShowToast { message, - is_error: _, + toast_type, } => { - crate::ui::toast::show_toast(message.into(), ui.as_weak()); + crate::ui::toast::show_toast(message.into(), toast_type, ui.as_weak()); + } + UiUpdate::ShowPermanentToast { + message, + toast_type, + } => { + crate::ui::toast::show_permanent_toast(message.into(), toast_type, ui.as_weak()); + } + UiUpdate::AsusdState(running) => { + let sys = ui.global::(); + sys.set_asusd_running(running); + if running { + crate::ui::toast::show_toast( + "asusd connected".into(), + crate::state::ToastType::Info, + ui.as_weak(), + ); + } } UiUpdate::ToggleWindow => { if ui.window().is_visible() { @@ -44,5 +71,8 @@ pub fn apply_ui_update(ui: &MainWindow, update: UiUpdate) { UiUpdate::Quit => { let _ = slint::quit_event_loop(); } + _ => { + info!("uiupdate not implemented yet: {:?}", update); + } } } diff --git a/rog-control-center/src/ui/window.rs b/rog-control-center/src/ui/window.rs index f54a1a9f3..5024076fe 100644 --- a/rog-control-center/src/ui/window.rs +++ b/rog-control-center/src/ui/window.rs @@ -1,30 +1,16 @@ //! Used to init the GUI use log::warn; use rog_dbus::list_iface_blocking; -use slint::ComponentHandle; -use std::sync::{Arc, Mutex}; use crate::MainWindow; -use crate::config::Config; -pub fn setup_window(config: Arc>) -> MainWindow { +pub fn setup_window() -> MainWindow { slint::set_xdg_app_id(crate::APP_ID) .map_err(|e| warn!("Couldn't set application ID: {e:?}")) .ok(); let ui = MainWindow::new().expect("Couldn't create main window"); - let background_startup = match config.try_lock() { - Ok(c) => c.startup_in_background, - Err(_) => false, - }; - - if !background_startup { - if let Err(e) = ui.window().show() { - warn!("Couldn't show main window: {e:?}"); - } - } - let _available = list_iface_blocking().unwrap_or_default(); // OLD CODE, PLEASE KEEP FOR NOW, MIGHT BE USED LATER @@ -45,7 +31,7 @@ pub fn setup_window(config: Arc>) -> MainWindow { //); ui.set_sidebar_items_avilable( [ - true, true, true, true, true, true, true, true, true, true, + true, true, true, true, true, true, true, true, true, ] .into(), ); diff --git a/rog-control-center/src/zbus_proxies.rs b/rog-control-center/src/zbus_proxies.rs deleted file mode 100644 index 3ffa330ac..000000000 --- a/rog-control-center/src/zbus_proxies.rs +++ /dev/null @@ -1,84 +0,0 @@ -use log::info; - -use zbus::blocking::proxy::ProxyImpl; -use zbus::blocking::{Connection, fdo}; - -pub const ZBUS_PATH: &str = "/xyz/ljones/rogcc"; -pub const ZBUS_IFACE: &str = "xyz.ljones.rogcc"; - -pub fn find_iface(iface_name: &str) -> Result, Box> -where - T: ProxyImpl<'static> + From>, -{ - let conn = Connection::system()?; - let f = fdo::ObjectManagerProxy::new(&conn, "xyz.ljones.Asusd", "/")?; - let interfaces = f.get_managed_objects()?; - let mut paths = Vec::new(); - for v in interfaces.iter() { - // let o: Vec = v.1.keys().map(|e| - // e.to_owned()).collect(); println!("{}, {:?}", v.0, o); - for k in v.1.keys() { - if k.as_str() == iface_name { - // println!("Found {iface_name} device at {}, {}", v.0, k); - paths.push(v.0.clone()); - } - } - } - if paths.len() > 1 { - info!("Multiple asusd interfaces devices found"); - } - if !paths.is_empty() { - let mut ctrl = Vec::new(); - paths.sort_by(|a, b| a.cmp(b)); - for path in paths { - ctrl.push( - T::builder(&conn) - .path(path.clone())? - .destination("xyz.ljones.Asusd")? - .build()?, - ); - } - return Ok(ctrl); - } - - Err("No Aura interface".into()) -} - -pub async fn find_iface_async(iface_name: &str) -> Result, Box> -where - T: zbus::proxy::ProxyImpl<'static> + From>, -{ - let conn = zbus::Connection::system().await?; - let f = zbus::fdo::ObjectManagerProxy::new(&conn, "xyz.ljones.Asusd", "/").await?; - let interfaces = f.get_managed_objects().await?; - let mut paths = Vec::new(); - for v in interfaces.iter() { - // let o: Vec = v.1.keys().map(|e| - // e.to_owned()).collect(); println!("{}, {:?}", v.0, o); - for k in v.1.keys() { - if k.as_str() == iface_name { - // println!("Found {iface_name} device at {}, {}", v.0, k); - paths.push(v.0.clone()); - } - } - } - if paths.len() > 1 { - info!("Multiple asusd interfaces devices found"); - } - if !paths.is_empty() { - let mut ctrl = Vec::new(); - paths.sort_by(|a, b| a.cmp(b)); - for path in paths { - ctrl.push( - T::builder(&conn) - .path(path.clone())? - .destination("xyz.ljones.Asusd")? - .build() - .await?, - ); - } - return Ok(ctrl); - } - - Err("No interface".into()) -} diff --git a/rog-control-center/ui/callbacks.slint b/rog-control-center/ui/callbacks.slint new file mode 100644 index 000000000..8011770fb --- /dev/null +++ b/rog-control-center/ui/callbacks.slint @@ -0,0 +1,211 @@ +//! This file contain the callbacks, the function that links the UI (Slint) with the "backend" (Rust) + +import { AttrMinMax, AttrBool } from "types.slint"; + +// Per-profile power limits edited in the Home "Advanced" overlay (G-Helper +// style). One instance per configurable profile (Quiet / Balanced / +// Performance); LowPower is deliberately not configurable. +export struct ProfileLimits { + ppt_pl1_spl: AttrMinMax, + ppt_pl2_sppt: AttrMinMax, + ppt_pl3_fppt: AttrMinMax, + ppt_fppt: AttrMinMax, + ppt_apu_sppt: AttrMinMax, + ppt_platform_sppt: AttrMinMax, + nv_tgp: AttrMinMax, + nv_dynamic_boost: AttrMinMax, + nv_temp_target: AttrMinMax, +} + +export global SystemInfo { + in-out property has_dgpu: true; + in-out property has_igpu: true; + // Set from Rust: false when the asusd daemon is unreachable (all daemon + // facing controls are greyed out and actions are no-ops). + in-out property asusd_running: true; + in-out property cpu_name: ""; + in-out property igpu_name: ""; + in-out property dgpu_name: ""; + in-out property product_name: ""; +} + +export global TelemetryData { + in-out property cpu_temp_val: -1.0; + in-out property gpu_temp_val: -1.0; + in-out property igpu_temp_val: -1.0; + + in-out property cpu_usage_val: -1.0; + in-out property gpu_usage_val: -1.0; + in-out property igpu_usage_val: -1.0; + in-out property ram_usage_val: -1.0; + + in-out property cpu_freq_mhz: -1.0; + in-out property gpu_freq_mhz: -1.0; + + in-out property cpu_fan_rpm: -1; + in-out property gpu_fan_rpm: -1; + in-out property mid_fan_rpm: -1; + + in-out property dgpu_suspended: false; +} + +export global PowerData { + in-out property ppt_pl1_spl: { min: 0, max: 100, current: 20 }; + callback cb_ppt_pl1_spl(int); + callback cb_default_ppt_pl1_spl(); + + in-out property ppt_pl2_sppt: { min: 0, max: 100, current: 20 }; + callback cb_ppt_pl2_sppt(int); + callback cb_default_ppt_pl2_sppt(); + + in-out property ppt_pl3_fppt: { min: 0, max: 100, current: 20 }; + callback cb_ppt_pl3_fppt(int); + callback cb_default_ppt_pl3_fppt(); + + in-out property ppt_fppt: { min: 0, max: 100, current: 20 }; + callback cb_ppt_fppt(int); + callback cb_default_ppt_fppt(); + + in-out property ppt_apu_sppt: { min: 0, max: 100, current: 20 }; + callback cb_ppt_apu_sppt(int); + callback cb_default_ppt_apu_sppt(); + + in-out property ppt_platform_sppt: { min: 0, max: 100, current: 20 }; + callback cb_ppt_platform_sppt(int); + callback cb_default_ppt_platform_sppt(); + + in-out property nv_dynamic_boost: { min: 0, max: 30, current: 5 }; + callback cb_nv_dynamic_boost(int); + callback cb_default_nv_dynamic_boost(); + + in-out property nv_tgp: { min: 0, max: 175, current: 80 }; + callback cb_nv_tgp(int); + callback cb_default_nv_tgp(); + + in-out property nv_temp_target: { min: 0, max: 80, current: 75 }; + callback cb_nv_temp_target(int); + callback cb_default_nv_temp_target(); + + in-out property enable_ppt_group: false; + callback cb_enable_ppt_group(bool); + + in-out property ppt_enabled_available; + in-out property ppt_enabled; + callback cb_ppt_enabled(bool); + + // ===== Per-profile power limits (Advanced overlay on Home) ===== + // profile_ev arguments below use the PlatformProfile enum (Quiet=2, + // Balanced=0, Performance=1); LowPower is not configurable. Values are + // stored per profile in the client config and applied when that profile + // becomes active (see the Rust ActionHandler). + in-out property limits_quiet: { + ppt_pl1_spl: { min: 0, max: 0, current: -1, supported: false }, + ppt_pl2_sppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_pl3_fppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_fppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_apu_sppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_platform_sppt: { min: 0, max: 0, current: -1, supported: false }, + nv_tgp: { min: 0, max: 0, current: -1, supported: false }, + nv_dynamic_boost: { min: 0, max: 0, current: -1 }, + nv_temp_target: { min: 0, max: 0, current: -1, supported: false }, + }; + in-out property limits_balanced: { + ppt_pl1_spl: { min: 0, max: 0, current: -1, supported: false }, + ppt_pl2_sppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_pl3_fppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_fppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_apu_sppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_platform_sppt: { min: 0, max: -1, current: -1, supported: false }, + nv_tgp: { min: 0, max: 0, current: -1, supported: false }, + nv_dynamic_boost: { min: 0, max: 0, current: -1 }, + nv_temp_target: { min: 0, max: 0, current: -1, supported: false }, + }; + in-out property limits_performance: { + ppt_pl1_spl: { min: 0, max: 0, current: -1, supported: false }, + ppt_pl2_sppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_pl3_fppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_fppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_apu_sppt: { min: 0, max: 0, current: -1, supported: false }, + ppt_platform_sppt: { min: 0, max: -1, current: -1, supported: false }, + nv_tgp: { min: 0, max: 0, current: -1, supported: false }, + nv_dynamic_boost: { min: 0, max: 0, current: -1 }, + nv_temp_target: { min: 0, max: 0, current: -1, supported: false }, + }; + // Whether the PPT group for each profile is enabled (mirrors asusd's + // per-profile tuning gate; NVIDIA has no such gate and is always stored). + in-out property limits_enabled_quiet: false; + in-out property limits_enabled_balanced: false; + in-out property limits_enabled_performance: false; + + // NOTE: slint 1.13.1 does not allow parameter NAMES on callbacks, only + // types — arguments are positional (profile_ev first, value second). Names + // use cb_set_/cb_reset_ to avoid clashing with the single-value callbacks + // declared above (slint has no callback overloading). + callback cb_set_ppt_pl1_spl(int, int); + callback cb_reset_ppt_pl1_spl(int); + callback cb_set_ppt_pl2_sppt(int, int); + callback cb_reset_ppt_pl2_sppt(int); + callback cb_set_ppt_pl3_fppt(int, int); + callback cb_reset_ppt_pl3_fppt(int); + callback cb_set_ppt_fppt(int, int); + callback cb_reset_ppt_fppt(int); + callback cb_set_ppt_apu_sppt(int, int); + callback cb_reset_ppt_apu_sppt(int); + callback cb_set_ppt_platform_sppt(int, int); + callback cb_reset_ppt_platform_sppt(int); + callback cb_set_nv_tgp(int, int); + callback cb_reset_nv_tgp(int); + callback cb_set_nv_dynamic_boost(int, int); + callback cb_reset_nv_dynamic_boost(int); + callback cb_set_nv_temp_target(int, int); + callback cb_reset_nv_temp_target(int); + callback cb_enable_profile_limits(int, bool); + callback cb_set_epp(int, int); + + in-out property platform_profile: 0; + in-out property <[string]> platform_profile_choices: [@tr("LowPower"), @tr("Quiet"), @tr("Balanced"), @tr("Performance")]; + in-out property <[int]> platform_profile_indexes: [3, 2, 0, 1]; + callback cb_platform_profile(int); + + in-out property <[string]> energy_performance_choices: [ + @tr("Default"), + @tr("Performance"), + @tr("BalancePerformance"), + @tr("BalancePower"), + @tr("Power") + ]; + in-out property profile_balanced_epp: 0; + callback cb_profile_balanced_epp(int); + in-out property profile_performance_epp: 0; + callback cb_profile_performance_epp(int); + in-out property profile_quiet_epp: 0; + callback cb_profile_quiet_epp(int); + in-out property platform_profile_linked_epp: true; + callback cb_platform_profile_linked_epp(bool); + + in-out property platform_profile_on_ac: 0; + callback cb_platform_profile_on_ac(int); + in-out property change_platform_profile_on_ac: true; + callback cb_change_platform_profile_on_ac(bool); + + in-out property platform_profile_on_battery: 0; + callback cb_platform_profile_on_battery(int); + in-out property change_platform_profile_on_battery: true; + callback cb_change_platform_profile_on_battery(bool); +} + +export global AsusArmouryData { + in-out property asus_armoury_loaded: false; + + in-out property panel_overdrive; + callback cb_panel_overdrive(AttrBool); + + in-out property boot_sound; + callback cb_boot_sound(AttrBool); + + in-out property mcu_powersave; + callback cb_mcu_powersave(AttrBool); + + in-out property screen_auto_brightness; + callback cb_screen_auto_brightness(AttrBool); +} diff --git a/rog-control-center/ui/main_window.slint b/rog-control-center/ui/main_window.slint index ba69b36d6..a5598be3b 100644 --- a/rog-control-center/ui/main_window.slint +++ b/rog-control-center/ui/main_window.slint @@ -1,167 +1,310 @@ -// rog-control-center — dashboard shell (AC-style base design) -// Sidebar + Home dashboard. Pages that are not ported yet render an empty -// (background-only) placeholder so every nav entry remains reachable while -// the redesign is built out page by page. import { Palette } from "std-widgets.slint"; import { AppSize, Theme } from "globals.slint"; -import { SystemPageData, AttrMinMax } from "pages/system.slint"; +import { + SystemInfo, + TelemetryData, + PowerData, + AsusArmouryData, +} from "callbacks.slint"; +import { AttrMinMax } from "types.slint"; import { PageHome } from "pages/home.slint"; +import { PerProfileSettingsOverlay } from "pages/per_profile_settings.slint"; import { SideBar } from "widgets/sidebar.slint"; -export { AppSize, AttrMinMax, SystemPageData } +export { + AppSize, + AttrMinMax, + SystemInfo, + TelemetryData, + PowerData, + AsusArmouryData, +} +export component AsusTray inherits SystemTrayIcon { + icon: @image-url("../data/rog-sidebar-logo.png"); + tooltip: "Rog Control Center"; + callback show-window(); + callback quit-window(); + clicked => { show-window(); } + + Menu { + MenuItem { + title: "Quit ROGCC"; + activated => { quit-window(); } + } + } +} export component MainWindow inherits Window { title: "ROG Control Center"; always-on-top: true; - default-font-family: "Noto Sans"; + default-font-family: "sans-serif"; default-font-size: 14px; default-font-weight: 400; icon: @image-url("../data/rog-control-center.png"); in property <[bool]> sidebar_items_avilable: [ true, // System Control (Home) - true, - true, - true, - true, - true, + true, // Keyboard Aura + true, // AniMe Matrix + true, // Slash Lighting + true, // Fan Curves true, // GPU Configuration true, // Battery Info true, // App Settings true, // About ]; - private property toast: false; - private property toast_text: "I show when something is waiting"; - callback show_toast(string); - show_toast(text) => { - toast = text != ""; - toast_text = text; - } - callback clear_toast_if_matches(string); - clear_toast_if_matches(text) => { - if (toast && toast_text == text) { - toast = false; - toast_text = ""; + private property toast: false; + private property toast_permanent: false; + private property toast_text: "I show when something is waiting"; + // 0 = info, 1 = warn, 2 = error + private property toast_type: 0; + callback show_toast(string, int); + show_toast(text, kind) => { + toast = text != ""; + toast_permanent = false; + toast_text = text; + toast_type = kind; } - } - min-height: AppSize.height; + callback show_permanent_toast(string, int); + show_permanent_toast(text, kind) => { + toast = text != ""; + toast_permanent = true; + toast_text = text; + toast_type = kind; + } + callback clear_permanent_toast(); + clear_permanent_toast() => { + if (toast_permanent) { + toast = false; + toast_text = ""; + } + } + // User pressed "Retry" on the permanent asusd-unavailable toast. + callback retry_asusd(); + callback clear_toast_if_matches(string); + clear_toast_if_matches(text) => { + if (toast && toast_text == text) { + toast = false; + toast_text = ""; + } + } + // Advanced (per-profile power settings) overlay control. + private property show_per_profile_overlay: false; + callback open_per_profile_settings(); + callback close_per_profile_settings(); + open_per_profile_settings() => { + root.show_per_profile_overlay = true; + } + close_per_profile_settings() => { + root.show_per_profile_overlay = false; + } + background: Theme.bg-base; + + // Window sizing: min keeps the dashboard usable on small laptops, the + // preferred size gives it room on normal laptops and monitors. min-width: AppSize.width; - // Default open size — taller than the handheld-friendly min so the dashboard - // has room on normal laptops. Resizable between min and max. - preferred-width: 1000px; - preferred-height: 700px; - max-width: 2560px; - max-height: 2000px; - background: Theme.bg-base; - - HorizontalLayout { - padding: 0px; - side-bar := SideBar { + min-height: AppSize.height; + preferred-width: 1000px; + preferred-height: 700px; + + HorizontalLayout { + padding: 0px; + side-bar := SideBar { // Title + icon travel together as one model — no separate icon array // to keep in sync when entries are added or reordered. model: [ - { title: @tr("Menu1" => "System Control"), icon: @image-url("images/nav-home.svg") }, - { title: @tr("Menu_system" => "System Tuning"), icon: @image-url("images/ic-cpu.svg") }, - { title: @tr("Menu2" => "Keyboard Aura"), icon: @image-url("images/nav-keyboard.svg") }, - { title: @tr("Menu3" => "AniMe Matrix"), icon: @image-url("images/nav-matrix.svg") }, - { title: @tr("Menu4" => "Slash Lighting"), icon: @image-url("images/nav-slash.svg") }, - { title: @tr("Menu5" => "Fan Curves"), icon: @image-url("images/nav-fan.svg") }, - { title: @tr("Menu6" => "GPU Configuration"), icon: @image-url("images/nav-gpu.svg") }, - { title: @tr("Menu9" => "Battery Info"), icon: @image-url("images/nav-battery.svg") }, - { title: @tr("Menu7" => "App Settings"), icon: @image-url("images/nav-apps.svg") }, - { title: @tr("Menu8" => "About"), icon: @image-url("images/nav-about.svg") }, - ]; - available: root.sidebar_items_avilable; - } + { title: @tr("Menu1" => "System Control"), icon: @image-url("images/nav-home.svg") }, + { title: @tr("Menu2" => "Keyboard Aura"), icon: @image-url("images/nav-keyboard.svg") }, + { title: @tr("Menu3" => "AniMe Matrix"), icon: @image-url("images/nav-matrix.svg") }, + { title: @tr("Menu4" => "Slash Lighting"), icon: @image-url("images/nav-slash.svg") }, + { title: @tr("Menu5" => "Fan Curves"), icon: @image-url("images/nav-fan.svg") }, + { title: @tr("Menu6" => "GPU Configuration"), icon: @image-url("images/nav-gpu.svg") }, + { title: @tr("Menu9" => "Battery Info"), icon: @image-url("images/nav-battery.svg") }, + { title: @tr("Menu7" => "App Settings"), icon: @image-url("images/nav-apps.svg") }, + { title: @tr("Menu8" => "About"), icon: @image-url("images/nav-about.svg") }, + ]; + available: root.sidebar_items_avilable; + } + // Page host: stretches to fill everything right of the sidebar. Rectangle { - property max-content-width: 1100px; - background: Theme.bg-base; - // Home dashboard fills the area right of the sidebar. + horizontal-stretch: 1; + background: Theme.bg-base; + + // Home dashboard fills the whole area right of the sidebar; the + // page scales its content up on wide monitors on its own. if side-bar.current-item == 0: PageHome { - // Responsive width: fill small windows, cap at max-content-width and center on large ones. - width: parent.width > parent.max-content-width ? parent.max-content-width : parent.width; - height: parent.height; - x: parent.width > parent.max-content-width ? (parent.width - parent.max-content-width) / 2 : 0px; - } - // Every not-yet-ported page: empty placeholder (background only). + width: 100%; + height: 100%; + open-per-profile-settings => { + root.open_per_profile_settings(); + } + } + // Every not-yet-ported page. if side-bar.current-item > 0: Rectangle { - width: parent.width; - height: parent.height; - background: Theme.bg-base; + width: 100%; + height: 100%; + background: Theme.bg-base; + } } } - } - // Toast: pill that fades + slides in from the bottom, centred on - // the WHOLE window (independent of the sidebar width). Tap to - // dismiss. Always mounted so the enter/exit animations can play. - Rectangle { - property pill-w: (toast-lbl.preferred-width + 48px) > (root.width - 32px) ? (root.width - 32px) : (toast-lbl.preferred-width + 48px); - x: (root.width - self.width) / 2; - y: root.height - 24px - (toast ? 0px : 16px) - self.height; - width: pill-w; - height: 40px; - border-radius: 20px; - background: Theme.bg-elevated; - border-width: 1px; - border-color: Theme.line; - drop-shadow-blur: 18px; - drop-shadow-color: #00000060; - opacity: toast ? 1.0 : 0.0; - visible: self.opacity > 0.01; - animate opacity { duration: 220ms; easing: ease-out; } - animate y { duration: 220ms; easing: cubic-bezier(0.22, 0.61, 0.36, 1); } - TouchArea { - height: 100%; + if SomeError.error_message != "": Rectangle { + x: 0px; + y: 0px; + width: root.width; + height: root.height; + background: Palette.background; + border-color: Palette.border; + border-width: 3px; + border-radius: 10px; + + TouchArea { + width: 100%; + height: 100%; + } + + VerticalLayout { + padding: 20px; + spacing: 12px; + alignment: center; + Text { + text <=> SomeError.error_message; + font-size: 18px; + color: Palette.control-foreground; + horizontal-alignment: center; + } + + Text { + text <=> SomeError.error_help; + color: Palette.control-foreground; + horizontal-alignment: center; + } + } + } + + // Advanced per-profile power settings overlay — declared last so it sits on + // top of every page (including the toast + error overlays). + PerProfileSettingsOverlay { width: 100%; - mouse-cursor: pointer; - clicked => { - toast = false; + height: 100%; + open: root.show_per_profile_overlay; + close() => { + root.close_per_profile_settings(); } } - toast-lbl := Text { - width: parent.width - 48px; - color: Theme.text-primary; - font-size: Theme.font-sm; - text: root.toast_text; - horizontal-alignment: center; - vertical-alignment: center; - overflow: elide; + + // Grey-out: full-window dim when asusd is unreachable. Blocks every click + // underneath (whole window), so the UI is visually greyed + interaction + // disabled but the app keeps running. The permanent toast below is on top + // of this so its Retry button stays clickable. + if !SystemInfo.asusd_running: Rectangle { + x: 0px; + y: 0px; + width: 100%; + height: 100%; + background: Theme.dark ? #000000 : #FFFFFF; + opacity: 0.55; + TouchArea { + width: 100%; + height: 100%; + } + + VerticalLayout { + alignment: center; + spacing: 10px; + Text { + text: @tr("asusd-unavailable" => "asusd is not running"); + color: Theme.text-primary; + font-size: Theme.font-lg; + font-weight: 600; + horizontal-alignment: center; + } + + Text { + text: @tr("asusd-unavailable-hint" => "Controls are disabled. Press Retry once asusd is up."); + color: Theme.text-secondary; + font-size: Theme.font-sm; + horizontal-alignment: center; + } + } } - } - if SomeError.error_message != "": Rectangle { - x: 0px; - y: 0px; - width: root.width; - height: root.height; - background: Palette.background; - border-color: Palette.border; - border-width: 3px; - border-radius: 10px; - - TouchArea { width: 100%; height: 100%; } - - VerticalLayout { - padding: 20px; - spacing: 12px; - alignment: center; - Text { - text <=> SomeError.error_message; - font-size: 18px; - color: Palette.control-foreground; - horizontal-alignment: center; + toast-pill := Rectangle { + property toast_accent: root.toast_type == 2 ? #E85656 : (root.toast_type == 1 ? #FFBD00 : #00A8E4); + property toast_bg: root.toast_type == 2 ? #5C1F24E6 : (root.toast_type == 1 ? #4D3A10E6 : #103247E6); + x: (root.width - self.width) / 2; + y: parent.height - self.height - 10px; + width: Math.min(640px, root.width - 32px); + height: 40px; + border-radius: 20px; + background: toast-pill.toast_bg; + border-width: 1px; + border-color: toast-pill.toast_accent; + drop-shadow-blur: 18px; + drop-shadow-color: #00000060; + opacity: toast ? 1.0 : 0.0; + visible: self.opacity > 0.01; + animate opacity { + duration: 220ms; + easing: ease-out; + } + animate y { + duration: 220ms; + easing: cubic-bezier(0.22, 0.61, 0.36, 1); + } + TouchArea { + height: 100%; + width: 100%; + mouse-cursor: pointer; + clicked => { + if (!toast_permanent) { + toast = false; + } + } } - Text { - text <=> SomeError.error_help; - color: Palette.control-foreground; + + toast-lbl := Text { + color: Theme.text-primary; + x: 20px; + width: parent.width - (toast_permanent ? 96px : 40px); + y: 0px; + height: 100%; + font-size: Theme.font-sm; + text: root.toast_text; + vertical-alignment: center; horizontal-alignment: center; + overflow: elide; + } + + if toast_permanent: Rectangle { + x: parent.width - 70px - 6px; + y: 6px; + width: 70px; + height: parent.height - 12px; + border-radius: 14px; + background: toast-pill.toast_accent; + Text { + text: @tr("Retry"); + color: #FFFFFF; + font-size: Theme.font-sm; + font-weight: 600; + horizontal-alignment: center; + vertical-alignment: center; + } + + retry-ta := TouchArea { + width: 100%; + height: 100%; + mouse-cursor: pointer; + clicked => { + root.retry_asusd(); + } + } } } } -} export global SomeError { - in property error_message: ""; - in property error_help: ""; -} \ No newline at end of file + in property error_message: ""; + in property error_help: ""; + } diff --git a/rog-control-center/ui/pages/home.slint b/rog-control-center/ui/pages/home.slint index d24a6cc02..7fbae85a3 100644 --- a/rog-control-center/ui/pages/home.slint +++ b/rog-control-center/ui/pages/home.slint @@ -1,75 +1,114 @@ -// PageHome — Dashboard page matching AC system-page.html reference -// Layout: HorizontalLayout 2-col (left 360px fixed + right flexible), no ScrollView (avoids binding loops) -// Font: Inter Display for product name -// Data: all from SystemPageData, no hardcoded values -import { RogProductCard, RogSlashMark, RogMetric, RogReading, RogStatRow, RogPage, RogModeGrid } from "../widgets/rog.slint"; +import { RogProductCard } from "../widgets/cards.slint"; +import { RogSlashMark, RogPage } from "../widgets/layout.slint"; +import { RogMetric, RogReading, RogStatRow } from "../widgets/metrics.slint"; +import { RogModeGrid, RogToggle } from "../widgets/controls.slint"; import { Theme } from "../globals.slint"; -import { SystemPageData } from "system.slint"; +import { + SystemInfo, + TelemetryData, + PowerData, + AsusArmouryData, +} from "../callbacks.slint"; +import { AttrBool } from "../types.slint"; +// NOTE (slint 1.13.1 engine bug): a VerticalLayout used as the FIRST child of +// a HorizontalLayout makes every later cell render empty. The product card is +// therefore a direct Rectangle-based child of the columns row (no VerticalLayout +// wrapper around it). Keep the left cell a Rectangle-like element, not a layout. export component PageHome inherits RogPage { background: Theme.bg-base; clip: true; + // Forwarded to the main window host, which owns the Advanced overlay. + callback open-per-profile-settings(); - // Product name from SystemPageData (backend reads /sys/class/dmi/id/product_name) - - // ====== Titlebar ====== - titlebar := Rectangle { - y: 0px; - height: 52px; - width: 100%; - background: Theme.bg-base; - Text { - text: @tr("Home"); - color: Theme.text-primary; - font-size: 20px; - font-weight: 700; - x: 32px; - y: 13px; - } - } + // ====== Responsive sizing ====== + // Below ~1000px of page width (narrower laptops) the dashboard tightens: + // smaller product card, tighter gaps and capped metric cards (compact). + // Above that everything scales with `content-width`. On wide screens a + // future-graphs zone appears on the right (show-graphs); then the left + // dashboard is bounded to `left-bound` (≈ the 1366 laptop width) instead of + // stretching endlessly, and the graphs take the remaining space. + property compact: root.width < 1000px; + // Graphs zone shows only on big windows; hidden on laptops → current layout. + property show-graphs: root.width >= 1500px; + // Width the dashboard content should occupy when the graphs zone is visible. + property left-bound: 1260px; + property content-width: root.compact ? root.width : (root.show-graphs ? root.left-bound : root.width); + property card-side: root.compact ? 300px : Math.min(520px, root.content-width * 0.31); + property gap-lg: root.compact ? 24px : Math.min(48px, root.content-width * 0.031); + property gap-md: root.compact ? 16px : Math.min(28px, root.content-width * 0.017); + property pad-x: root.compact ? 20px : Math.min(44px, root.content-width * 0.02); + property right-col: root.content-width - 2 * root.pad-x - root.card-side - root.gap-lg; + property metric-max: root.compact ? 300px : Math.min(600px, (root.right-col - root.gap-lg) / 2); + + // Columns-row bookkeeping for the graphs zone (slint 1.13.1 is unreliable + // with an invisible/stretched cell, so the zone's width is explicit): + // the row spans the page minus the dashboard padding. + property columns-w: root.width - 2 * root.pad-x; + // Cap for the telemetry (right column) when the graphs zone is shown. + property rightcap: root.left-bound - root.card-side - root.gap-lg; + // Width the graphs zone takes when shown = everything left over. + property graphs-w: root.columns-w - root.card-side - 2 * root.gap-lg - root.rightcap; - // ====== Dashboard — fills remaining window height so the vertical flex - // gaps inside the right column can grow in fullscreen. ====== - dashboard := Rectangle { - y: titlebar.height; - width: 100%; - height: parent.height - titlebar.height; + // ====== Titlebar + Dashboard Root Layout ====== + VerticalLayout { + spacing: 0px; + + // ====== Titlebar ====== + Rectangle { + height: 52px; + background: Theme.bg-base; + HorizontalLayout { + padding-left: 32px; + Text { + text: @tr("Home"); + color: Theme.text-primary; + font-size: 20px; + font-weight: 700; + vertical-alignment: center; + } + } + } + // ====== Dashboard — fills remaining height via vertical-stretch: 1 ====== VerticalLayout { - padding-left: 24px; - padding-right: 24px; + vertical-stretch: 1; + padding-left: root.pad-x; + padding-right: root.pad-x; padding-top: 12px; padding-bottom: 24px; spacing: 0px; + // Columns: bounded left dashboard (product card + telemetry) and, only + // on wide windows (show-graphs), a graphs zone on the right. HorizontalLayout { - spacing: 36px; - - // Left column: product card (kept square) - VerticalLayout { - width: 360px; - alignment: start; + spacing: root.gap-lg; + // Left column: the product card, directly as a (Rectangle-based) + // child — never wrapped in a VerticalLayout (see note above). + // Square via the fixed card-side length; no parent geometry. product-card := RogProductCard { - width: 100%; - // Keep the card square: height tracks the column width. - height: parent.width; - name: SystemPageData.product_name != "" ? SystemPageData.product_name : "ROG Laptop"; - // CPU + GPU straight off the backend — no hardcoded specs. - details: [ - SystemPageData.cpu_name, - SystemPageData.has_dgpu ? SystemPageData.dgpu_name : SystemPageData.igpu_name, - ]; - } + width: root.card-side; + height: root.card-side; + name: SystemInfo.product_name != "" ? SystemInfo.product_name : "ROG Laptop"; + // CPU + GPU straight off the backend — no hardcoded specs. + details: [ + SystemInfo.cpu_name, + SystemInfo.has_dgpu ? SystemInfo.dgpu_name : SystemInfo.igpu_name, + ]; } // Right column: System Usage + Operation Mode. - // No `alignment` (default distributes free space by stretch). - // Content blocks have vertical-stretch:0 so they keep their - // natural height; only the flex spacers grow in fullscreen. + // Content blocks keep their natural height (vertical-stretch: 0); + // the flex spacers grow instead, so big windows just get taller + // gaps and nothing overflows on short ones. VerticalLayout { horizontal-stretch: 1; - spacing: 20px; + // Bound the telemetry when the graphs zone is shown so the + // dashboard keeps its laptop look instead of stretching far + // right; otherwise it fills the whole width (like today). + max-width: root.show-graphs ? root.rightcap : 999999px; + spacing: root.gap-md; // System Usage header Rectangle { @@ -77,7 +116,8 @@ export component PageHome inherits RogPage { vertical-stretch: 0; HorizontalLayout { spacing: 8px; - RogSlashMark {} + RogSlashMark { } + Text { text: @tr("System Usage"); color: Theme.text-primary; @@ -85,110 +125,138 @@ export component PageHome inherits RogPage { font-weight: 600; vertical-alignment: center; } + + Rectangle { + horizontal-stretch: 1; + } } } - // Row 1: CPU | GPU — cards capped so they don't over-widen; - // the fixed 36px middle gap stays as it was. + // Row 1: CPU | GPU HorizontalLayout { vertical-stretch: 0; - spacing: 36px; + spacing: root.gap-lg; cpu-metric := RogMetric { horizontal-stretch: 1; - max-width: 340px; - title: "CPU"; + max-width: root.metric-max; + title: SystemInfo.cpu_name != "" ? SystemInfo.cpu_name : @tr("Unknown"); RogReading { label: @tr("Frequency"); - value: SystemPageData.cpu_freq_mhz > 0.0 ? "\{Math.round(SystemPageData.cpu_freq_mhz)} MHz" : "--"; - progress: SystemPageData.cpu_freq_mhz / 5000.0; + value: TelemetryData.cpu_freq_mhz > 0.0 ? "\{Math.round(TelemetryData.cpu_freq_mhz)} MHz" : "--"; + progress: TelemetryData.cpu_freq_mhz / 5000.0; } + RogReading { label: @tr("Usage"); - value: SystemPageData.cpu_usage_val >= 0.0 ? "\{Math.round(SystemPageData.cpu_usage_val)} %" : "--"; - progress: SystemPageData.cpu_usage_val / 100.0; + value: TelemetryData.cpu_usage_val >= 0.0 ? "\{Math.round(TelemetryData.cpu_usage_val)} %" : "--"; + progress: TelemetryData.cpu_usage_val / 100.0; } - RogStatRow { + + RogReading { label: @tr("Temperature"); - value: SystemPageData.cpu_temp_val > 0.0 ? "\{Math.round(SystemPageData.cpu_temp_val)} °C" : "--"; + value: TelemetryData.cpu_temp_val > 0.0 ? "\{Math.round(TelemetryData.cpu_temp_val)} °C" : "--"; + // Gauge 0..100 °C (RogMeter clamps to 0..1). + progress: TelemetryData.cpu_temp_val / 100.0; } } - if SystemPageData.has_dgpu: RogMetric { + if SystemInfo.has_dgpu: RogMetric { horizontal-stretch: 1; - max-width: 340px; - title: "dGPU"; + max-width: root.metric-max; + title: SystemInfo.dgpu_name != "" ? SystemInfo.dgpu_name : @tr("Unknown"); // Each field judges independently: a suspended dGPU shows // "Suspended" everywhere, but a readable field still shows // even when another one can't be read right now. RogReading { label: @tr("Frequency"); - value: SystemPageData.dgpu_suspended ? @tr("Suspended") : (SystemPageData.gpu_freq_mhz > 0.0 ? "\{Math.round(SystemPageData.gpu_freq_mhz)} MHz" : "--"); - progress: SystemPageData.gpu_freq_mhz / 2500.0; + value: TelemetryData.dgpu_suspended ? @tr("Suspended") : (TelemetryData.gpu_freq_mhz > 0.0 ? "\{Math.round(TelemetryData.gpu_freq_mhz)} MHz" : "--"); + progress: TelemetryData.gpu_freq_mhz / 2500.0; } + RogReading { label: @tr("Usage"); - value: SystemPageData.dgpu_suspended ? @tr("Suspended") : (SystemPageData.gpu_usage_val >= 0.0 ? "\{Math.round(SystemPageData.gpu_usage_val)} %" : "--"); - progress: SystemPageData.gpu_usage_val / 100.0; + value: TelemetryData.dgpu_suspended ? @tr("Suspended") : (TelemetryData.gpu_usage_val >= 0.0 ? "\{Math.round(TelemetryData.gpu_usage_val)} %" : "--"); + progress: TelemetryData.gpu_usage_val / 100.0; } - RogStatRow { + + RogReading { label: @tr("Temperature"); - value: SystemPageData.dgpu_suspended ? @tr("Suspended") : (SystemPageData.gpu_temp_val > 0.0 ? "\{Math.round(SystemPageData.gpu_temp_val)} °C" : "--"); + value: TelemetryData.dgpu_suspended ? @tr("Suspended") : (TelemetryData.gpu_temp_val > 0.0 ? "\{Math.round(TelemetryData.gpu_temp_val)} °C" : "--"); + progress: TelemetryData.dgpu_suspended ? 0.0 : (TelemetryData.gpu_temp_val / 100.0); } + RogStatRow { label: @tr("TGP"); - value: SystemPageData.dgpu_suspended ? @tr("Suspended") : (SystemPageData.nv_tgp.current >= 0 ? "\{Math.round(SystemPageData.nv_tgp.current)} W" : "--"); + value: TelemetryData.dgpu_suspended ? @tr("Suspended") : (PowerData.nv_tgp.current >= 0 ? "\{Math.round(PowerData.nv_tgp.current)} W" : "--"); } } } // Vertical flex gap: grows in fullscreen, keeps the rows apart. - Rectangle { vertical-stretch: 1; min-height: 0px; } + Rectangle { + vertical-stretch: 1; + min-height: 0px; + } - // Row 2: Fan | Memory + // Row 2: Fan | iGPU HorizontalLayout { vertical-stretch: 0; - spacing: 36px; + spacing: root.gap-lg; fan-metric := RogMetric { horizontal-stretch: 1; - max-width: 340px; + max-width: root.metric-max; title: @tr("Fan"); - if SystemPageData.cpu_fan_rpm >= 0: RogReading { + // All three fan bars are always shown; -1 rpm (absent + // sensor) renders "--" with an empty meter so the card + // never looks empty. 0 rpm is a real reading. + RogReading { label: @tr("CPU Fan"); - // 0 RPM is a real reading (idle/stopped fan); only the - // -1 sentinel from the backend means "no sensor". - value: "\{SystemPageData.cpu_fan_rpm} RPM"; - progress: Math.max(0, SystemPageData.cpu_fan_rpm) / 5000.0; - } - // Hide the GPU / mid fan rows entirely when the sensor is - // absent (-1 sentinel) instead of leaving a "--" placeholder. - if SystemPageData.gpu_fan_rpm >= 0: RogReading { + value: TelemetryData.cpu_fan_rpm >= 0 ? "\{TelemetryData.cpu_fan_rpm} RPM" : "--"; + progress: Math.max(0, TelemetryData.cpu_fan_rpm) / 5000.0; + } + + RogReading { label: @tr("GPU Fan"); - value: "\{SystemPageData.gpu_fan_rpm} RPM"; - progress: SystemPageData.gpu_fan_rpm / 5000.0; + value: TelemetryData.gpu_fan_rpm >= 0 ? "\{TelemetryData.gpu_fan_rpm} RPM" : "--"; + progress: Math.max(0, TelemetryData.gpu_fan_rpm) / 5000.0; } - if SystemPageData.mid_fan_rpm >= 0: RogReading { + + RogReading { label: @tr("Mid Fan"); - value: "\{SystemPageData.mid_fan_rpm} RPM"; - progress: SystemPageData.mid_fan_rpm / 5000.0; + value: TelemetryData.mid_fan_rpm >= 0 ? "\{TelemetryData.mid_fan_rpm} RPM" : "--"; + progress: Math.max(0, TelemetryData.mid_fan_rpm) / 5000.0; } } - mem-metric := RogMetric { + // iGPU is always shown — a laptop always has one. + // (No bare `if` conditional here: a conditional as a + // Row-2 layout child breaks the column's stretch on + // slint 1.13.1.) + igpu-metric := RogMetric { horizontal-stretch: 1; - max-width: 340px; - title: @tr("Memory"); + max-width: root.metric-max; + title: SystemInfo.igpu_name != "" ? SystemInfo.igpu_name : @tr("Unknown"); + RogReading { + label: @tr("Temperature"); + value: TelemetryData.igpu_temp_val > 0.0 ? "\{Math.round(TelemetryData.igpu_temp_val)} °C" : "--"; + progress: TelemetryData.igpu_temp_val / 100.0; + } + RogReading { - label: "RAM"; - value: SystemPageData.ram_usage_val >= 0.0 ? "\{Math.round(SystemPageData.ram_usage_val)} %" : "--"; - progress: SystemPageData.ram_usage_val / 100.0; + label: @tr("Usage"); + value: TelemetryData.igpu_usage_val >= 0.0 ? "\{Math.round(TelemetryData.igpu_usage_val)} %" : "--"; + progress: TelemetryData.igpu_usage_val / 100.0; } } } // Vertical flex gap before the mode section. - Rectangle { vertical-stretch: 1; min-height: 0px; } + Rectangle { + vertical-stretch: 1; + min-height: 0px; + } // Operation Mode section — switches the real asusd platform // profile. Buttons are generated from the device's actual @@ -196,15 +264,16 @@ export component PageHome inherits RogPage { // the hardware instead of a hardcoded AC-style set. ev is the // profile enum value (passed to cb_platform_profile); p is the // position (used for layout, label lookup and the active check). - VerticalLayout { + if PowerData.platform_profile_indexes.length > 0: VerticalLayout { vertical-stretch: 0; spacing: 12px; - if SystemPageData.platform_profile_indexes.length > 0: Rectangle { + Rectangle { height: 24px; HorizontalLayout { spacing: 8px; - RogSlashMark {} + RogSlashMark { } + Text { text: @tr("Operation Mode"); color: Theme.text-primary; @@ -212,16 +281,135 @@ export component PageHome inherits RogPage { font-weight: 600; vertical-alignment: center; } - Rectangle { horizontal-stretch: 1; } + + Rectangle { + horizontal-stretch: 1; + } } } - if SystemPageData.platform_profile_indexes.length > 0: RogModeGrid { - profile-indexes: SystemPageData.platform_profile_indexes; - profile-choices: SystemPageData.platform_profile_choices; - current-profile: SystemPageData.platform_profile; + RogModeGrid { + profile-indexes: PowerData.platform_profile_indexes; + current-profile: PowerData.platform_profile; selected(ev) => { - SystemPageData.cb_platform_profile(ev); + PowerData.cb_platform_profile(ev); + } + advanced() => { + root.open-per-profile-settings(); + } + } + } + + RogMetric { + horizontal-stretch: 1; + title: @tr("Asus-Armoury"); + if !AsusArmouryData.asus_armoury_loaded: armoury_warning := Rectangle { + Text { + text: @tr("Asus-Armoury not loaded"); + color: Theme.text-primary; + font-size: Theme.font-md; + font-weight: 600; + } + } + if AsusArmouryData.boot_sound.supported: bount_sound_rect := Rectangle { + height: 24px; + vertical-stretch: 1; + HorizontalLayout { + spacing: 8px; + Text { + text: @tr("Boot Sound"); + color: Theme.text-primary; + font-size: Theme.font-md; + font-weight: 600; + } + + RogToggle { + checked: AsusArmouryData.boot_sound.current; + toggled(v) => { + AsusArmouryData.cb_boot_sound({ + current: v, + supported: AsusArmouryData.boot_sound.supported, + }); + } + } + } + } + + if AsusArmouryData.panel_overdrive.supported: panel_od_rect := Rectangle { + height: 24px; + vertical-stretch: 1; + HorizontalLayout { + spacing: 8px; + Text { + text: @tr("Panel OverDrive"); + color: Theme.text-primary; + font-size: Theme.font-md; + font-weight: 600; + } + + RogToggle { + checked: AsusArmouryData.panel_overdrive.current; + toggled(v) => { + AsusArmouryData.cb_panel_overdrive({ current: v, supported: AsusArmouryData.panel_overdrive.supported }); + } + } + } + } + } + } + + // Future graphs zone: CPU / GPU placeholders — only on wide windows. + graphs-zone := Rectangle { + visible: root.show-graphs; + horizontal-stretch: 0; + width: root.show-graphs ? root.graphs-w : 0px; + VerticalLayout { + spacing: root.gap-md; + Rectangle { + vertical-stretch: 1; + background: Theme.bg-card; + border-radius: Theme.radius-card; + border-width: 1px; + border-color: Theme.line; + VerticalLayout { + alignment: center; + spacing: 8px; + Text { + text: @tr("CPU"); + color: Theme.text-primary; + font-size: 22px; + font-weight: 700; + } + + Text { + text: @tr("graphs-coming-soon" => "Graphs coming soon"); + color: Theme.text-dim; + font-size: 13px; + } + } + } + + Rectangle { + vertical-stretch: 1; + background: Theme.bg-card; + border-radius: Theme.radius-card; + border-width: 1px; + border-color: Theme.line; + VerticalLayout { + alignment: center; + spacing: 8px; + Text { + text: @tr("GPU"); + color: Theme.text-primary; + font-size: 22px; + font-weight: 700; + } + + Text { + text: @tr("graphs-coming-soon" => "Graphs coming soon"); + color: Theme.text-dim; + font-size: 13px; + } } } } diff --git a/rog-control-center/ui/pages/per_profile_settings.slint b/rog-control-center/ui/pages/per_profile_settings.slint new file mode 100644 index 000000000..396024fea --- /dev/null +++ b/rog-control-center/ui/pages/per_profile_settings.slint @@ -0,0 +1,535 @@ +// Modal "Advanced" per-profile power settings (Home -> Operation Mode -> +// Advanced). Three configurable profiles: Quiet / Balanced / Performance. +// LowPower is deliberately not configurable. +// +// All rows here are kept Rectangle-based / use layouts in safe positions (this +// project is pinned to slint 1.13.1, see ui/README.md about layouts). +import { RogSlashMark } from "../widgets/layout.slint"; +import { RogSlider } from "../widgets/controls.slint"; +import { ScrollView } from "std-widgets.slint"; +import { Theme } from "../globals.slint"; +import { PowerData, ProfileLimits } from "../callbacks.slint"; + +// — AC-style toggle switch — +component RogToggle inherits Rectangle { + in property checked: false; + callback toggled(bool); + width: 40px; + height: 22px; + border-radius: 11px; + background: root.checked ? Theme.rog-red : Theme.toggle-off; + Rectangle { + x: root.checked ? 21px : 3px; + y: 3px; + width: 16px; + height: 16px; + border-radius: 8px; + background: #FFFFFF; + drop-shadow-blur: 3px; + drop-shadow-color: #00000040; + } + + ta := TouchArea { + width: 100%; + height: 100%; + mouse-cursor: pointer; + clicked => { root.toggled(!root.checked); } + } + + accessible-role: switch; + accessible-checked: root.checked; +} + +// — Row of selectable pills (tabs / EPP / AC-battery targets) — +// NOTE (slint 1.13.1): no explicit `height` on this component — a layout cell +// that contains a nested layout AND has a fixed height makes every later +// sibling render empty. Let the inner leaf Rectangles define the height. +component ChoicePills inherits Rectangle { + in property <[string]> choices: []; + in property current: -1; + callback chosen(int); + HorizontalLayout { + spacing: 8px; + for c[i] in root.choices: Rectangle { + horizontal-stretch: 1; + height: 30px; + border-radius: Theme.radius-item; + background: i == root.current ? Theme.mode-selected : Theme.bg-elevated; + border-width: 1px; + border-color: i == root.current ? Theme.rog-red : transparent; + Text { + text: c; + color: i == root.current ? Theme.text-primary : Theme.text-secondary; + font-size: Theme.font-sm; + horizontal-alignment: center; + vertical-alignment: center; + overflow: elide; + } + + ta := TouchArea { + width: 100%; + height: 100%; + mouse-cursor: pointer; + clicked => { root.chosen(i); } + } + + accessible-role: button; + accessible-label: c; + accessible-action-default => { root.chosen(i); } + } + } +} + +// — One power-limit row: slider + value + reset chip — +component LimitRow inherits Rectangle { + in property label; + in property minimum; + in property maximum; + in property value; + in property unit; + in property enabled: true; + callback changed(int); + callback reset(); + HorizontalLayout { + spacing: 10px; + RogSlider { + horizontal-stretch: 1; + label: root.label; + minimum: root.minimum; + maximum: Math.max(root.minimum + 1.0, root.maximum); + value: root.value; + unit: root.unit; + enabled: root.enabled; + released(v) => { root.changed(Math.round(v)); } + } + + reset-chip := Rectangle { + width: 52px; + height: 30px; + border-radius: Theme.radius-item; + background: ta-reset.has-hover ? Theme.bg-elevated : transparent; + border-width: 1px; + border-color: Theme.line; + Text { + text: @tr("Reset"); + color: Theme.text-secondary; + font-size: Theme.font-xs; + horizontal-alignment: center; + vertical-alignment: center; + } + + ta-reset := TouchArea { + width: 100%; + height: 100%; + mouse-cursor: pointer; + clicked => { root.reset(); } + } + + accessible-role: button; + accessible-label: @tr("Reset"); + accessible-action-default => { root.reset(); } + } + } +} + +export component PerProfileSettingsOverlay inherits Rectangle { + in property open: false; + callback close(); + // Tab index 0..2 -> Quiet, Balanced, Performance + in-out property selected-profile: 0; + property <[int]> profile-enums: [2, 0, 1]; + property <[string]> profile-labels: [@tr("Quiet"), @tr("Balanced"), @tr("Performance")]; + property current-env: root.profile-enums[root.selected-profile]; + property active-limits: + root.current-env == 2 ? PowerData.limits_quiet : + (root.current-env == 0 ? PowerData.limits_balanced : PowerData.limits_performance); + property active-limits-enabled: + root.current-env == 2 ? PowerData.limits_enabled_quiet : + (root.current-env == 0 ? PowerData.limits_enabled_balanced : PowerData.limits_enabled_performance); + property active-epp: + root.current-env == 2 ? PowerData.profile_quiet_epp : + (root.current-env == 0 ? PowerData.profile_balanced_epp : PowerData.profile_performance_epp); + + background: transparent; + visible: root.open; + opacity: root.open ? 1.0 : 0.0; + animate opacity { duration: 160ms; } + + // Dimmed backdrop — tap to close + Rectangle { + width: 100%; + height: 100%; + background: Theme.dark ? #000000 : #000000; + opacity: 0.55; + ta-backdrop := TouchArea { + width: 100%; + height: 100%; + clicked => { root.close(); } + } + } + + // Panel + panel := Rectangle { + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + width: Math.min(880px, parent.width - 64px); + height: Math.min(700px, parent.height - 48px); + background: Theme.bg-card; + border-radius: Theme.radius-card; + border-width: 1px; + border-color: Theme.line; + drop-shadow-blur: 24px; + drop-shadow-color: #00000080; + + VerticalLayout { + padding: 20px; + spacing: 12px; + + // Header + Rectangle { + height: 34px; + HorizontalLayout { + spacing: 8px; + RogSlashMark { } + + Text { + text: @tr("Power profiles"); + color: Theme.text-primary; + font-size: Theme.font-lg; + font-weight: 700; + vertical-alignment: center; + } + + Rectangle { + horizontal-stretch: 1; + } + + close-box := Rectangle { + width: 28px; + height: 28px; + border-radius: 14px; + background: ta-close.has-hover ? Theme.bg-elevated : transparent; + Text { + text: "✕"; + color: Theme.text-secondary; + font-size: 14px; + horizontal-alignment: center; + vertical-alignment: center; + } + + ta-close := TouchArea { + width: 100%; + height: 100%; + mouse-cursor: pointer; + clicked => { root.close(); } + } + + accessible-role: button; + accessible-label: @tr("Close"); + accessible-action-default => { root.close(); } + } + } + } + + // Profile tabs + ChoicePills { + choices: root.profile-labels; + current: root.selected-profile; + chosen(i) => { root.selected-profile = i; } + } + + // EPP for the selected profile + // (no fixed height on this wrapper either — see the note on ChoicePills) + Rectangle { + VerticalLayout { + spacing: 6px; + Text { + text: @tr("Energy Performance Preference"); + color: Theme.text-secondary; + font-size: Theme.font-sm; + } + + ChoicePills { + choices: PowerData.energy_performance_choices; + current: root.active-epp; + chosen(i) => { + if (root.current-env == 2) { PowerData.profile_quiet_epp = i; } + if (root.current-env == 0) { PowerData.profile_balanced_epp = i; } + if (root.current-env == 1) { PowerData.profile_performance_epp = i; } + PowerData.cb_set_epp(root.current-env, i); + } + } + } + } + + // Throttle policy on AC / battery (global, not per profile) + Rectangle { + VerticalLayout { + spacing: 6px; + Text { + text: @tr("Throttle policy"); + color: Theme.text-secondary; + font-size: Theme.font-sm; + } + + HorizontalLayout { + Text { + text: @tr("On AC use"); + color: Theme.text-secondary; + font-size: Theme.font-sm; + vertical-alignment: center; + } + + Rectangle { + horizontal-stretch: 1; + } + + Text { + text: @tr("Change on AC"); + color: Theme.text-secondary; + font-size: Theme.font-sm; + vertical-alignment: center; + } + + RogToggle { + checked: PowerData.change_platform_profile_on_ac; + toggled(v) => { PowerData.cb_change_platform_profile_on_ac(v); } + } + } + + ChoicePills { + choices: PowerData.platform_profile_choices; + current: PowerData.platform_profile_on_ac; + chosen(i) => { + PowerData.platform_profile_on_ac = i; + PowerData.cb_platform_profile_on_ac(i); + } + } + + HorizontalLayout { + Text { + text: @tr("On battery use"); + color: Theme.text-secondary; + font-size: Theme.font-sm; + vertical-alignment: center; + } + + Rectangle { + horizontal-stretch: 1; + } + + Text { + text: @tr("Change on battery"); + color: Theme.text-secondary; + font-size: Theme.font-sm; + vertical-alignment: center; + } + + RogToggle { + checked: PowerData.change_platform_profile_on_battery; + toggled(v) => { PowerData.cb_change_platform_profile_on_battery(v); } + } + } + + ChoicePills { + choices: PowerData.platform_profile_choices; + current: PowerData.platform_profile_on_battery; + chosen(i) => { + PowerData.platform_profile_on_battery = i; + PowerData.cb_platform_profile_on_battery(i); + } + } + } + } + + // Per-profile enable gate + HorizontalLayout { + Text { + text: @tr("Apply these limits when this profile is active"); + color: Theme.text-secondary; + font-size: Theme.font-sm; + vertical-alignment: center; + } + + Rectangle { + horizontal-stretch: 1; + } + + RogToggle { + checked: root.active-limits-enabled; + toggled(v) => { + if (root.current-env == 2) { PowerData.limits_enabled_quiet = v; } + if (root.current-env == 0) { PowerData.limits_enabled_balanced = v; } + if (root.current-env == 1) { PowerData.limits_enabled_performance = v; } + PowerData.cb_enable_profile_limits(root.current-env, v); + } + } + } + + // Limits — scrolls if the window is short + ScrollView { + vertical-stretch: 1; + VerticalLayout { + spacing: 8px; + Text { + text: @tr("CPU Power Limits"); + color: Theme.text-primary; + font-size: Theme.font-md; + font-weight: 600; + } + + LimitRow { + label: @tr("CPU Sustained Power Limit"); + minimum: root.active-limits.ppt_pl1_spl.min; + maximum: root.active-limits.ppt_pl1_spl.max; + value: root.active-limits.ppt_pl1_spl.current; + unit: "W"; + enabled: root.active-limits-enabled; + changed(v) => { + if (root.current-env == 2) { PowerData.limits_quiet.ppt_pl1_spl.current = v; } + if (root.current-env == 0) { PowerData.limits_balanced.ppt_pl1_spl.current = v; } + if (root.current-env == 1) { PowerData.limits_performance.ppt_pl1_spl.current = v; } + PowerData.cb_set_ppt_pl1_spl(root.current-env, v); + } + reset() => { PowerData.cb_reset_ppt_pl1_spl(root.current-env); } + } + + LimitRow { + label: @tr("CPU Turbo Power Limit"); + minimum: root.active-limits.ppt_pl2_sppt.min; + maximum: root.active-limits.ppt_pl2_sppt.max; + value: root.active-limits.ppt_pl2_sppt.current; + unit: "W"; + enabled: root.active-limits-enabled; + changed(v) => { + if (root.current-env == 2) { PowerData.limits_quiet.ppt_pl2_sppt.current = v; } + if (root.current-env == 0) { PowerData.limits_balanced.ppt_pl2_sppt.current = v; } + if (root.current-env == 1) { PowerData.limits_performance.ppt_pl2_sppt.current = v; } + PowerData.cb_set_ppt_pl2_sppt(root.current-env, v); + } + reset() => { PowerData.cb_reset_ppt_pl2_sppt(root.current-env); } + } + + LimitRow { + label: @tr("CPU Fast Burst Power Limit"); + minimum: root.active-limits.ppt_pl3_fppt.min; + maximum: root.active-limits.ppt_pl3_fppt.max; + value: root.active-limits.ppt_pl3_fppt.current; + unit: "W"; + enabled: root.active-limits-enabled; + changed(v) => { + if (root.current-env == 2) { PowerData.limits_quiet.ppt_pl3_fppt.current = v; } + if (root.current-env == 0) { PowerData.limits_balanced.ppt_pl3_fppt.current = v; } + if (root.current-env == 1) { PowerData.limits_performance.ppt_pl3_fppt.current = v; } + PowerData.cb_set_ppt_pl3_fppt(root.current-env, v); + } + reset() => { PowerData.cb_reset_ppt_pl3_fppt(root.current-env); } + } + + LimitRow { + label: @tr("Package Fast Power Limit"); + minimum: root.active-limits.ppt_fppt.min; + maximum: root.active-limits.ppt_fppt.max; + value: root.active-limits.ppt_fppt.current; + unit: "W"; + enabled: root.active-limits-enabled; + changed(v) => { + if (root.current-env == 2) { PowerData.limits_quiet.ppt_fppt.current = v; } + if (root.current-env == 0) { PowerData.limits_balanced.ppt_fppt.current = v; } + if (root.current-env == 1) { PowerData.limits_performance.ppt_fppt.current = v; } + PowerData.cb_set_ppt_fppt(root.current-env, v); + } + reset() => { PowerData.cb_reset_ppt_fppt(root.current-env); } + } + + LimitRow { + label: @tr("APU Sustained Power Limit"); + minimum: root.active-limits.ppt_apu_sppt.min; + maximum: root.active-limits.ppt_apu_sppt.max; + value: root.active-limits.ppt_apu_sppt.current; + unit: "W"; + enabled: root.active-limits-enabled; + changed(v) => { + if (root.current-env == 2) { PowerData.limits_quiet.ppt_apu_sppt.current = v; } + if (root.current-env == 0) { PowerData.limits_balanced.ppt_apu_sppt.current = v; } + if (root.current-env == 1) { PowerData.limits_performance.ppt_apu_sppt.current = v; } + PowerData.cb_set_ppt_apu_sppt(root.current-env, v); + } + reset() => { PowerData.cb_reset_ppt_apu_sppt(root.current-env); } + } + + LimitRow { + label: @tr("Platform Sustained Power Limit"); + minimum: root.active-limits.ppt_platform_sppt.min; + maximum: root.active-limits.ppt_platform_sppt.max; + value: root.active-limits.ppt_platform_sppt.current; + unit: "W"; + enabled: root.active-limits-enabled; + changed(v) => { + if (root.current-env == 2) { PowerData.limits_quiet.ppt_platform_sppt.current = v; } + if (root.current-env == 0) { PowerData.limits_balanced.ppt_platform_sppt.current = v; } + if (root.current-env == 1) { PowerData.limits_performance.ppt_platform_sppt.current = v; } + PowerData.cb_set_ppt_platform_sppt(root.current-env, v); + } + reset() => { PowerData.cb_reset_ppt_platform_sppt(root.current-env); } + } + + Text { + text: @tr("GPU (NVIDIA)"); + color: Theme.text-primary; + font-size: Theme.font-md; + font-weight: 600; + } + + LimitRow { + label: @tr("GPU TGP"); + minimum: root.active-limits.nv_tgp.min; + maximum: root.active-limits.nv_tgp.max; + value: root.active-limits.nv_tgp.current; + unit: "W"; + enabled: true; + changed(v) => { + if (root.current-env == 2) { PowerData.limits_quiet.nv_tgp.current = v; } + if (root.current-env == 0) { PowerData.limits_balanced.nv_tgp.current = v; } + if (root.current-env == 1) { PowerData.limits_performance.nv_tgp.current = v; } + PowerData.cb_set_nv_tgp(root.current-env, v); + } + reset() => { PowerData.cb_reset_nv_tgp(root.current-env); } + } + + LimitRow { + label: @tr("GPU Dynamic Boost"); + minimum: root.active-limits.nv_dynamic_boost.min; + maximum: root.active-limits.nv_dynamic_boost.max; + value: root.active-limits.nv_dynamic_boost.current; + unit: "W"; + enabled: true; + changed(v) => { + if (root.current-env == 2) { PowerData.limits_quiet.nv_dynamic_boost.current = v; } + if (root.current-env == 0) { PowerData.limits_balanced.nv_dynamic_boost.current = v; } + if (root.current-env == 1) { PowerData.limits_performance.nv_dynamic_boost.current = v; } + PowerData.cb_set_nv_dynamic_boost(root.current-env, v); + } + reset() => { PowerData.cb_reset_nv_dynamic_boost(root.current-env); } + } + + LimitRow { + label: @tr("GPU Temperature Limit"); + minimum: root.active-limits.nv_temp_target.min; + maximum: root.active-limits.nv_temp_target.max; + value: root.active-limits.nv_temp_target.current; + unit: "°C"; + enabled: true; + changed(v) => { + if (root.current-env == 2) { PowerData.limits_quiet.nv_temp_target.current = v; } + if (root.current-env == 0) { PowerData.limits_balanced.nv_temp_target.current = v; } + if (root.current-env == 1) { PowerData.limits_performance.nv_temp_target.current = v; } + PowerData.cb_set_nv_temp_target(root.current-env, v); + } + reset() => { PowerData.cb_reset_nv_temp_target(root.current-env); } + } + } + } + } + } +} diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint deleted file mode 100644 index e8a1757f8..000000000 --- a/rog-control-center/ui/pages/system.slint +++ /dev/null @@ -1,175 +0,0 @@ -// Device/environment data shared with the Home dashboard. -// Data-only: every value is fed from the backend via set_ui_callbacks. - -export struct AttrMinMax { - min: int, - max: int, - current: float, -} - -export global SystemPageData { - in-out property charge_control_enabled: true; - in-out property charge_control_end_threshold: 30; - callback cb_charge_control_end_threshold(/* charge limit */ int); - in-out property platform_profile: 0; - in-out property <[string]> platform_profile_choices: [@tr("Balanced"), @tr("Performance"), @tr("Quiet"), @tr("LowPower")]; - // The dropdown list index is used to index in to this and find the correct - // value for platform profile - in-out property <[int]> platform_profile_indexes: [0, 1, 2, 3]; - callback cb_platform_profile(int); - in-out property <[string]> energy_performance_choices: [ - @tr("Default"), - @tr("Performance"), - @tr("BalancePerformance"), - @tr("BalancePower"), - @tr("Power") - ]; - in-out property profile_balanced_epp: 0; - callback cb_profile_balanced_epp(int); - in-out property profile_performance_epp: 0; - callback cb_profile_performance_epp(int); - in-out property profile_quiet_epp: 0; - callback cb_profile_quiet_epp(int); - // if the EPP should change with throttle - in-out property platform_profile_linked_epp: true; - callback cb_platform_profile_linked_epp(bool); - in-out property platform_profile_on_ac: 0; - callback cb_platform_profile_on_ac(int); - in-out property change_platform_profile_on_ac: true; - callback cb_change_platform_profile_on_ac(bool); - in-out property platform_profile_on_battery: 0; - callback cb_platform_profile_on_battery(int); - in-out property change_platform_profile_on_battery: true; - callback cb_change_platform_profile_on_battery(bool); - // - in-out property panel_overdrive; - callback cb_panel_overdrive(int); - in-out property boot_sound; - callback cb_boot_sound(int); - in-out property screen_auto_brightness; - callback cb_screen_auto_brightness(int); - in-out property mcu_powersave; - callback cb_mcu_powersave(int); - in-out property mini_led_mode; - callback cb_mini_led_mode(int); - // Local-dimming modes (kernel ABI). setup_system.rs picks the 2- or - // 3-state set per device; the index is the value written over dbus. - in-out property <[string]> mini_led_mode_choices: [@tr("Off"), @tr("Multi-zone"), @tr("Multi-zone Strong")]; - in-out property <[string]> mini_led_choices_onoff: [@tr("Off"), @tr("Multi-zone")]; - in-out property <[string]> mini_led_choices_modes: [@tr("Off"), @tr("Multi-zone"), @tr("Multi-zone Strong")]; - - in-out property screenpad_gamma; - callback cb_screenpad_gamma(float); - // percentage - in-out property screenpad_brightness: 50; - callback cb_screenpad_brightness(int); - in-out property screenpad_sync_with_primary: false; - callback cb_screenpad_sync_with_primary(bool); - - in-out property asus_armoury_loaded: false; - - in-out property ppt_pl1_spl: { - min: 0, - max: 100, - current: 20, - }; - callback cb_ppt_pl1_spl(int); - callback cb_default_ppt_pl1_spl(); - - in-out property ppt_pl2_sppt: { - min: 0, - max: 100, - current: 20, - }; - callback cb_ppt_pl2_sppt(int); - callback cb_default_ppt_pl2_sppt(); - - in-out property ppt_pl3_fppt: { - min: 0, - max: 100, - current: 20, - }; - callback cb_ppt_pl3_fppt(int); - callback cb_default_ppt_pl3_fppt(); - - in-out property ppt_fppt: { - min: 0, - max: 100, - current: 20, - }; - callback cb_ppt_fppt(int); - callback cb_default_ppt_fppt(); - - in-out property ppt_apu_sppt: { - min: 0, - max: 100, - current: 20, - }; - callback cb_ppt_apu_sppt(int); - callback cb_default_ppt_apu_sppt(); - - in-out property ppt_platform_sppt: { - min: 0, - max: 100, - current: 20, - }; - callback cb_ppt_platform_sppt(int); - callback cb_default_ppt_platform_sppt(); - - in-out property nv_dynamic_boost: { - min: 0, - max: 30, - current: 5, - }; - callback cb_nv_dynamic_boost(int); - callback cb_default_nv_dynamic_boost(); - - in-out property nv_tgp: { - min: 0, - max: 175, - current: 80, - }; - callback cb_nv_tgp(int); - callback cb_default_nv_tgp(); - - in-out property nv_temp_target: { - min: 0, - max: 80, - current: 75, - }; - callback cb_nv_temp_target(int); - callback cb_default_nv_temp_target(); - - in-out property enable_ppt_group: false; - callback cb_enable_ppt_group(bool); - - in-out property ppt_enabled_available; - in-out property ppt_enabled; - callback cb_ppt_enabled(bool); - in-out property battery_health: -1; - - in-out property battery_power_consumption: -1.0; - in-out property battery_status: "Unknown"; - in-out property battery_time_estimate: ""; - in-out property cpu_temp_val: -1.0; - in-out property gpu_temp_val: -1.0; - in-out property cpu_usage_val: -1.0; - in-out property gpu_usage_val: -1.0; - in-out property ram_usage_val: -1.0; - in-out property cpu_freq_mhz: -1.0; - in-out property gpu_freq_mhz: -1.0; - in-out property cpu_fan_rpm: -1; - in-out property gpu_fan_rpm: -1; - in-out property mid_fan_rpm: -1; - in-out property has_dgpu: true; - in-out property cpu_name: ""; - in-out property igpu_name: ""; - in-out property dgpu_name: ""; - in-out property igpu_temp_val: -1.0; - in-out property igpu_usage_val: -1.0; - in-out property has_igpu: false; - in-out property dgpu_suspended: false; - - // Real product name from DMI (read in setup_system.rs). - in-out property product_name: ""; -} diff --git a/rog-control-center/ui/types.slint b/rog-control-center/ui/types.slint new file mode 100644 index 000000000..9774a0fb4 --- /dev/null +++ b/rog-control-center/ui/types.slint @@ -0,0 +1,10 @@ +export struct AttrMinMax { + min: int, + max: int, + current: float, + supported: bool, +} +export struct AttrBool { + current: bool, + supported: bool, +} diff --git a/rog-control-center/ui/widgets/cards.slint b/rog-control-center/ui/widgets/cards.slint new file mode 100644 index 000000000..617a01835 --- /dev/null +++ b/rog-control-center/ui/widgets/cards.slint @@ -0,0 +1,242 @@ +// UI Cards +// Contains large visual components like the product info card and effect buttons. +import { Theme } from "../globals.slint"; + +// — Product card: product image/placeholder + model (25px 900) + spec lines — +export component RogProductCard inherits Rectangle { + in property product-image; + in property name; + in property <[string]> details; + background: Theme.bg-card; + border-radius: 4px; + drop-shadow-blur: 18px; + drop-shadow-color: #00000073; + drop-shadow-offset-y: 4px; + clip: true; + VerticalLayout { + // Image area: stretches (grows with card height to keep the square ratio) + Rectangle { + vertical-stretch: 1; + min-height: 160px; + // Hero panel: dark purple-red in dark mode, soft lavender in light. + background: Theme.dark ? #15101e : #ECEAF2; + + if root.product-image.width > 0: Image { + source: root.product-image; + width: 75%; + height: 85%; + horizontal-alignment: center; + vertical-alignment: center; + image-fit: contain; + } + if root.product-image.width == 0: Image { + source: Theme.dark ? @image-url("../images/laptop-placeholder.svg") + : @image-url("../images/laptop-placeholder-light.svg"); + width: 94%; + height: 94%; + horizontal-alignment: center; + vertical-alignment: center; + image-fit: contain; + } + } + // Text info area + VerticalLayout { + padding-top: 16px; + padding-bottom: 18px; + padding-left: 20px; + padding-right: 20px; + spacing: 4px; + Text { + text: root.name; + color: Theme.text-primary; + font-size: 22px; + font-weight: 900; + letter-spacing: 0.5px; + horizontal-alignment: center; + // Long model names wrap instead of being clipped at the edge. + wrap: word-wrap; + } + + for d in root.details: Text { + text: d; + color: Theme.text-secondary; + font-size: 11.5px; + horizontal-alignment: center; + wrap: word-wrap; + } + } + } +} + + +// —— Display mode card for GPU page (icon + title + description) —— +export component RogDisplayModeCard inherits Rectangle { + in property icon; + in property title; + in property description; + in property selected: false; + in property enabled: true; + callback clicked(); + background: Theme.bg-card; + border-radius: 4px; + border-width: 1px; + border-color: root.selected ? Theme.rog-red : (root.enabled ? transparent : Theme.line); + animate border-color { duration: 150ms; } + clip: true; + + VerticalLayout { + padding: 20px; + spacing: 12px; + alignment: center; + + Image { + source: root.icon; + colorize: root.selected ? Theme.rog-red : (root.enabled ? Theme.text-dim : Theme.fg-disabled); + // Full-width box with contain: the SVG scales to 56px tall and + // centers horizontally, regardless of card width. + height: 56px; + image-fit: contain; + opacity: root.enabled ? 1.0 : 0.4; + } + + Text { + text: root.title; + color: root.selected ? Theme.text-primary : (root.enabled ? Theme.text-secondary : Theme.fg-disabled); + font-size: 15px; + font-weight: 600; + horizontal-alignment: center; + } + + Text { + text: root.description; + color: root.enabled ? Theme.text-dim : Theme.fg-disabled; + font-size: 11px; + horizontal-alignment: center; + wrap: word-wrap; + opacity: root.enabled ? 1.0 : 0.5; + } + } + + ta := TouchArea { + enabled: root.enabled; + clicked => { root.clicked(); } + } + + accessible-role: button; + accessible-label: root.title; + accessible-action-default => { + if (root.enabled) { root.clicked(); } + } + FocusScope { + enabled: root.enabled; + key-pressed(event) => { + if (event.text == Key.Return || event.text == Key.Space) { + root.clicked(); + return accept; + } + reject + } + if self.has-focus: Rectangle { + border-radius: 4px; + border-width: 2px; + border-color: Theme.rog-red; + background: transparent; + } + } +} + + + +// —— Effect icon button for lighting page (large circular icon, radio-style single-select) —— +// IMPORTANT: parent manages 'selected' state — this component only signals clicked() +export component RogEffectButton inherits Rectangle { + in property icon; + in property label; + in property selected: false; // read-only from parent, do NOT self-assign + in property enabled: true; + callback clicked(); + width: 92px; + height: 104px; + opacity: root.enabled ? 1.0 : 0.35; + + // Circle background, horizontally centred in the cell (absolute x, not a + // layout child, so a fixed-width circle never drifts to the left). + outer := Rectangle { + x: (parent.width - self.width) / 2; + y: 4px; + width: 72px; + height: 72px; + border-radius: 36px; + background: root.selected ? Theme.mode-selected : Theme.bg-app; + border-width: root.selected ? 2px : 1px; + border-color: root.selected ? Theme.rog-red : Theme.line-soft; + + Image { + source: root.icon; + colorize: root.selected ? Theme.rog-red : Theme.text-secondary; + width: 32px; + height: 32px; + image-fit: contain; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Label spans the full cell width so its centre lines up with the circle. + Text { + text: root.label; + color: root.selected ? Theme.text-primary : Theme.text-secondary; + font-size: 12px; + font-weight: root.selected ? 600 : 400; + x: 0; + y: 82px; + width: 100%; + horizontal-alignment: center; + overflow: elide; + } + + ta := TouchArea { + width: 100%; + height: 100%; + enabled: root.enabled; + clicked => { root.clicked(); } + } + + accessible-role: button; + accessible-label: root.label; + accessible-action-default => { + if (root.enabled) { root.clicked(); } + } + FocusScope { + enabled: root.enabled; + key-pressed(event) => { + if (event.text == Key.Return || event.text == Key.Space) { + root.clicked(); + return accept; + } + reject + } + if self.has-focus: Rectangle { + x: 2px; + y: 2px; + width: parent.width - 4px; + height: parent.height - 4px; + border-radius: 4px; + border-width: 2px; + border-color: Theme.rog-red; + background: transparent; + } + } +} + + +// — Card: dark fill + radius 8 + deep shadow, no bright border (matches AC cards) — +export component RogCard inherits Rectangle { + in property radius: Theme.radius-card; + background: Theme.bg-card; + border-radius: root.radius; + drop-shadow-blur: 18px; + drop-shadow-color: #00000070; + drop-shadow-offset-y: 4px; + clip: true; +} diff --git a/rog-control-center/ui/widgets/rog.slint b/rog-control-center/ui/widgets/controls.slint similarity index 63% rename from rog-control-center/ui/widgets/rog.slint rename to rog-control-center/ui/widgets/controls.slint index d784516d0..4840940b0 100644 --- a/rog-control-center/ui/widgets/rog.slint +++ b/rog-control-center/ui/widgets/controls.slint @@ -1,63 +1,8 @@ -// rog-control-center — AC-style component library (redone against 2.5K AC footage) -// RogCard=AC card, RogMetricCard=dashboard quad (icon + big number + red progress), -// RogSlider=hand-drawn AC slider (grey track + red fill + white/red handle), RogToggle=hand-drawn ON-red/OFF-grey, -// RogModeSelector=performance segment, RogDropdownRow=dropdown, RogSectionHeader=heading +// Interactive Form Controls +// Contains inputs, sliders, toggles, selectors, and tab bars used across the UI. import { Theme } from "../globals.slint"; import { ComboBox, Button } from "std-widgets.slint"; - -// — Page root: declares the opacity transition. The actual opacity value is -// driven by main_window per active page (crossfade); this only provides the curve. — -export component RogPage inherits Rectangle { - animate opacity { duration: 180ms; easing: ease-out; } -} - -// — "Reboot required" notice banner — -// Shown inline next to controls whose changes only apply after a restart -// (GPU mode, MUX, reserved memory, miniLED...). Parent toggles `visible`. -export component RogRebootBanner inherits Rectangle { - height: 40px; - border-radius: 8px; - background: #66550033; - border-width: 1px; - border-color: #E0B04033; - animate opacity { duration: 200ms; easing: ease-out; } - HorizontalLayout { - padding-left: 14px; - padding-right: 14px; - spacing: 10px; - // Full-height cell so the icon can be centred inside it: Slint - // layouts own direct-child geometry, so an Image child can't set y. - Rectangle { - height: 100%; - Image { - source: @image-url("../images/nav-about.svg"); - colorize: #E0B040; - width: 18px; - height: 18px; - x: (parent.width - self.width) / 2; - y: (parent.height - self.height) / 2; - } - } - Text { - text: @tr("Changes are staged. They will take effect after restarting."); - color: Theme.text-secondary; - font-size: Theme.font-sm; - vertical-alignment: center; - overflow: elide; - } - } -} - -// — Card: dark fill + radius 8 + deep shadow, no bright border (matches AC cards) — -export component RogCard inherits Rectangle { - in property radius: Theme.radius-card; - background: Theme.bg-card; - border-radius: root.radius; - drop-shadow-blur: 18px; - drop-shadow-color: #00000070; - drop-shadow-offset-y: 4px; - clip: true; -} +import { RogCard } from "cards.slint"; @@ -74,7 +19,8 @@ export component RogModeSelector inherits Rectangle { accessible-label: root.modes[root.current]; fs := FocusScope { - width: 100%; height: 100%; + width: 100%; + height: 100%; key-pressed(event) => { if (event.text == Key.Return || event.text == Key.Space) { root.current = root.focused-index; @@ -115,6 +61,7 @@ export component RogModeSelector inherits Rectangle { root.selected(i); } } + if fs.has-focus && i == root.focused-index: Rectangle { border-radius: Theme.radius-item; border-width: 2px; @@ -133,15 +80,25 @@ export component RogModeSelector inherits Rectangle { } } + // — Performance mode grid (large icons + labels) — export component RogModeGrid inherits Rectangle { in property <[int]> profile-indexes; - in property <[string]> profile-choices; in property current-profile; callback selected(int); + // Opens the per-profile power settings overlay (Home dashboard). + callback advanced(); + + function profile-label(ev: int) -> string { + if ev == 0 { return @tr("Balanced"); } + if ev == 1 { return @tr("Performance"); } + if ev == 2 { return @tr("Quiet"); } + if ev == 3 { return @tr("LowPower"); } + if ev == 4 { return @tr("Custom"); } + return ""; + } height: 104px; - width: 100%; HorizontalLayout { spacing: 0px; @@ -150,11 +107,12 @@ export component RogModeGrid inherits Rectangle { height: 100%; accessible-role: button; - accessible-label: root.profile-choices[p]; + accessible-label: root.profile-label(ev); accessible-action-default => { root.selected(ev); } if root.current-profile == ev: Rectangle { - width: 100%; height: 100%; + width: 100%; + height: 100%; background: Theme.mode-selected; border-radius: 4px; drop-shadow-blur: 16px; @@ -166,11 +124,11 @@ export component RogModeGrid inherits Rectangle { spacing: 10px; Image { source: ev == 0 ? @image-url("../images/mode-performance.svg") : - ev == 1 ? @image-url("../images/mode-turbo.svg") : - ev == 2 ? @image-url("../images/mode-silent.svg") : - ev == 3 ? @image-url("../images/mode-silent.svg") : - ev == 4 ? @image-url("../images/mode-manual.svg") : - @image-url("../images/mode-performance.svg"); + ev == 1 ? @image-url("../images/mode-turbo.svg") : + ev == 2 ? @image-url("../images/mode-silent.svg") : + ev == 3 ? @image-url("../images/mode-silent.svg") : + ev == 4 ? @image-url("../images/mode-manual.svg") : + @image-url("../images/mode-performance.svg"); colorize: root.current-profile == ev ? Theme.text-primary : Theme.text-secondary; height: 32px; image-fit: contain; @@ -178,7 +136,7 @@ export component RogModeGrid inherits Rectangle { } Text { - text: root.profile-choices[p]; + text: root.profile-label(ev); color: root.current-profile == ev ? Theme.text-primary : Theme.text-secondary; font-size: 14px; horizontal-alignment: center; @@ -187,7 +145,8 @@ export component RogModeGrid inherits Rectangle { if root.current-profile == ev: Rectangle { y: parent.height - 6px; - width: 40px; height: 3px; + width: 40px; + height: 3px; x: (parent.width - 40px) / 2; background: Theme.rog-red; drop-shadow-blur: 10px; @@ -195,17 +154,77 @@ export component RogModeGrid inherits Rectangle { drop-shadow-color: Theme.rog-red-glow; } + FocusScope { + enabled: true; + key-pressed(event) => { + if (event.text == Key.Return || event.text == Key.Space) { + root.selected(ev); + return accept; + } + reject + } + if self.has-focus: Rectangle { + width: parent.width; + height: parent.height; + border-radius: 4px; + border-width: 2px; + border-color: Theme.rog-red; + background: transparent; + } + } + + // TouchArea declared AFTER the FocusScope so it is the topmost hit + // target under the pointer. Otherwise the focus scope eats the first + // click (focus, then activate) and every switch needs two clicks — + // same layout rule as the sidebar's working NavItem. ta := TouchArea { - width: 100%; height: 100%; + width: 100%; + height: 100%; mouse-cursor: pointer; clicked => { root.selected(ev); } } + } + + // Trailing "Advanced" tile: opens the per-profile power settings overlay. + Rectangle { + horizontal-stretch: 1; + height: 100%; + accessible-role: button; + accessible-label: @tr("Advanced"); + accessible-action-default => { root.advanced(); } + + Rectangle { + width: 100%; + height: 100%; + background: ta-adv.has-hover ? Theme.bg-elevated : transparent; + border-radius: 4px; + opacity: 0.6; + } + + VerticalLayout { + alignment: center; + spacing: 10px; + Image { + source: @image-url("../images/mode-manual.svg"); + colorize: Theme.text-secondary; + height: 32px; + image-fit: contain; + horizontal-alignment: center; + } + + Text { + text: @tr("Advanced"); + color: Theme.text-secondary; + font-size: 14px; + horizontal-alignment: center; + } + } FocusScope { enabled: true; key-pressed(event) => { if (event.text == Key.Return || event.text == Key.Space) { - root.selected(ev); + root.advanced(); return accept; } reject @@ -219,10 +238,19 @@ export component RogModeGrid inherits Rectangle { background: transparent; } } + + // TouchArea last (topmost) — see the note on the profile tiles above. + ta-adv := TouchArea { + width: 100%; + height: 100%; + mouse-cursor: pointer; + clicked => { root.advanced(); } + } } } } + // — Hand-drawn AC slider (grey track + red fill + white round red-bordered handle) — export component RogSlider inherits Rectangle { in property label; @@ -255,7 +283,11 @@ export component RogSlider inherits Rectangle { font-size: Theme.font-sm; vertical-alignment: center; } - Rectangle { horizontal-stretch: 1; } + + Rectangle { + horizontal-stretch: 1; + } + if root.available: Text { text: "\{Math.round(root.value)}" + (root.unit == "" ? "" : " " + root.unit); color: Theme.rog-red; @@ -270,10 +302,12 @@ export component RogSlider inherits Rectangle { font-weight: 700; vertical-alignment: center; } - if root.controls-width > 0px: Rectangle { width: root.controls-width; } + if root.controls-width > 0px: Rectangle { + width: root.controls-width; + } } + track := Rectangle { - width: 100%; height: 24px; // Track + fill positioned absolutely. Do NOT wrap in a layout with // `alignment: center`: that is non-stretch mode, which centers the @@ -305,6 +339,7 @@ export component RogSlider inherits Rectangle { drop-shadow-blur: 4px; drop-shadow-color: #00000040; } + ta := TouchArea { width: 100%; height: 100%; @@ -319,6 +354,7 @@ export component RogSlider inherits Rectangle { } } } + accessible-role: slider; accessible-label: root.label; accessible-value: root.value; @@ -379,6 +415,7 @@ export component RogSlider inherits Rectangle { } } + // — Power-limit slider: AC slider with optional help popup and reset-to-default. // `available` / "N/A" handling lives in RogSlider itself. — export component RogPowerSlider inherits RogSlider { @@ -408,6 +445,7 @@ export component RogPowerSlider inherits RogSlider { color: Theme.text-primary; text: root.help-text; } + Button { text: @tr("OK"); clicked => { help-popup.close(); } @@ -437,6 +475,7 @@ export component RogPowerSlider inherits RogSlider { color: Theme.text-primary; text: @tr("confirm_reset" => "Are you sure you want to reset this?"); } + HorizontalLayout { spacing: 10px; alignment: end; @@ -444,6 +483,7 @@ export component RogPowerSlider inherits RogSlider { text: @tr("Cancel"); clicked => { reset-popup.close(); } } + Button { text: @tr("Reset"); clicked => { @@ -472,6 +512,7 @@ export component RogPowerSlider inherits RogSlider { mouse-cursor: root.enabled ? pointer : default; clicked => { reset-popup.show(); } } + accessible-role: button; accessible-label: @tr("Reset {}", root.label); accessible-enabled: root.enabled; @@ -481,7 +522,8 @@ export component RogPowerSlider inherits RogSlider { } } FocusScope { - width: 100%; height: 100%; + width: 100%; + height: 100%; enabled: root.enabled; key-pressed(e) => { if (e.text == Key.Return || e.text == Key.Space) { @@ -497,6 +539,7 @@ export component RogPowerSlider inherits RogSlider { background: transparent; } } + Text { text: "↺"; color: root.enabled ? Theme.text-secondary : Theme.fg-disabled; @@ -513,13 +556,15 @@ export component RogPowerSlider inherits RogSlider { help-ta := TouchArea { clicked => { help-popup.show(); } } + accessible-role: button; accessible-label: @tr("Help for {}", root.label); accessible-action-default => { help-popup.show(); } FocusScope { - width: 100%; height: 100%; + width: 100%; + height: 100%; enabled: true; key-pressed(e) => { if (e.text == Key.Return || e.text == Key.Space) { @@ -535,6 +580,7 @@ export component RogPowerSlider inherits RogSlider { background: transparent; } } + Text { text: "?"; color: Theme.text-secondary; @@ -547,6 +593,7 @@ export component RogPowerSlider inherits RogSlider { } } + // — Hand-drawn AC toggle (ON red / OFF grey, rounded pill) — export component RogToggle inherits Rectangle { in property label; @@ -567,7 +614,6 @@ export component RogToggle inherits Rectangle { accessible-checked: root.checked; accessible-action-default => { if (root.enabled) { - root.checked = !root.checked; root.toggled(root.checked); } } @@ -585,7 +631,10 @@ export component RogToggle inherits Rectangle { font-size: Theme.font-sm; vertical-alignment: center; } - if !root.compact: Rectangle { horizontal-stretch: 1; } + + if !root.compact: Rectangle { + horizontal-stretch: 1; + } tg := Rectangle { width: 44px; height: 24px; @@ -594,33 +643,16 @@ export component RogToggle inherits Rectangle { ta := TouchArea { enabled: root.enabled; clicked => { - root.checked = !root.checked; - root.toggled(root.checked); - } - } - FocusScope { - enabled: root.enabled; - key-pressed(event) => { - if (event.text == Key.Return || event.text == Key.Space) { - root.checked = !root.checked; - root.toggled(root.checked); - return accept; - } - reject - } - if self.has-focus: Rectangle { - x: -3px; y: -3px; - width: parent.width + 6px; - height: parent.height + 6px; - border-radius: 15px; - border-width: 2px; - border-color: Theme.rog-red; - background: transparent; + root.toggled(!root.checked); } } + Rectangle { x: root.checked ? parent.width - self.width - 3px : 3px; - animate x { duration: 120ms; easing: ease-in-out; } + animate x { + duration: 120ms; + easing: ease-in-out; + } y: 3px; width: 18px; height: 18px; @@ -632,6 +664,7 @@ export component RogToggle inherits Rectangle { } } + // — int switch (for panel_overdrive / boot_sound etc., int 0/1) — export component RogToggleInt inherits Rectangle { in property label; @@ -651,7 +684,6 @@ export component RogToggleInt inherits Rectangle { accessible-checked: root.checked-int != 0; accessible-action-default => { if (root.enabled && root.available) { - root.checked-int = root.checked-int != 0 ? 0 : 1; root.toggled(root.checked-int); } } @@ -668,7 +700,10 @@ export component RogToggleInt inherits Rectangle { font-size: Theme.font-sm; vertical-alignment: center; } - if !root.compact: Rectangle { horizontal-stretch: 1; } + + if !root.compact: Rectangle { + horizontal-stretch: 1; + } tg := Rectangle { width: 44px; height: 24px; @@ -677,30 +712,10 @@ export component RogToggleInt inherits Rectangle { ta := TouchArea { enabled: root.enabled && root.available; clicked => { - root.checked-int = root.checked-int != 0 ? 0 : 1; root.toggled(root.checked-int); } } - FocusScope { - enabled: root.enabled && root.available; - key-pressed(event) => { - if (event.text == Key.Return || event.text == Key.Space) { - root.checked-int = root.checked-int != 0 ? 0 : 1; - root.toggled(root.checked-int); - return accept; - } - reject - } - if self.has-focus: Rectangle { - x: -3px; y: -3px; - width: parent.width + 6px; - height: parent.height + 6px; - border-radius: 15px; - border-width: 2px; - border-color: Theme.rog-red; - background: transparent; - } - } + Rectangle { x: root.checked-int != 0 ? parent.width - self.width - 3px : 3px; y: 3px; @@ -714,6 +729,7 @@ export component RogToggleInt inherits Rectangle { } } + // — Dropdown row (MiniLED / EPP) — export component RogDropdownRow inherits Rectangle { in property label; @@ -734,7 +750,11 @@ export component RogDropdownRow inherits Rectangle { font-size: Theme.font-sm; vertical-alignment: center; } - Rectangle { horizontal-stretch: 1; } + + Rectangle { + horizontal-stretch: 1; + } + if root.enabled: ComboBox { min-width: root.combo-min-width; current-index <=> root.current-index; @@ -751,197 +771,6 @@ export component RogDropdownRow inherits Rectangle { } } -// ============================================================ -// AC home (dashboard) components — matched against ac-system-page.html (1:1) -// ============================================================ - -// — AC signature slash accent: compact slanted parallelograms. — -// The source is pre-skewed, so there is no rotated bounding box and the mark -// remains vertically centred relative to its title at every row height. -export component RogSlashMark inherits Rectangle { - in property bar-color: Theme.rog-red; - in property count: 3; - in property bar-w: 5px; - in property bar-h: 14px; - width: root.count * (root.bar-w + 11px) - 2px; - height: 100%; - - for i in root.count: Image { - source: @image-url("../images/slash-bar.svg"); - colorize: root.bar-color; - x: i * (root.bar-w + 11px); - y: (root.height - root.bar-h) / 2; - width: root.bar-w + 9px; - height: root.bar-h; - image-fit: fill; - } -} - -// — Dark slashes by metric headings retain their original visual weight. — -export component RogDarkSlashes inherits RogSlashMark { - bar-color: Theme.bg-elevated; - count: 3; - bar-w: 5px; - bar-h: 22px; -} - -// — Purple→red gradient meter (#6437FF→#FF1929; Slint has no native gradient, simulated) — -export component RogMeter inherits Rectangle { - in property value: 0.0; // 0..1 - in property thickness: 4px; - height: root.thickness; - background: Theme.line; - border-radius: 1px; - clip: true; - fill := Rectangle { - x: 0; - width: root.width * clamp(root.value, 0.0, 1.0); - height: 100%; - clip: true; - // Real smooth meter gradient (purple → red), clipped to the fill - // width. Replaces the old 7-band fake gradient. - Rectangle { - x: 0; - width: root.width; - height: 100%; - background: @linear-gradient(90deg, Theme.meter-start, Theme.meter-end); - } - } -} - -// — Single stat row: left label, right value (13px secondary; dim uses text-dim) — -export component RogStatRow inherits Rectangle { - in property label; - in property value; - in property dim: false; - background: transparent; - height: 20px; - HorizontalLayout { - Text { - text: root.label; - color: root.dim ? Theme.text-dim : Theme.text-secondary; - font-size: 13px; - vertical-alignment: center; - } - Rectangle { horizontal-stretch: 1; } - Text { - text: root.value; - color: root.dim ? Theme.text-dim : Theme.text-secondary; - font-size: 13px; - vertical-alignment: center; - } - } -} - -// — One reading: label+value row + purple-red meter — -export component RogReading inherits VerticalLayout { - in property label; - in property value; - in property progress: 0.0; - spacing: 3px; - HorizontalLayout { - Text { - text: root.label; - color: Theme.text-secondary; - font-size: 13px; - vertical-alignment: center; - } - Rectangle { horizontal-stretch: 1; } - Text { - text: root.value; - color: Theme.text-secondary; - font-size: 13px; - vertical-alignment: center; - } - } - RogMeter { value: root.progress; } -} - -// — Metric block: big heading + shared slash accent + free reading/stat fill. — -export component RogMetric inherits VerticalLayout { - in property title; - spacing: 7px; - alignment: start; - HorizontalLayout { - spacing: 10px; - padding-bottom: 10px; - Text { - text: root.title; - color: Theme.text-primary; - font-size: 30px; - font-weight: 400; - } - Rectangle { horizontal-stretch: 1; } - RogDarkSlashes {} - } - @children -} - -// — Product card: product image/placeholder + model (25px 900) + spec lines — -export component RogProductCard inherits Rectangle { - in property product-image; - in property name; - in property <[string]> details; - background: Theme.bg-card; - border-radius: 4px; - drop-shadow-blur: 18px; - drop-shadow-color: #00000073; - drop-shadow-offset-y: 4px; - clip: true; - VerticalLayout { - // Image area: stretches (grows with card height to keep the square ratio) - Rectangle { - vertical-stretch: 1; - min-height: 160px; - // Hero panel: dark purple-red in dark mode, soft lavender in light. - background: Theme.dark ? #15101e : #ECEAF2; - - if root.product-image.width > 0: Image { - source: root.product-image; - width: 75%; - height: 85%; - horizontal-alignment: center; - vertical-alignment: center; - image-fit: contain; - } - if root.product-image.width == 0: Image { - source: Theme.dark ? @image-url("../images/laptop-placeholder.svg") - : @image-url("../images/laptop-placeholder-light.svg"); - width: 94%; - height: 94%; - horizontal-alignment: center; - vertical-alignment: center; - image-fit: contain; - } - } - // Text info area - VerticalLayout { - padding-top: 16px; - padding-bottom: 18px; - padding-left: 20px; - padding-right: 20px; - spacing: 4px; - Text { - text: root.name; - color: Theme.text-primary; - font-family: "Inter Display"; - font-size: 22px; - font-weight: 900; - letter-spacing: 0.5px; - horizontal-alignment: center; - // Long model names wrap instead of being clipped at the edge. - wrap: word-wrap; - } - for d in root.details: Text { - text: d; - color: Theme.text-secondary; - font-size: 11.5px; - horizontal-alignment: center; - wrap: word-wrap; - } - } - } -} // ============================================================ // System Settings Page Components — AC multi-tab layout @@ -961,12 +790,13 @@ export component RogTabBar inherits Rectangle { accessible-label: root.tabs[root.active]; fs := FocusScope { - width: 100%; height: 100%; + width: 100%; + height: 100%; key-pressed(event) => { if (event.text == Key.Return || event.text == Key.Space) { if (root.focused-index < root.tabs.length && (root.focused-index >= root.tab-enabled.length - || root.tab-enabled[root.focused-index])) { + || root.tab-enabled[root.focused-index])) { root.active = root.focused-index; root.tab-changed(root.active); } @@ -1046,9 +876,12 @@ export component RogTabBar inherits Rectangle { root.tab-changed(i); } } + if fs.has-focus && i == root.focused-index: Rectangle { - x: 2px; y: 2px; - width: parent.width - 4px; height: parent.height - 4px; + x: 2px; + y: 2px; + width: parent.width - 4px; + height: parent.height - 4px; border-radius: 4px; border-width: 2px; border-color: Theme.rog-red; @@ -1063,166 +896,8 @@ export component RogTabBar inherits Rectangle { background: Theme.line-soft; } } - Rectangle { horizontal-stretch: 1; } // push tabs left - } -} - - -// —— Effect icon button for lighting page (large circular icon, radio-style single-select) —— -// IMPORTANT: parent manages 'selected' state — this component only signals clicked() -export component RogEffectButton inherits Rectangle { - in property icon; - in property label; - in property selected: false; // read-only from parent, do NOT self-assign - in property enabled: true; - callback clicked(); - width: 92px; - height: 104px; - opacity: root.enabled ? 1.0 : 0.35; - - // Circle background, horizontally centred in the cell (absolute x, not a - // layout child, so a fixed-width circle never drifts to the left). - outer := Rectangle { - x: (parent.width - self.width) / 2; - y: 4px; - width: 72px; - height: 72px; - border-radius: 36px; - background: root.selected ? Theme.mode-selected : Theme.bg-app; - border-width: root.selected ? 2px : 1px; - border-color: root.selected ? Theme.rog-red : Theme.line-soft; - - Image { - source: root.icon; - colorize: root.selected ? Theme.rog-red : Theme.text-secondary; - width: 32px; - height: 32px; - image-fit: contain; - horizontal-alignment: center; - vertical-alignment: center; - } - } - - // Label spans the full cell width so its centre lines up with the circle. - Text { - text: root.label; - color: root.selected ? Theme.text-primary : Theme.text-secondary; - font-size: 12px; - font-weight: root.selected ? 600 : 400; - x: 0; - y: 82px; - width: 100%; - horizontal-alignment: center; - overflow: elide; - } - - ta := TouchArea { - width: 100%; - height: 100%; - enabled: root.enabled; - clicked => { root.clicked(); } - } - accessible-role: button; - accessible-label: root.label; - accessible-action-default => { - if (root.enabled) { root.clicked(); } - } - FocusScope { - enabled: root.enabled; - key-pressed(event) => { - if (event.text == Key.Return || event.text == Key.Space) { - root.clicked(); - return accept; - } - reject - } - if self.has-focus: Rectangle { - x: 2px; y: 2px; - width: parent.width - 4px; height: parent.height - 4px; - border-radius: 4px; - border-width: 2px; - border-color: Theme.rog-red; - background: transparent; - } - } -} - -// —— Display mode card for GPU page (icon + title + description) —— -export component RogDisplayModeCard inherits Rectangle { - in property icon; - in property title; - in property description; - in property selected: false; - in property enabled: true; - callback clicked(); - background: Theme.bg-card; - border-radius: 4px; - border-width: 1px; - border-color: root.selected ? Theme.rog-red : (root.enabled ? transparent : Theme.line); - animate border-color { duration: 150ms; } - clip: true; - - VerticalLayout { - padding: 20px; - spacing: 12px; - alignment: center; - - Image { - source: root.icon; - colorize: root.selected ? Theme.rog-red : (root.enabled ? Theme.text-dim : Theme.fg-disabled); - // Full-width box with contain: the SVG scales to 56px tall and - // centers horizontally, regardless of card width. - width: 100%; - height: 56px; - image-fit: contain; - opacity: root.enabled ? 1.0 : 0.4; - } - - Text { - text: root.title; - color: root.selected ? Theme.text-primary : (root.enabled ? Theme.text-secondary : Theme.fg-disabled); - font-size: 15px; - font-weight: 600; - horizontal-alignment: center; - } - - Text { - text: root.description; - color: root.enabled ? Theme.text-dim : Theme.fg-disabled; - font-size: 11px; - horizontal-alignment: center; - wrap: word-wrap; - opacity: root.enabled ? 1.0 : 0.5; - } - } - - ta := TouchArea { - enabled: root.enabled; - clicked => { root.clicked(); } - } - accessible-role: button; - accessible-label: root.title; - accessible-action-default => { - if (root.enabled) { root.clicked(); } - } - FocusScope { - enabled: root.enabled; - key-pressed(event) => { - if (event.text == Key.Return || event.text == Key.Space) { - root.clicked(); - return accept; - } - reject - } - if self.has-focus: Rectangle { - border-radius: 4px; - border-width: 2px; - border-color: Theme.rog-red; - background: transparent; - } + Rectangle { + horizontal-stretch: 1; + } // push tabs left } } - -// —— Collapsible section header (title + chevron) —— -// (removed: was only used by the GPU process list, which had no backend and -// was deleted. Re-add from history if a real collapsible section is needed.) diff --git a/rog-control-center/ui/widgets/layout.slint b/rog-control-center/ui/widgets/layout.slint new file mode 100644 index 000000000..79c75ccb0 --- /dev/null +++ b/rog-control-center/ui/widgets/layout.slint @@ -0,0 +1,88 @@ +// Structural Layout Components +// Contains page wrappers, accent marks, and notice banners used to structure views. +import { Theme } from "../globals.slint"; + +// — Page root: declares the opacity transition. The actual opacity value is +// driven by main_window per active page (crossfade); this only provides the curve. — +export component RogPage inherits Rectangle { + animate opacity { + duration: 180ms; + easing: ease-out; + } +} + + +// — "Reboot required" notice banner — +// Shown inline next to controls whose changes only apply after a restart +// (GPU mode, MUX, reserved memory, miniLED...). Parent toggles `visible`. +export component RogRebootBanner inherits Rectangle { + height: 40px; + border-radius: 8px; + background: #66550033; + border-width: 1px; + border-color: #E0B04033; + animate opacity { + duration: 200ms; + easing: ease-out; + } + HorizontalLayout { + padding-left: 14px; + padding-right: 14px; + spacing: 10px; + // Full-height cell so the icon can be centred inside it: Slint + // layouts own direct-child geometry, so an Image child can't set y. + Rectangle { + Image { + source: @image-url("../images/nav-about.svg"); + colorize: #E0B040; + width: 18px; + height: 18px; + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + } + } + + Text { + text: @tr("Changes are staged. They will take effect after restarting."); + color: Theme.text-secondary; + font-size: Theme.font-sm; + vertical-alignment: center; + overflow: elide; + } + } +} + + +// ============================================================ +// AC home (dashboard) components — matched against ac-system-page.html (1:1) +// ============================================================ + +// — AC signature slash accent: compact slanted parallelograms. — +// The source is pre-skewed, so there is no rotated bounding box and the mark +// remains vertically centred relative to its title at every row height. +export component RogSlashMark inherits Rectangle { + in property bar-color: Theme.rog-red; + in property count: 3; + in property bar-w: 5px; + in property bar-h: 14px; + width: root.count * (root.bar-w + 11px) - 2px; + + for i in root.count: Image { + source: @image-url("../images/slash-bar.svg"); + colorize: root.bar-color; + x: i * (root.bar-w + 11px); + y: (root.height - root.bar-h) / 2; + width: root.bar-w + 9px; + height: root.bar-h; + image-fit: fill; + } +} + + +// — Dark slashes by metric headings retain their original visual weight. — +export component RogDarkSlashes inherits RogSlashMark { + bar-color: Theme.bg-elevated; + count: 3; + bar-w: 5px; + bar-h: 22px; +} diff --git a/rog-control-center/ui/widgets/metrics.slint b/rog-control-center/ui/widgets/metrics.slint new file mode 100644 index 000000000..3fe383dae --- /dev/null +++ b/rog-control-center/ui/widgets/metrics.slint @@ -0,0 +1,125 @@ +// Dashboard Metrics +// Contains readouts, stat rows, and meters used for displaying hardware telemetry. +import { Theme } from "../globals.slint"; +import { RogDarkSlashes } from "layout.slint"; + +// — Single stat row: left label, right value (13px secondary; dim uses text-dim) — +export component RogMeter inherits Rectangle { + in property value: 0.0; // 0..1 + in property thickness: 4px; + height: root.thickness; + background: Theme.line; + border-radius: 1px; + clip: true; + fill := Rectangle { + x: 0; + width: root.width * clamp(root.value, 0.0, 1.0); + height: 100%; + clip: true; + // Real smooth meter gradient (purple → red), clipped to the fill + // width. Replaces the old 7-band fake gradient. + Rectangle { + x: 0; + width: root.width; + height: 100%; + background: @linear-gradient(90deg, Theme.meter-start, Theme.meter-end); + } + } +} + +export component RogStatRow inherits Rectangle { + in property label; + in property value; + in property dim: false; + background: transparent; + height: 20px; + HorizontalLayout { + Text { + text: root.label; + color: root.dim ? Theme.text-dim : Theme.text-secondary; + font-size: 13px; + vertical-alignment: center; + } + + Rectangle { + horizontal-stretch: 1; + } + + Text { + text: root.value; + color: root.dim ? Theme.text-dim : Theme.text-secondary; + font-size: 13px; + vertical-alignment: center; + } + } +} + + +// — One reading: label+value row + purple-red meter — +export component RogReading inherits Rectangle { + in property label; + in property value; + in property weight; + in property progress: 0.0; + Rectangle { + VerticalLayout { + spacing: 8px; + HorizontalLayout { + Text { + text: root.label; + color: Theme.text-primary; + font-size: Theme.font-md; + font-weight: root.weight; + vertical-alignment: center; + } + + Rectangle { + horizontal-stretch: 1; + } + + Text { + text: root.value; + color: Theme.text-secondary; + font-size: 13px; + vertical-alignment: center; + } + } + + RogMeter { + value: root.progress; + } + } + } +} + + +// — Metric block: big heading + shared slash accent + free reading/stat fill. — +export component RogMetric inherits VerticalLayout { + in property title; + spacing: 7px; + alignment: start; + HorizontalLayout { + spacing: 10px; + padding-bottom: 10px; + Text { + text: root.title; + color: Theme.text-primary; + font-size: 30px; + font-weight: 400; + // Long hardware names (e.g. "RTX 4090 Mobile") wrap instead of + // overflowing the card width. + wrap: word-wrap; + } + + Rectangle { + horizontal-stretch: 1; + } + + RogDarkSlashes { } + } + + @children +} + + +// — Purple→red gradient meter (#6437FF→#FF1929; Slint has no native gradient, simulated) — diff --git a/rog-control-center/ui/widgets/sidebar.slint b/rog-control-center/ui/widgets/sidebar.slint index 1f1fcf295..42155e0b6 100644 --- a/rog-control-center/ui/widgets/sidebar.slint +++ b/rog-control-center/ui/widgets/sidebar.slint @@ -89,7 +89,8 @@ export component NavItem inherits Rectangle { } // Focus ring (2px red outline inside the item bounds). if self.has-focus: Rectangle { - x: 2px; y: 2px; + x: 2px; + y: 2px; width: parent.width - 4px; height: parent.height - 4px; border-radius: 4px; @@ -109,7 +110,6 @@ export component NavItem inherits Rectangle { component HorizontalDivider inherits Rectangle { height: 1px; - width: 100%; background: Theme.divider; } @@ -133,8 +133,14 @@ export component SideBar inherits Rectangle { // The slide: animate the rail width, and a 0..1 factor for the label fade. property expand-anim: root.expanded ? 1.0 : 0.0; - animate width { duration: 200ms; easing: cubic-bezier(0.22, 0.61, 0.36, 1); } - animate expand-anim { duration: 220ms; easing: ease-out; } + animate width { + duration: 200ms; + easing: cubic-bezier(0.22, 0.61, 0.36, 1); + } + animate expand-anim { + duration: 220ms; + easing: ease-out; + } VerticalLayout { spacing: 2px; @@ -144,7 +150,6 @@ export component SideBar inherits Rectangle { // — Header: ROG logo (collapsed) / logo + wordmark (expanded) — Rectangle { height: 64px; - width: 100%; background: transparent; Image { source: @image-url("../../data/rog-sidebar-logo.png"); @@ -154,6 +159,7 @@ export component SideBar inherits Rectangle { y: (parent.height - self.height) / 2; image-fit: contain; } + Text { text: "ROG Control"; color: Theme.text-primary; @@ -182,7 +188,6 @@ export component SideBar inherits Rectangle { // Entries whose backend is absent on this device are skipped // entirely (height 0), so they don't leave a blank slot. property avail: index < root.available.length && root.available[index]; - width: 100%; height: avail ? 48px : 0px; visible: avail; if avail: NavItem { @@ -201,9 +206,9 @@ export component SideBar inherits Rectangle { // — Pin / collapse toggle at the bottom — HorizontalDivider { } + Rectangle { height: 44px; - width: 100%; background: pin-ta.has-hover ? Theme.sidebar-selected : transparent; Image { source: @image-url("../images/nav-expand.svg"); @@ -214,6 +219,7 @@ export component SideBar inherits Rectangle { y: (parent.height - self.height) / 2; image-fit: contain; } + Text { text: root.pinned ? @tr("Collapse") : @tr("Expand"); color: Theme.text-secondary; @@ -222,6 +228,7 @@ export component SideBar inherits Rectangle { x: 56px; opacity: root.expand-anim; } + pin-ta := TouchArea { width: 100%; height: 100%;