diff --git a/.gitignore b/.gitignore index e5209aae..4e4f8d08 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ vendor_* .~lock.* *.ods# *.patch +*.log # gnome extension node-modules diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a126c85e..135a0331 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -31,6 +31,7 @@ stages: - deploy format: + stage: format except: - tags <<: *rust_cache @@ -42,6 +43,7 @@ format: - rm -rf "$CI_PROJECT_DIR/ci-target" || true check: + stage: check except: - tags <<: *rust_cache @@ -55,6 +57,7 @@ check: - rm -rf "$CI_PROJECT_DIR/ci-target" || true test: + stage: test except: - tags <<: *rust_cache @@ -65,6 +68,7 @@ test: - rm -rf "$CI_PROJECT_DIR/ci-target" || true release: + stage: release only: - tags <<: *rust_cache diff --git a/Cargo.lock b/Cargo.lock index 70d70b70..78c4d320 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4502,6 +4502,8 @@ dependencies = [ "rog_dbus", "rog_platform", "rog_profiles", + "rog_slash", + "ron", "serde", "slint", "slint-build", diff --git a/Cargo.toml b/Cargo.toml index 8eee3a43..ebd6c01f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,9 @@ tokio = { version = "^1.39.0", default-features = false, features = [ "time", "rt", "rt-multi-thread", + "fs", + "io-util", + "io-util", ] } concat-idents = "^1.1" dirs = "^4.0" diff --git a/data/asusd.rules b/data/asusd.rules index 6e5c5e91..83812ca9 100644 --- a/data/asusd.rules +++ b/data/asusd.rules @@ -19,4 +19,11 @@ LABEL="asusd_start" ACTION=="add|change", DRIVER=="asus-nb-wmi", TAG+="systemd", ENV{SYSTEMD_WANTS}+="asusd.service" ACTION=="add|remove", DRIVER=="asus-nb-wmi", TAG+="systemd", ENV{SYSTEMD_WANTS}+="asusd.service" +# ASUS Custom Fan Curve Control - Grant user write access +# This allows rog-control-center to adjust fan curves without sudo +SUBSYSTEM=="hwmon", ATTR{name}=="asus_custom_fan_curve", \ + RUN+="/bin/sh -c 'chmod 0666 /sys%p/pwm*'", \ + RUN+="/bin/sh -c 'chmod 0666 /sys%p/temp*_auto_point*_pwm'", \ + RUN+="/bin/sh -c 'chmod 0666 /sys%p/temp*_auto_point*_temp'" + LABEL="asusd_end" diff --git a/rog-control-center/Cargo.toml b/rog-control-center/Cargo.toml index 3b0eddf9..936d0db8 100644 --- a/rog-control-center/Cargo.toml +++ b/rog-control-center/Cargo.toml @@ -29,6 +29,7 @@ rog_dbus = { path = "../rog-dbus" } rog_aura = { path = "../rog-aura" } rog_profiles = { path = "../rog-profiles" } rog_platform = { path = "../rog-platform" } +rog_slash = { path = "../rog-slash" } supergfxctl = { git = "https://gitlab.com/asus-linux/supergfxctl.git", default-features = false } dmi_id = { path = "../dmi-id" } @@ -39,6 +40,7 @@ env_logger.workspace = true tokio.workspace = true serde.workspace = true zbus.workspace = true +ron.workspace = true dirs.workspace = true notify-rust.workspace = true concat-idents.workspace = true diff --git a/rog-control-center/src/main.rs b/rog-control-center/src/main.rs index 8e3811d6..5b63a3b9 100644 --- a/rog-control-center/src/main.rs +++ b/rog-control-center/src/main.rs @@ -11,16 +11,21 @@ use gumdrop::Options; use log::{debug, info, warn, LevelFilter}; use rog_control_center::cli_options::CliStart; use rog_control_center::config::Config; +use tokio::runtime::Runtime; + +thread_local! { + pub static UI: std::cell::RefCell> = Default::default(); + pub static TRAY_TOOLTIP: std::cell::RefCell> = Default::default(); +} use rog_control_center::error::Result; use rog_control_center::notify::start_notifications; use rog_control_center::slint::ComponentHandle; -use rog_control_center::tray::init_tray; +use rog_control_center::tray::{init_tray, TrayEvent, TrayStats}; use rog_control_center::ui::setup_window; use rog_control_center::zbus_proxies::{ AppState, ROGCCZbus, ROGCCZbusProxyBlocking, ZBUS_IFACE, ZBUS_PATH, }; -use rog_control_center::{print_versions, MainWindow}; -use tokio::runtime::Runtime; +use rog_control_center::{print_versions, MainWindow, TrayTooltip}; #[tokio::main] async fn main() -> Result<()> { @@ -165,17 +170,33 @@ async fn main() -> Result<()> { start_notifications(config.clone(), &rt)?; - if enable_tray_icon { - init_tray(supported_properties, config.clone()); - } + let (tray_tx, mut tray_rx) = tokio::sync::mpsc::unbounded_channel(); + // Channel for broadcasting system stats to the tray tooltip + let (stats_tx, stats_rx) = tokio::sync::watch::channel(TrayStats::default()); - thread_local! { pub static UI: std::cell::RefCell> = Default::default()}; + if enable_tray_icon { + init_tray(supported_properties, config.clone(), tray_tx, stats_rx); + } // i_slint_backend_selector::with_platform(|_| Ok(())).unwrap(); if !startup_in_background { if let Ok(mut app_state) = app_state.lock() { *app_state = AppState::MainWindowShouldOpen; } + } else { + // Even in background, we need the UI handle for status polling and tray sync + let config_copy = config.clone(); + let stats_tx_copy = stats_tx.clone(); + slint::invoke_from_event_loop(move || { + UI.with(|ui_cell| { + let mut ui = ui_cell.borrow_mut(); + if ui.is_none() { + let newui = setup_window(config_copy, stats_tx_copy.clone()); + ui.replace(newui); + } + }); + }) + .ok(); } if std::env::var("RUST_TRANSLATIONS").is_ok() { @@ -190,9 +211,18 @@ async fn main() -> Result<()> { thread::spawn(move || { let mut state = AppState::StartingUp; loop { + // Handle tray events + while let Ok(event) = tray_rx.try_recv() { + match event { + TrayEvent::ToggleTooltip(_, _) => { + // Native tooltip handled by ksni, no custom window needed + } + } + } + if is_rog_ally { let config_copy_2 = config.clone(); - let newui = setup_window(config.clone()); + let newui = setup_window(config.clone(), stats_tx.clone()); newui.window().on_close_requested(move || { exit(0); }); @@ -233,6 +263,7 @@ async fn main() -> Result<()> { let config_copy = config.clone(); let app_state_copy = app_state.clone(); + let stats_tx_loop = stats_tx.clone(); slint::invoke_from_event_loop(move || { UI.with(|ui| { let app_state_copy = app_state_copy.clone(); @@ -247,7 +278,7 @@ async fn main() -> Result<()> { }); } else { let config_copy_2 = config_copy.clone(); - let newui = setup_window(config_copy); + let newui = setup_window(config_copy, stats_tx_loop.clone()); newui.window().on_close_requested(move || { if let Ok(mut app_state) = app_state_copy.lock() { *app_state = AppState::MainWindowClosed; diff --git a/rog-control-center/src/notify.rs b/rog-control-center/src/notify.rs index 7b1f1bfd..1ec74460 100644 --- a/rog-control-center/src/notify.rs +++ b/rog-control-center/src/notify.rs @@ -11,6 +11,8 @@ use std::time::Duration; use log::{debug, error, info, warn}; use notify_rust::{Hint, Notification, Timeout}; +use rog_dbus::zbus_platform::PlatformProxy; +use rog_platform::platform::PlatformProfile; use rog_platform::power::AsusPower; use serde::{Deserialize, Serialize}; use supergfxctl::pci_device::GfxPower; @@ -28,6 +30,7 @@ pub struct EnabledNotifications { pub enabled: bool, pub receive_notify_gfx: bool, pub receive_notify_gfx_status: bool, + pub receive_notify_platform_profile: bool, } impl Default for EnabledNotifications { @@ -36,6 +39,7 @@ impl Default for EnabledNotifications { enabled: true, receive_notify_gfx: true, receive_notify_gfx_status: true, + receive_notify_platform_profile: true, } } } @@ -86,6 +90,57 @@ fn start_dpu_status_mon(config: Arc>) { } } +/// Start monitoring for platform profile changes (triggered by Fn+F5 or software) +/// and display an OSD notification when the profile changes. +fn start_platform_profile_mon(config: Arc>, rt: &Runtime) { + let enabled_notifications_copy = config.clone(); + rt.spawn(async move { + let conn = match zbus::Connection::system().await { + Ok(c) => c, + Err(e) => { + error!("zbus signal: platform_profile_mon: {e}"); + return; + } + }; + let proxy = match PlatformProxy::builder(&conn).build().await { + Ok(p) => p, + Err(e) => { + error!("zbus signal: platform_profile_mon proxy: {e}"); + return; + } + }; + + // Get initial profile to avoid notification on startup + let mut last_profile = proxy.platform_profile().await.ok(); + + info!("Started platform profile change monitor"); + use futures_util::StreamExt; + let mut stream = proxy.receive_platform_profile_changed().await; + while let Some(e) = stream.next().await { + if let Ok(config) = enabled_notifications_copy.lock() { + if !config.notifications.enabled + || !config.notifications.receive_notify_platform_profile + { + continue; + } + } + if let Ok(new_profile) = e.get().await { + // Only show notification if profile actually changed + if last_profile != Some(new_profile) { + debug!("Platform profile changed to: {:?}", new_profile); + if let Err(e) = do_platform_profile_notif("Power Profile:", &new_profile) + .show() + .map(|n| n.on_close(|_| ())) + { + warn!("Failed to show platform profile notification: {e}"); + } + last_profile = Some(new_profile); + } + } + } + }); +} + pub fn start_notifications( config: Arc>, rt: &Runtime, @@ -144,6 +199,9 @@ pub fn start_notifications( info!("Attempting to start plain dgpu status monitor"); start_dpu_status_mon(config.clone()); + info!("Starting platform profile change monitor"); + start_platform_profile_mon(config.clone(), rt); + // GPU MUX Mode notif // TODO: need to get armoury attrs and iter to find // let enabled_notifications_copy = config.clone(); @@ -209,3 +267,28 @@ fn do_gpu_status_notif(message: &str, data: &GfxPower) -> Notification { notif.icon(icon); notif } + +/// Create a notification for platform profile (power mode) changes. +/// Uses profile-specific icons and user-friendly names. +fn do_platform_profile_notif(message: &str, profile: &PlatformProfile) -> Notification { + let profile_name = match profile { + PlatformProfile::Balanced => "Balanced", + PlatformProfile::Performance => "Performance", + PlatformProfile::Quiet => "Quiet", + PlatformProfile::LowPower => "Low Power", + PlatformProfile::Custom => "Custom", + }; + let mut notif = base_notification(message, &profile_name.to_owned()); + + // Use appropriate icons for each profile + // These icons should be available in the system or ROG icon pack + let icon = match profile { + PlatformProfile::Balanced => "asus_notif_blue", // Blue for balanced + PlatformProfile::Performance => "asus_notif_red", // Red for performance + PlatformProfile::Quiet => "asus_notif_green", // Green for quiet/power saving + PlatformProfile::LowPower => "asus_notif_green", // Green for low power + PlatformProfile::Custom => "asus_notif_white", // White for custom + }; + notif.icon(icon); + notif +} diff --git a/rog-control-center/src/tray.rs b/rog-control-center/src/tray.rs index ea806711..8a127994 100644 --- a/rog-control-center/src/tray.rs +++ b/rog-control-center/src/tray.rs @@ -59,6 +59,29 @@ struct AsusTray { current_title: String, current_icon: Icon, proxy: ROGCCZbusProxyBlocking<'static>, + tray_channel: Option>, + // System stats for native tooltip + cpu_temp: String, + gpu_temp: String, + cpu_fan: String, + gpu_fan: String, + power_w: String, + power_profile: String, +} + +#[derive(Debug, Clone, Copy)] +pub enum TrayEvent { + ToggleTooltip(i32, i32), +} + +#[derive(Debug, Clone, Default)] +pub struct TrayStats { + pub cpu_temp: String, + pub gpu_temp: String, + pub cpu_fan: String, + pub gpu_fan: String, + pub power_w: String, + pub power_profile: String, } impl ksni::Tray for AsusTray { @@ -78,6 +101,26 @@ impl ksni::Tray for AsusTray { ksni::Status::Active } + fn activate(&mut self, x: i32, y: i32) { + if let Some(tx) = &self.tray_channel { + let _ = tx.send(TrayEvent::ToggleTooltip(x, y)); + } + } + + fn tool_tip(&self) -> ksni::ToolTip { + ksni::ToolTip { + title: "ROG Control Center".into(), + description: format!( + "Profile: {}\nCPU: {}°C | GPU: {}°C\nFans: {} / {} RPM\nPower: {} W", + self.power_profile, + self.cpu_temp, self.gpu_temp, + self.cpu_fan, self.gpu_fan, + self.power_w + ), + ..Default::default() + } + } + fn menu(&self) -> Vec> { use ksni::menu::*; vec![ @@ -155,7 +198,12 @@ fn find_dgpu() -> Option { } /// The tray is controlled somewhat by `Arc>` -pub fn init_tray(_supported_properties: Vec, config: Arc>) { +pub fn init_tray( + _supported_properties: Vec, + config: Arc>, + tray_channel: tokio::sync::mpsc::UnboundedSender, + mut stats_rx: tokio::sync::watch::Receiver, +) { tokio::spawn(async move { let user_con = zbus::blocking::Connection::session().unwrap(); let proxy = ROGCCZbusProxyBlocking::new(&user_con).unwrap(); @@ -166,6 +214,14 @@ pub fn init_tray(_supported_properties: Vec, config: Arc, config: Arc { + let stats = stats_rx.borrow().clone(); + tray.update(move |t| { + t.cpu_temp = stats.cpu_temp; + t.gpu_temp = stats.gpu_temp; + t.cpu_fan = stats.cpu_fan; + t.gpu_fan = stats.gpu_fan; + t.power_w = stats.power_w; + t.power_profile = stats.power_profile; + }).await; } - } - if has_supergfx { - if let Ok(mode) = gfx_proxy.mode().await { - if let Ok(power) = gfx_proxy.power().await { - if last_power != power { - set_tray_icon_and_tip(mode, power, &mut tray, has_supergfx).await; - last_power = power; + _ = tokio::time::sleep(Duration::from_millis(1000)) => { + if let Ok(lock) = config.try_lock() { + if !lock.enable_tray_icon { + return; } } - } - } else if let Some(dev) = dev.as_ref() { - if let Ok(power) = dev.get_runtime_status() { - if last_power != power { - set_tray_icon_and_tip(GfxMode::Hybrid, power, &mut tray, has_supergfx) - .await; - last_power = power; + // Handle GPU icon updates + if has_supergfx { + if let Ok(mode) = gfx_proxy.mode().await { + if let Ok(power) = gfx_proxy.power().await { + if last_power != power { + set_tray_icon_and_tip(mode, power, &mut tray, has_supergfx).await; + last_power = power; + } + } + } + } else if let Some(dev) = dev.as_ref() { + if let Ok(power) = dev.get_runtime_status() { + if last_power != power { + set_tray_icon_and_tip(GfxMode::Hybrid, power, &mut tray, has_supergfx) + .await; + last_power = power; + } + } } } } diff --git a/rog-control-center/src/ui/aura_animator.rs b/rog-control-center/src/ui/aura_animator.rs new file mode 100644 index 00000000..e16b1a81 --- /dev/null +++ b/rog-control-center/src/ui/aura_animator.rs @@ -0,0 +1,200 @@ +//! Software-based keyboard animation for keyboards that only support Static mode. +//! Provides Rainbow and Color Cycle animations via timer-based color updates. + +use log::{info, warn}; +use slint::Weak; +use std::process::Command; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use crate::MainWindow; + +/// Animation mode enum matching the UI +#[derive(Clone, Copy, Debug, PartialEq, Default)] +pub enum AnimationMode { + #[default] + None, + Rainbow, + ColorCycle, +} + +impl From for AnimationMode { + fn from(v: i32) -> Self { + match v { + 1 => AnimationMode::Rainbow, + 2 => AnimationMode::ColorCycle, + _ => AnimationMode::None, + } + } +} + +/// Shared state for the animator +pub struct AnimatorState { + /// Current animation mode + pub mode: AtomicU32, + /// Animation speed in milliseconds (update interval) + pub speed_ms: AtomicU32, + /// Stop signal + pub stop: AtomicBool, + /// Current hue for rainbow mode (0-360) + hue: AtomicU32, +} + +impl Default for AnimatorState { + fn default() -> Self { + Self { + mode: AtomicU32::new(0), + speed_ms: AtomicU32::new(200), + stop: AtomicBool::new(false), + hue: AtomicU32::new(0), + } + } +} + +/// Convert HSV to RGB (H: 0-360, S: 0-100, V: 0-100) +fn hsv_to_rgb(h: u32, s: u32, v: u32) -> (u8, u8, u8) { + let s = s as f32 / 100.0; + let v = v as f32 / 100.0; + let c = v * s; + let h_prime = (h as f32 / 60.0) % 6.0; + let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs()); + let m = v - c; + + let (r, g, b) = match h_prime as u32 { + 0 => (c, x, 0.0), + 1 => (x, c, 0.0), + 2 => (0.0, c, x), + 3 => (0.0, x, c), + 4 => (x, 0.0, c), + _ => (c, 0.0, x), + }; + + ( + ((r + m) * 255.0) as u8, + ((g + m) * 255.0) as u8, + ((b + m) * 255.0) as u8, + ) +} + +/// Format RGB as hex color string for asusctl +fn rgb_to_hex(r: u8, g: u8, b: u8) -> String { + format!("{:02x}{:02x}{:02x}", r, g, b) +} + +// Simple LCG for random numbers to avoid pulling in rand crate +fn next_random(seed: &mut u64) -> u32 { + *seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); + (*seed >> 32) as u32 +} + +/// Start the animation loop (runs in tokio task) +pub fn start_animator(state: Arc, _ui_weak: Weak) { + info!("Starting keyboard animator"); + + tokio::spawn(async move { + // Local state for Color Cycle (RGB) + let mut current_r: f32 = 255.0; + let mut current_g: f32 = 0.0; + let mut current_b: f32 = 0.0; + let mut target_r: f32 = 0.0; + let mut target_g: f32 = 255.0; + let mut target_b: f32 = 0.0; + let mut seed = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(12345); + + loop { + // Check for stop signal + if state.stop.load(Ordering::Relaxed) { + info!("Animator stopping"); + break; + } + + let mode = AnimationMode::from(state.mode.load(Ordering::Relaxed) as i32); + // Cap speed at 150ms for stability + let raw_speed = state.speed_ms.load(Ordering::Relaxed); + let effective_speed = raw_speed.max(150) as u64; + + if mode == AnimationMode::None { + // No animation, sleep longer + tokio::time::sleep(Duration::from_millis(500)).await; + continue; + } + + // Calculate next color + let hex_color = match mode { + AnimationMode::Rainbow => { + // Hue step 1 for smooth, granular transitions + let hue = state.hue.fetch_add(1, Ordering::Relaxed) % 360; + let (r, g, b) = hsv_to_rgb(hue, 100, 100); + rgb_to_hex(r, g, b) + } + AnimationMode::ColorCycle => { + // RGB Linear Interpolation (Fading) - NOT Rainbow + + // 1. Check distance to target + let dist_sq = (target_r - current_r).powi(2) + + (target_g - current_g).powi(2) + + (target_b - current_b).powi(2); + + // If close, pick new random target color + if dist_sq < 100.0 { + let next_h = next_random(&mut seed) % 360; + let (r, g, b) = hsv_to_rgb(next_h, 100, 100); + target_r = r as f32; + target_g = g as f32; + target_b = b as f32; + } + + // 2. Lerp towards target (5% per frame for smooth ease-out) + let factor = 0.05; + current_r += (target_r - current_r) * factor; + current_g += (target_g - current_g) * factor; + current_b += (target_b - current_b) * factor; + + rgb_to_hex(current_r as u8, current_g as u8, current_b as u8) + } + AnimationMode::None => continue, + }; + + // Send color update via asusctl command (blocking, AWAITED to prevent races) + let hex = hex_color.clone(); + let _ = tokio::task::spawn_blocking(move || { + let result = Command::new("asusctl") + .args([ + "aura", "static", "-c", &hex, + ]) + .output(); + + if let Err(e) = result { + warn!("Failed to set aura color: {}", e); + } + }) + .await; + + // Sleep for the animation speed interval + tokio::time::sleep(Duration::from_millis(effective_speed)).await; + } + }); +} + +/// Stop the animator +pub fn stop_animator(state: &Arc) { + state.stop.store(true, Ordering::Relaxed); + state.mode.store(0, Ordering::Relaxed); +} + +/// Set animation mode +pub fn set_animation_mode(state: &Arc, mode: AnimationMode) { + state.mode.store(mode as u32, Ordering::Relaxed); + // Reset stop flag in case we're restarting + state.stop.store(false, Ordering::Relaxed); +} + +/// Set animation speed +pub fn set_animation_speed(state: &Arc, speed_ms: u32) { + let clamped = speed_ms.clamp(50, 2000); + state.speed_ms.store(clamped, Ordering::Relaxed); +} diff --git a/rog-control-center/src/ui/mod.rs b/rog-control-center/src/ui/mod.rs index ac1122f0..3adc544a 100644 --- a/rog-control-center/src/ui/mod.rs +++ b/rog-control-center/src/ui/mod.rs @@ -1,6 +1,12 @@ +pub mod aura_animator; pub mod setup_anime; pub mod setup_aura; +pub mod setup_fan_curve_custom; pub mod setup_fans; +pub mod setup_screenpad; +pub mod setup_slash; +pub mod setup_status; +pub mod setup_supergfx; pub mod setup_system; use std::sync::{Arc, Mutex}; @@ -11,9 +17,14 @@ use rog_dbus::list_iface_blocking; use slint::{ComponentHandle, SharedString, Weak}; use crate::config::Config; +use crate::tray::TrayStats; use crate::ui::setup_anime::setup_anime_page; use crate::ui::setup_aura::setup_aura_page; use crate::ui::setup_fans::setup_fan_curve_page; +use crate::ui::setup_screenpad::setup_screenpad; +use crate::ui::setup_slash::setup_slash; +use crate::ui::setup_status::setup_status; +use crate::ui::setup_supergfx::setup_supergfx; use crate::ui::setup_system::{setup_system_page, setup_system_page_callbacks}; use crate::{AppSettingsPageData, MainWindow}; @@ -82,7 +93,10 @@ pub fn show_toast( }; } -pub fn setup_window(config: Arc>) -> MainWindow { +pub fn setup_window( + config: Arc>, + stats_tx: tokio::sync::watch::Sender, +) -> MainWindow { slint::set_xdg_app_id("rog-control-center") .map_err(|e| warn!("Couldn't set application ID: {e:?}")) .ok(); @@ -101,9 +115,27 @@ pub fn setup_window(config: Arc>) -> MainWindow { available.contains(&"xyz.ljones.Platform".to_string()), available.contains(&"xyz.ljones.Aura".to_string()), available.contains(&"xyz.ljones.Anime".to_string()), + available.contains(&"xyz.ljones.Slash".to_string()), + // Supergfx check + { + if let Ok(conn) = zbus::blocking::Connection::system() { + zbus::blocking::fdo::DBusProxy::new(&conn) + .ok() + .and_then(|p| { + p.name_has_owner("org.supergfxctl.Daemon".try_into().ok()?) + .ok() + }) + .unwrap_or(false) + } else { + false + } + }, + // Screenpad check (Backlight interface) + available.contains(&"xyz.ljones.Backlight".to_string()), available.contains(&"xyz.ljones.FanCurves".to_string()), true, true, + true, ] .into(), ); @@ -112,6 +144,33 @@ pub fn setup_window(config: Arc>) -> MainWindow { slint::quit_event_loop().unwrap(); }); + // Auto-hide toast logic + let toast_gen = Arc::new(Mutex::new(0u64)); + let ui_weak = ui.as_weak(); + ui.on_start_toast_timer(move || { + let toast_gen_clone = toast_gen.clone(); + let ui_weak_clone = ui_weak.clone(); + let my_gen = { + if let Ok(mut g) = toast_gen.lock() { + *g += 1; + *g + } else { + 0 + } + }; + if my_gen > 0 { + tokio::spawn(async move { + tokio::time::sleep(tokio::time::Duration::from_secs(4)).await; + if let Ok(g) = toast_gen_clone.lock() { + if *g == my_gen { + let _ = + ui_weak_clone.upgrade_in_event_loop(move |ui| ui.invoke_hide_toast()); + } + } + }); + } + }); + setup_app_settings_page(&ui, config.clone()); if available.contains(&"xyz.ljones.Platform".to_string()) { setup_system_page(&ui, config.clone()); @@ -123,10 +182,24 @@ pub fn setup_window(config: Arc>) -> MainWindow { if available.contains(&"xyz.ljones.Anime".to_string()) { setup_anime_page(&ui, config.clone()); } - if available.contains(&"xyz.ljones.FanCurves".to_string()) { - setup_fan_curve_page(&ui, config); + if available.contains(&"xyz.ljones.Slash".to_string()) { + setup_slash(&ui, config.clone()); } + // Always try to setup supergfx if detected above, but for simplicity here we assume if sidebar has it (re-check or just run) + // We didn't capture the boolean above. Let's just run it, it handles its own availability check internally via async proxy creation. + setup_supergfx(&ui, config.clone()); + + if available.contains(&"xyz.ljones.Backlight".to_string()) { + setup_screenpad(&ui, config.clone()); + } + + if available.contains(&"xyz.ljones.FanCurves".to_string()) { + setup_fan_curve_page(&ui, config.clone()); + } + + setup_status(&ui, config, stats_tx); + ui } @@ -153,18 +226,49 @@ pub fn setup_app_settings_page(ui: &MainWindow, config: Arc>) { lock.write(); } }); + + // Master notifications toggle let config_copy = config.clone(); - global.on_set_enable_dgpu_notifications(move |enable| { + global.on_set_notifications_enabled(move |enable| { if let Ok(mut lock) = config_copy.try_lock() { lock.notifications.enabled = enable; lock.write(); } }); + // Granular notification toggles + let config_copy = config.clone(); + global.on_set_notify_gfx_switch(move |enable| { + if let Ok(mut lock) = config_copy.try_lock() { + lock.notifications.receive_notify_gfx = enable; + lock.write(); + } + }); + + let config_copy = config.clone(); + global.on_set_notify_gfx_status(move |enable| { + if let Ok(mut lock) = config_copy.try_lock() { + lock.notifications.receive_notify_gfx_status = enable; + lock.write(); + } + }); + + let config_copy = config.clone(); + global.on_set_notify_platform_profile(move |enable| { + if let Ok(mut lock) = config_copy.try_lock() { + lock.notifications.receive_notify_platform_profile = enable; + lock.write(); + } + }); + + // Initialize UI values from config if let Ok(lock) = config.try_lock() { global.set_run_in_background(lock.run_in_background); global.set_startup_in_background(lock.startup_in_background); global.set_enable_tray_icon(lock.enable_tray_icon); - global.set_enable_dgpu_notifications(lock.notifications.enabled); + global.set_notifications_enabled(lock.notifications.enabled); + global.set_notify_gfx_switch(lock.notifications.receive_notify_gfx); + global.set_notify_gfx_status(lock.notifications.receive_notify_gfx_status); + global.set_notify_platform_profile(lock.notifications.receive_notify_platform_profile); } } diff --git a/rog-control-center/src/ui/setup_aura.rs b/rog-control-center/src/ui/setup_aura.rs index 31938416..5ba459f4 100644 --- a/rog-control-center/src/ui/setup_aura.rs +++ b/rog-control-center/src/ui/setup_aura.rs @@ -7,6 +7,9 @@ use rog_dbus::zbus_aura::AuraProxy; use slint::{ComponentHandle, Model, RgbaColor, SharedString}; use crate::config::Config; +use crate::ui::aura_animator::{ + set_animation_mode, set_animation_speed, start_animator, AnimationMode, AnimatorState, +}; use crate::ui::show_toast; use crate::{ set_ui_callbacks, set_ui_props_async, AuraPageData, MainWindow, PowerZones as SlintPowerZones, @@ -123,8 +126,17 @@ pub fn setup_aura_page(ui: &MainWindow, _states: Arc>) { .ok(); } + // Create animator state (shared across callbacks) + let animator_state = Arc::new(AnimatorState::default()); + if let Ok(modes) = aura.supported_basic_modes().await { log::debug!("Available LED modes {modes:?}"); + + // Check if only Static mode is available (enable software animation) + let static_only = modes.len() == 1 && modes.iter().any(|m| *m == 0.into()); + + let handle_for_anim = handle.clone(); + let animator_state_clone = animator_state.clone(); handle .upgrade_in_event_loop(move |handle| { let m: Vec = modes.iter().map(|n| (*n).into()).collect(); @@ -143,6 +155,33 @@ pub fn setup_aura_page(ui: &MainWindow, _states: Arc>) { handle .global::() .set_available_mode_names(res.as_slice().into()); + + // Enable software animation if only Static mode is available + if static_only { + info!("Only Static mode available - enabling software animation controls"); + handle + .global::() + .set_soft_animation_available(true); + + // Start the animator thread + start_animator(animator_state_clone.clone(), handle_for_anim.clone()); + + // Connect mode callback + let state_for_mode = animator_state_clone.clone(); + handle + .global::() + .on_cb_soft_animation_mode(move |mode| { + set_animation_mode(&state_for_mode, AnimationMode::from(mode)); + }); + + // Connect speed callback + let state_for_speed = animator_state_clone.clone(); + handle + .global::() + .on_cb_soft_animation_speed(move |speed| { + set_animation_speed(&state_for_speed, speed as u32); + }); + } }) .map_err(|e| error!("{e:}")) .ok(); diff --git a/rog-control-center/src/ui/setup_fan_curve_custom.rs b/rog-control-center/src/ui/setup_fan_curve_custom.rs new file mode 100644 index 00000000..da89e0b4 --- /dev/null +++ b/rog-control-center/src/ui/setup_fan_curve_custom.rs @@ -0,0 +1,241 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use log::{error, info}; +use serde::{Deserialize, Serialize}; + +use crate::{FanType, MainWindow, Node}; + +const ASUS_CUSTOM_FAN_NAME: &str = "asus_custom_fan_curve"; +const CONFIG_FILE_NAME: &str = "custom_fans.ron"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CustomCurvePoint { + pub temp: u8, + pub pwm: u8, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CustomFanConfig { + pub cpu_curve: Vec, + pub gpu_curve: Vec, + pub enabled: bool, +} + +#[derive(Clone)] +struct SysfsPaths { + root: PathBuf, +} + +impl SysfsPaths { + fn new() -> Option { + let hwmon = Path::new("/sys/class/hwmon"); + if let Ok(entries) = fs::read_dir(hwmon) { + for entry in entries.flatten() { + let path = entry.path(); + let name_path = path.join("name"); + if let Ok(name) = fs::read_to_string(&name_path) { + if name.trim() == ASUS_CUSTOM_FAN_NAME { + info!("Found ASUS Custom Fan Control at {:?}", path); + return Some(Self { root: path }); + } + } + } + } + None + } + + fn enable_path(&self, index: u8) -> PathBuf { + self.root.join(format!("pwm{}_enable", index)) + } + + fn point_pwm_path(&self, fan_idx: u8, point_idx: u8) -> PathBuf { + self.root + .join(format!("pwm{}_auto_point{}_pwm", fan_idx, point_idx)) + } + + fn point_temp_path(&self, fan_idx: u8, point_idx: u8) -> PathBuf { + self.root + .join(format!("pwm{}_auto_point{}_temp", fan_idx, point_idx)) + } +} + +// Helper to write with logging +fn write_sysfs(path: &Path, value: &str) -> std::io::Result<()> { + // debug!("Writing {} to {:?}", value, path); + fs::write(path, value) +} + +fn _read_sysfs_u8(path: &Path) -> std::io::Result { + let s = fs::read_to_string(path)?; + s.trim() + .parse::() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) +} + +pub fn is_custom_fan_supported() -> bool { + SysfsPaths::new().is_some() +} + +// Logic to apply a full curve to a specific fan (1=CPU, 2=GPU usually) +// Implements the "Gradual Descent" algorithm +fn apply_curve_to_fan( + paths: &SysfsPaths, + fan_idx: u8, + points: &[CustomCurvePoint], +) -> std::io::Result<()> { + // Sort target points by temp (Hardware Requirement) + let mut sorted_target = points.to_vec(); + sorted_target.sort_by_key(|p| p.temp); + + // Ensure we have 8 points (fill with last if needed, or sensible default) + while sorted_target.len() < 8 { + if let Some(last) = sorted_target.last() { + sorted_target.push(last.clone()); + } else { + sorted_target.push(CustomCurvePoint { + temp: 100, + pwm: 255, + }); + } + } + sorted_target.truncate(8); + + // Validate Temp Order (Synchronous Check) + for (i, p) in sorted_target.iter().enumerate() { + if i > 0 { + let prev_temp = sorted_target[i - 1].temp; + if p.temp < prev_temp { + error!("Invalid temp order"); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Temp disorder", + )); + } + } + } + + // Spawn completely detached thread for ALL I/O + let paths_clone = paths.clone(); + let sorted_target = sorted_target.clone(); + + std::thread::spawn(move || { + let paths = paths_clone; + + // 1. Enable custom mode + if let Err(e) = write_sysfs(&paths.enable_path(fan_idx), "1") { + error!("Failed to enable custom fan mode: {}", e); + return; + } + + // 2. Write Temps + for (i, p) in sorted_target.iter().enumerate() { + let point_idx = (i + 1) as u8; + if let Err(e) = write_sysfs( + &paths.point_temp_path(fan_idx, point_idx), + &p.temp.to_string(), + ) { + error!("Failed to write temp point {}: {}", point_idx, e); + } + } + + // 3. Write PWMs directly (hardware handles gradual transition) + for (i, target_p) in sorted_target.iter().enumerate() { + let point_idx = (i + 1) as u8; + if let Err(e) = write_sysfs( + &paths.point_pwm_path(fan_idx, point_idx), + &target_p.pwm.to_string(), + ) { + error!("Failed to write PWM point {}: {}", point_idx, e); + } + } + + // 4. Ensure enable is set + let _ = write_sysfs(&paths.enable_path(fan_idx), "1"); + }); + + Ok(()) +} + +fn set_fan_auto(paths: &SysfsPaths, fan_idx: u8) -> std::io::Result<()> { + // 2 = Auto (usually) + write_sysfs(&paths.enable_path(fan_idx), "2") +} + +fn load_config() -> CustomFanConfig { + if let Some(config_dir) = dirs::config_dir() { + let path = config_dir.join("rog").join(CONFIG_FILE_NAME); + if let Ok(content) = fs::read_to_string(path) { + if let Ok(cfg) = ron::from_str(&content) { + return cfg; + } + } + } + CustomFanConfig::default() +} + +fn save_config(config: &CustomFanConfig) { + if let Some(config_dir) = dirs::config_dir() { + let rog_dir = config_dir.join("rog"); + let _ = fs::create_dir_all(&rog_dir); + let path = rog_dir.join(CONFIG_FILE_NAME); + if let Ok(s) = ron::ser::to_string_pretty(config, ron::ser::PrettyConfig::default()) { + let _ = fs::write(path, s); + } + } +} + +// Public entry point called from setup_fans.rs or similar +// Returns immediately - all work is done in a detached thread +pub fn apply_custom_fan_curve( + _handle_weak: slint::Weak, + fan_type: FanType, + enabled: bool, + nodes: Vec, +) { + // Fan Index: 1=CPU, 2=GPU usually. + let fan_idx = match fan_type { + FanType::CPU => 1, + FanType::GPU => 2, + _ => return, // Ignore others + }; + + // Convert nodes to points (fast, CPU-only) + let points: Vec = nodes + .iter() + .map(|n| CustomCurvePoint { + temp: n.x as u8, + pwm: n.y as u8, + }) + .collect(); + + // Spawn a completely detached thread for ALL I/O + std::thread::spawn(move || { + // Get paths (blocking FS operation) + let Some(paths) = SysfsPaths::new() else { + error!("No custom fan support found"); + return; + }; + + // Save config + let mut cfg = load_config(); + if enabled { + match fan_type { + FanType::CPU => cfg.cpu_curve = points.clone(), + FanType::GPU => cfg.gpu_curve = points.clone(), + _ => {} + } + } + cfg.enabled = enabled; + save_config(&cfg); + + // Apply curve or set auto + if enabled { + if let Err(e) = apply_curve_to_fan(&paths, fan_idx, &points) { + error!("Failed to apply fan curve: {}", e); + } + } else if let Err(e) = set_fan_auto(&paths, fan_idx) { + error!("Failed to set fan auto: {}", e); + } + }); +} diff --git a/rog-control-center/src/ui/setup_fans.rs b/rog-control-center/src/ui/setup_fans.rs index 5ee7f647..bca587af 100644 --- a/rog-control-center/src/ui/setup_fans.rs +++ b/rog-control-center/src/ui/setup_fans.rs @@ -1,4 +1,6 @@ -use std::sync::{Arc, Mutex}; +use crate::ui::show_toast; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, OnceLock}; use log::error; use rog_dbus::zbus_fan_curves::FanCurvesProxy; @@ -8,7 +10,25 @@ use rog_profiles::fan_curve_set::CurveData; use slint::{ComponentHandle, Model, Weak}; use crate::config::Config; -use crate::{FanPageData, FanType, MainWindow, Node}; +use crate::{FanPageData, FanType, MainWindow, Node, Profile}; + +// Isolated Rust-side cache for fan curves (not affected by Slint reactivity) +type FanCacheKey = (i32, i32); // (Profile as i32, FanType as i32) +static FAN_CACHE: OnceLock>>> = OnceLock::new(); + +fn fan_cache() -> &'static Mutex>> { + FAN_CACHE.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn cache_fan_curve(profile: Profile, fan_type: FanType, nodes: Vec) { + let key = (profile as i32, fan_type as i32); + fan_cache().lock().unwrap().insert(key, nodes); +} + +fn get_cached_fan_curve(profile: Profile, fan_type: FanType) -> Option> { + let key = (profile as i32, fan_type as i32); + fan_cache().lock().unwrap().get(&key).cloned() +} pub fn update_fan_data( handle: Weak, @@ -19,7 +39,7 @@ pub fn update_fan_data( handle .upgrade_in_event_loop(move |handle| { let global = handle.global::(); - let collect = |temp: &[u8], pwm: &[u8]| -> slint::ModelRc { + let _collect = |temp: &[u8], pwm: &[u8]| -> slint::ModelRc { let tmp: Vec = temp .iter() .zip(pwm.iter()) @@ -33,61 +53,100 @@ pub fn update_fan_data( for fan in bal { global.set_balanced_available(true); + let nodes_vec: Vec = fan + .temp + .iter() + .zip(fan.pwm.iter()) + .map(|(x, y)| Node { + x: *x as f32, + y: *y as f32, + }) + .collect(); + let nodes: slint::ModelRc = nodes_vec.as_slice().into(); match fan.fan { rog_profiles::FanCurvePU::CPU => { global.set_cpu_fan_available(true); global.set_balanced_cpu_enabled(fan.enabled); - global.set_balanced_cpu(collect(&fan.temp, &fan.pwm)) + global.set_balanced_cpu(nodes.clone()); + cache_fan_curve(Profile::Balanced, FanType::CPU, nodes_vec); } rog_profiles::FanCurvePU::GPU => { global.set_gpu_fan_available(true); global.set_balanced_gpu_enabled(fan.enabled); - global.set_balanced_gpu(collect(&fan.temp, &fan.pwm)) + global.set_balanced_gpu(nodes.clone()); + cache_fan_curve(Profile::Balanced, FanType::GPU, nodes_vec); } rog_profiles::FanCurvePU::MID => { global.set_mid_fan_available(true); global.set_balanced_mid_enabled(fan.enabled); - global.set_balanced_mid(collect(&fan.temp, &fan.pwm)) + global.set_balanced_mid(nodes.clone()); + cache_fan_curve(Profile::Balanced, FanType::Middle, nodes_vec); } } } for fan in perf { global.set_performance_available(true); + let nodes_vec: Vec = fan + .temp + .iter() + .zip(fan.pwm.iter()) + .map(|(x, y)| Node { + x: *x as f32, + y: *y as f32, + }) + .collect(); + let nodes: slint::ModelRc = nodes_vec.as_slice().into(); match fan.fan { rog_profiles::FanCurvePU::CPU => { global.set_cpu_fan_available(true); global.set_performance_cpu_enabled(fan.enabled); - global.set_performance_cpu(collect(&fan.temp, &fan.pwm)) + global.set_performance_cpu(nodes.clone()); + cache_fan_curve(Profile::Performance, FanType::CPU, nodes_vec); } rog_profiles::FanCurvePU::GPU => { global.set_gpu_fan_available(true); global.set_performance_gpu_enabled(fan.enabled); - global.set_performance_gpu(collect(&fan.temp, &fan.pwm)) + global.set_performance_gpu(nodes.clone()); + cache_fan_curve(Profile::Performance, FanType::GPU, nodes_vec); } rog_profiles::FanCurvePU::MID => { global.set_mid_fan_available(true); global.set_performance_mid_enabled(fan.enabled); - global.set_performance_mid(collect(&fan.temp, &fan.pwm)) + global.set_performance_mid(nodes.clone()); + cache_fan_curve(Profile::Performance, FanType::Middle, nodes_vec); } } } for fan in quiet { global.set_quiet_available(true); + let nodes_vec: Vec = fan + .temp + .iter() + .zip(fan.pwm.iter()) + .map(|(x, y)| Node { + x: *x as f32, + y: *y as f32, + }) + .collect(); + let nodes: slint::ModelRc = nodes_vec.as_slice().into(); match fan.fan { rog_profiles::FanCurvePU::CPU => { global.set_cpu_fan_available(true); global.set_quiet_cpu_enabled(fan.enabled); - global.set_quiet_cpu(collect(&fan.temp, &fan.pwm)) + global.set_quiet_cpu(nodes.clone()); + cache_fan_curve(Profile::Quiet, FanType::CPU, nodes_vec); } rog_profiles::FanCurvePU::GPU => { global.set_gpu_fan_available(true); global.set_quiet_gpu_enabled(fan.enabled); - global.set_quiet_gpu(collect(&fan.temp, &fan.pwm)) + global.set_quiet_gpu(nodes.clone()); + cache_fan_curve(Profile::Quiet, FanType::GPU, nodes_vec); } rog_profiles::FanCurvePU::MID => { global.set_mid_fan_available(true); global.set_quiet_mid_enabled(fan.enabled); - global.set_quiet_mid(collect(&fan.temp, &fan.pwm)) + global.set_quiet_mid(nodes.clone()); + cache_fan_curve(Profile::Quiet, FanType::Middle, nodes_vec); } } } @@ -171,6 +230,7 @@ pub fn setup_fan_curve_page(ui: &MainWindow, _config: Arc>) { let choices_for_ui = platform_profile_choices.clone(); let handle_next1 = handle_copy.clone(); if let Err(e) = handle_copy.upgrade_in_event_loop(move |handle| { + let handle_weak_for_fans = handle.as_weak(); let global = handle.global::(); let fans1 = fans.clone(); let choices = choices_for_ui.clone(); @@ -212,17 +272,103 @@ pub fn setup_fan_curve_page(ui: &MainWindow, _config: Arc>) { update_fan_data(handle_next, balanced, perf, quiet); }); }); + + let handle_weak_for_cancel = handle_weak_for_fans.clone(); global.on_set_fan_data(move |fan, profile, enabled, data| { + if crate::ui::setup_fan_curve_custom::is_custom_fan_supported() { + let handle_weak = handle_weak_for_fans.clone(); + let data: Vec = data.iter().collect(); + + use log::info; + info!("MainThread: Request to apply custom curve for {:?}", fan); + + // Explicitly spawn a thread to handle this, preventing ANY main thread blocking + std::thread::spawn(move || { + info!("WorkerThread: applying curve for {:?}", fan); + crate::ui::setup_fan_curve_custom::apply_custom_fan_curve( + handle_weak.clone(), + fan, + enabled, + data, + ); + info!("WorkerThread: returned from apply (async), clearing busy flag for {:?}", fan); + + // Clear busy flag + let _ = handle_weak.upgrade_in_event_loop(move |h| { + let g = h.global::(); + match fan { + FanType::CPU => g.set_is_busy_cpu(false), + FanType::GPU => g.set_is_busy_gpu(false), + FanType::Middle => g.set_is_busy_mid(false), + } + info!("MainThread: cleared busy flag for {:?}", fan); + }); + }); + + return; + } + let fans = fans.clone(); - let data: Vec = data.iter().collect(); - let data = fan_data_for(fan, enabled, data); + let handle_weak = handle_weak_for_fans.clone(); + let nodes_vec: Vec = data.iter().collect(); + let _data_copy = nodes_vec.clone(); + let cache_copy = nodes_vec.clone(); // Clone for cache update + let fan_data = fan_data_for(fan, enabled, nodes_vec); tokio::spawn(async move { - fans.set_fan_curve(profile.into(), data) - .await - .map_err(|e| error!("{e:}")) - .ok() + show_toast( + "Fan curve applied".into(), + "Failed to apply fan curve".into(), + handle_weak.clone(), + fans.set_fan_curve(profile.into(), fan_data).await, + ); + let _ = handle_weak.upgrade_in_event_loop(move |h| { + let g = h.global::(); + // Update Rust-side cache (isolated from Slint properties) + cache_fan_curve(profile, fan, cache_copy); + + match fan { + FanType::CPU => g.set_is_busy_cpu(false), + FanType::GPU => g.set_is_busy_gpu(false), + FanType::Middle => g.set_is_busy_mid(false), + } + }); }); }); + global.on_cancel(move |fan_type, profile| { + let handle_weak = handle_weak_for_cancel.clone(); + let _ = handle_weak.upgrade_in_event_loop(move |h: MainWindow| { + let global = h.global::(); + + // Retrieve from isolated Rust cache + let nodes_opt = get_cached_fan_curve(profile, fan_type); + + if let Some(nodes_vec) = nodes_opt { + use log::info; + info!("Canceling {:?} {:?} - restoring {} nodes from isolated cache", fan_type, profile, nodes_vec.len()); + + let new_model: slint::ModelRc = nodes_vec.as_slice().into(); + + match (profile, fan_type) { + (crate::Profile::Balanced, FanType::CPU) => global.set_balanced_cpu(new_model), + (crate::Profile::Balanced, FanType::GPU) => global.set_balanced_gpu(new_model), + (crate::Profile::Balanced, FanType::Middle) => global.set_balanced_mid(new_model), + (crate::Profile::Performance, FanType::CPU) => global.set_performance_cpu(new_model), + (crate::Profile::Performance, FanType::GPU) => global.set_performance_gpu(new_model), + (crate::Profile::Performance, FanType::Middle) => global.set_performance_mid(new_model), + (crate::Profile::Quiet, FanType::CPU) => global.set_quiet_cpu(new_model), + (crate::Profile::Quiet, FanType::GPU) => global.set_quiet_gpu(new_model), + (crate::Profile::Quiet, FanType::Middle) => global.set_quiet_mid(new_model), + _ => {} + } + } else { + log::warn!("Cancel failed: No cached data for {:?} {:?}", fan_type, profile); + } + }); + }); + // Initialize warning + if crate::ui::setup_fan_curve_custom::is_custom_fan_supported() { + global.set_show_custom_warning(true); + } }) { error!("setup_fan_curve_page: upgrade_in_event_loop: {e:?}"); } diff --git a/rog-control-center/src/ui/setup_screenpad.rs b/rog-control-center/src/ui/setup_screenpad.rs new file mode 100644 index 00000000..2babee35 --- /dev/null +++ b/rog-control-center/src/ui/setup_screenpad.rs @@ -0,0 +1,143 @@ +use log::{debug, error}; +use rog_dbus::zbus_backlight::BacklightProxy; +use slint::ComponentHandle; +use std::sync::{Arc, Mutex}; + +use crate::config::Config; +use crate::ui::show_toast; +use crate::{MainWindow, ScreenpadPageData}; + +pub fn setup_screenpad(ui: &MainWindow, _config: Arc>) { + let handle = ui.as_weak(); + + tokio::spawn(async move { + // Create the connections/proxies here + let conn = match zbus::Connection::system().await { + Ok(conn) => conn, + Err(e) => { + error!("Failed to connect to system bus for Screenpad: {e:}"); + return; + } + }; + + let backlight = match BacklightProxy::builder(&conn).build().await { + Ok(backlight) => backlight, + Err(e) => { + error!("Failed to create backlight proxy for Screenpad: {e:}"); + return; + } + }; + + // Initialize state + debug!("Initializing Screenpad page data"); + + // Use helper to set initial properties + if let Ok(val) = backlight.screenpad_brightness().await { + handle + .upgrade_in_event_loop(move |h| { + h.global::().set_brightness(val); + // Assume power is on if brightness > 0 + h.global::().set_power(val > 0); + }) + .ok(); + } + + if let Ok(gamma_str) = backlight.screenpad_gamma().await { + if let Ok(gamma) = gamma_str.parse::() { + handle + .upgrade_in_event_loop(move |h| { + h.global::().set_gamma(gamma); + }) + .ok(); + } + } + + if let Ok(sync) = backlight.screenpad_sync_with_primary().await { + handle + .upgrade_in_event_loop(move |h| { + h.global::().set_sync_with_primary(sync); + }) + .ok(); + } + + // Set up callbacks + let handle_copy = handle.clone(); + let backlight_copy = backlight.clone(); + handle + .upgrade_in_event_loop(move |h| { + let global = h.global::(); + + // Brightness Callback + let hl = handle_copy.clone(); + let bl = backlight_copy.clone(); + global.on_cb_brightness(move |val| { + let bl = bl.clone(); + let hl = hl.clone(); + tokio::spawn(async move { + show_toast( + format!("Screenpad brightness set to {}", val).into(), + "Failed to set Screenpad brightness".into(), + hl, + bl.set_screenpad_brightness(val).await, + ); + }); + }); + + // Gamma Callback + let hl = handle_copy.clone(); + let bl = backlight_copy.clone(); + global.on_cb_gamma(move |val| { + let bl = bl.clone(); + let hl = hl.clone(); + tokio::spawn(async move { + show_toast( + format!("Screenpad gamma set to {:.2}", val).into(), + "Failed to set Screenpad gamma".into(), + hl, + bl.set_screenpad_gamma(&val.to_string()).await, + ); + }); + }); + + // Sync Callback + let hl = handle_copy.clone(); + let bl = backlight_copy.clone(); + global.on_cb_sync_with_primary(move |val| { + let bl = bl.clone(); + let hl = hl.clone(); + tokio::spawn(async move { + show_toast( + format!( + "Screenpad sync {}", + if val { "enabled" } else { "disabled" } + ) + .into(), + "Failed to toggle Screenpad sync".into(), + hl, + bl.set_screenpad_sync_with_primary(val).await, + ); + }); + }); + + // Power Callback (Toggle brightness to 0/last or 100) + let hl = handle_copy.clone(); + let bl = backlight_copy.clone(); + global.on_cb_power(move |val| { + let bl = bl.clone(); + let hl = hl.clone(); + tokio::spawn(async move { + let target = if val { 100 } else { 0 }; + let _ = bl.set_screenpad_brightness(target).await; + hl.upgrade_in_event_loop(move |h| { + h.global::().set_brightness(target); + }) + .ok(); + }); + }); + }) + .ok(); + + // Optional: Value watches for external changes + // (Similar to setup_system.rs if needed) + }); +} diff --git a/rog-control-center/src/ui/setup_slash.rs b/rog-control-center/src/ui/setup_slash.rs new file mode 100644 index 00000000..e9428bc7 --- /dev/null +++ b/rog-control-center/src/ui/setup_slash.rs @@ -0,0 +1,132 @@ +use crate::config::Config; +use crate::set_ui_callbacks; +use crate::ui::show_toast; +use crate::{MainWindow, SlashPageData}; +use rog_dbus::{find_iface_async, zbus_slash::SlashProxy}; +use rog_slash::SlashMode; +use slint::{ComponentHandle, Model}; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +pub fn setup_slash(ui: &MainWindow, _config: Arc>) { + let ui_weak = ui.as_weak(); + + tokio::spawn(async move { + // Find the Slash interface proxy + let proxies = match find_iface_async::("xyz.ljones.Slash").await { + Ok(p) => p, + Err(e) => { + log::warn!("Failed to find Slash interface: {}", e); + return; + } + }; + + let proxy = match proxies.first() { + Some(p) => p.clone(), + None => return, + }; + + // UI Callbacks (MUST be done on UI thread to access global state) + { + let proxy_copy = proxy.clone(); + + let _ = ui_weak.upgrade_in_event_loop(move |ui| { + let proxy = proxy_copy.clone(); + let ui_handle = ui; + + set_ui_callbacks!(ui_handle, SlashPageData(), proxy.enabled(), "Slash enabled {}", "Failed to enable slash"); + + // Fix: Cast f32 (UI) to u8 (DBus) + set_ui_callbacks!(ui_handle, SlashPageData(as f32), proxy.brightness(as u8), "Slash brightness set to {}", "Failed to set slash brightness"); + set_ui_callbacks!(ui_handle, SlashPageData(as f32), proxy.interval(as u8), "Slash interval set to {}", "Failed to set slash interval"); + + set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_battery_warning(), "Battery warning set to {}", "Failed to set battery warning"); + set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_battery(), "Show on battery set to {}", "Failed to set show on battery"); + set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_boot(), "Show on boot set to {}", "Failed to set show on boot"); + set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_shutdown(), "Show on shutdown set to {}", "Failed to set show on shutdown"); + set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_sleep(), "Show on sleep set to {}", "Failed to set show on sleep"); + set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_lid_closed(), "Show on lid closed set to {}", "Failed to set show on lid closed"); + }); + } + + // Custom Mode Logic - Callback setup + { + let proxy_copy = proxy.clone(); + let ui_weak_copy = ui_weak.clone(); + let _ = ui_weak.upgrade_in_event_loop(move |ui| { + let data = ui.global::(); + + data.on_cb_mode_index(move |idx| { + let proxy_copy = proxy_copy.clone(); + let handle_weak = ui_weak_copy.clone(); + + let mode_str_opt = if let Some(h) = handle_weak.upgrade() { + let d = h.global::(); + if idx >= 0 && (idx as usize) < d.get_modes().row_count() { + Some(d.get_modes().row_data(idx as usize).unwrap_or_default()) + } else { + None + } + } else { + None + }; + + if let Some(mode_str) = mode_str_opt { + if let Ok(mode) = SlashMode::from_str(&mode_str) { + tokio::spawn(async move { + show_toast( + format!("Slash mode set to {}", mode).into(), + "Failed to set slash mode".into(), + handle_weak, + proxy_copy.set_mode(mode).await, + ); + }); + } + } + }); + }); + } + + // D-Bus Signal -> UI + let proxy_copy = proxy.clone(); + let handle_copy = ui_weak.clone(); + tokio::spawn(async move { + let mut changes = proxy_copy.receive_mode_changed().await; + use futures_util::StreamExt; + while let Some(change) = changes.next().await { + if let Ok(mode) = change.get().await { + let mode_str = mode.to_string(); + let handle_copy = handle_copy.clone(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(h) = handle_copy.upgrade() { + let d = h.global::(); + let model = d.get_modes(); + for (i, m) in model.iter().enumerate() { + if m == mode_str { + d.set_mode_index(i as i32); + break; + } + } + } + }); + } + } + }); + + if let Ok(m) = proxy.mode().await { + let mode_str = m.to_string(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(h) = ui_weak.upgrade() { + let d = h.global::(); + let model = d.get_modes(); + for (i, m) in model.iter().enumerate() { + if m == mode_str { + d.set_mode_index(i as i32); + break; + } + } + } + }); + } + }); +} diff --git a/rog-control-center/src/ui/setup_status.rs b/rog-control-center/src/ui/setup_status.rs new file mode 100644 index 00000000..f3c98c22 --- /dev/null +++ b/rog-control-center/src/ui/setup_status.rs @@ -0,0 +1,150 @@ +use crate::config::Config; +use crate::tray::TrayStats; +use crate::{MainWindow, SystemStatus}; +use rog_dbus::zbus_platform::PlatformProxy; +use slint::ComponentHandle; +use std::collections::VecDeque; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use tokio::fs; +use tokio::time::Duration; +use zbus::Connection; + +pub fn setup_status( + ui: &MainWindow, + _config: Arc>, + stats_tx: tokio::sync::watch::Sender, +) { + let ui_weak = ui.as_weak(); + + tokio::spawn(async move { + let mut power_history: VecDeque = VecDeque::with_capacity(150); // 300s window at 2s poll + // DBus connection for profile + let conn = Connection::system().await.ok(); + let platform = if let Some(c) = &conn { + PlatformProxy::new(c).await.ok() + } else { + None + }; + + loop { + let (cpu_temp, gpu_temp, cpu_fan, gpu_fan) = read_hwmon().await; + let power_microwatts = read_power().await; + + // Rolling average logic + if power_history.len() >= 150 { + power_history.pop_front(); + } + power_history.push_back(power_microwatts); + + let sum: i64 = power_history.iter().map(|&x| x as i64).sum(); + let avg_microwatts = if !power_history.is_empty() { + sum / power_history.len() as i64 + } else { + 0 + }; + + // Convert to Watts + let power_w = power_microwatts as f64 / 1_000_000.0; + let avg_w = avg_microwatts as f64 / 1_000_000.0; + + // Fetch profile + let mut profile_str = "Unknown".to_string(); + if let Some(p) = &platform { + if let Ok(prof) = p.platform_profile().await { + profile_str = format!("{:?}", prof); + } + } + let ui_weak_loop = ui_weak.clone(); // Clone ui_weak for this iteration + + // Send to Tray + let _ = stats_tx.send(TrayStats { + cpu_temp: format!("{}", cpu_temp), + gpu_temp: format!("{}", gpu_temp), + cpu_fan: format!("{}", cpu_fan), + gpu_fan: format!("{}", gpu_fan), + power_w: format!("{:.1}", power_w), + power_profile: profile_str, + }); + + let _ = slint::invoke_from_event_loop(move || { + if let Some(ui) = ui_weak_loop.upgrade() { + let global = ui.global::(); + global.set_cpu_temp(cpu_temp); + global.set_gpu_temp(gpu_temp); + global.set_cpu_fan(cpu_fan); + global.set_gpu_fan(gpu_fan); + + global.set_power_w(slint::SharedString::from(format!("{:.1}", power_w))); + global.set_power_avg_w(slint::SharedString::from(format!("{:.1}", avg_w))); + } + }); + + tokio::time::sleep(Duration::from_secs(2)).await; + } + }); +} + +async fn read_hwmon() -> (i32, i32, i32, i32) { + let mut cpu_temp = 0; + let mut gpu_temp = 0; + let mut cpu_fan = 0; + let mut gpu_fan = 0; + + let mut entries = match fs::read_dir("/sys/class/hwmon").await { + Ok(e) => e, + Err(_) => return (0, 0, 0, 0), + }; + + while let Ok(Some(entry)) = entries.next_entry().await { + let path = entry.path(); + let name_path = path.join("name"); + + if let Ok(name_str) = fs::read_to_string(&name_path).await { + let name = name_str.trim(); + + if name == "k10temp" || name == "coretemp" || name == "zenpower" { + // Try temp1_input (TCtl/Package) + if let Ok(temp) = read_val(&path.join("temp1_input")).await { + cpu_temp = temp / 1000; + } + } else if name == "amdgpu" || name == "nvidia" { + if let Ok(temp) = read_val(&path.join("temp1_input")).await { + gpu_temp = temp / 1000; + } + } else if name == "asus" || name == "asus_custom_fan_curve" { + if let Ok(fan) = read_val(&path.join("fan1_input")).await { + cpu_fan = fan; + } + if let Ok(fan) = read_val(&path.join("fan2_input")).await { + gpu_fan = fan; + } + } + } + } + + (cpu_temp, gpu_temp, cpu_fan, gpu_fan) +} + +async fn read_val(path: &PathBuf) -> Result { + let s = fs::read_to_string(path).await.map_err(|_| ())?; + s.trim().parse::().map_err(|_| ()) +} + +async fn read_power() -> i32 { + let mut p = 0; + // Try BAT0 then BAT1 + if let Ok(v) = read_val(&PathBuf::from("/sys/class/power_supply/BAT0/power_now")).await { + p = v.abs(); + } else if let Ok(v) = read_val(&PathBuf::from("/sys/class/power_supply/BAT1/power_now")).await { + p = v.abs(); + } + + // Check status + if let Ok(s) = fs::read_to_string("/sys/class/power_supply/BAT0/status").await { + if s.trim() == "Discharging" { + return -p; + } + } + p +} diff --git a/rog-control-center/src/ui/setup_supergfx.rs b/rog-control-center/src/ui/setup_supergfx.rs new file mode 100644 index 00000000..64995d4b --- /dev/null +++ b/rog-control-center/src/ui/setup_supergfx.rs @@ -0,0 +1,102 @@ +use crate::config::Config; +use crate::ui::show_toast; +use crate::{MainWindow, SupergfxPageData}; +use slint::{ComponentHandle, Model, SharedString, VecModel}; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use zbus::proxy; + +#[proxy( + interface = "org.supergfxctl.Daemon", + default_service = "org.supergfxctl.Daemon", + default_path = "/org/supergfxctl/Gfx" +)] +trait Supergfx { + fn supported(&self) -> zbus::Result>; + fn mode(&self) -> zbus::Result; + fn set_mode(&self, mode: &str) -> zbus::Result<()>; + fn vendor(&self) -> zbus::Result; +} + +pub fn setup_supergfx(ui: &MainWindow, _config: Arc>) { + let ui_weak = ui.as_weak(); + + tokio::spawn(async move { + let conn = match zbus::Connection::system().await { + Ok(c) => c, + Err(e) => { + log::warn!("Failed to connect to system bus: {}", e); + return; + } + }; + + let proxy = match SupergfxProxy::new(&conn).await { + Ok(p) => p, + Err(e) => { + log::warn!("Failed to create Supergfx proxy: {}", e); + return; + } + }; + + // Register Callbacks on UI Thread + { + let proxy_copy = proxy.clone(); + let ui_weak_copy = ui_weak.clone(); + let _ = ui_weak.upgrade_in_event_loop(move |ui| { + let handle_copy = ui_weak_copy.clone(); + ui.global::() + .on_set_mode(move |mode_str| { + let proxy = proxy_copy.clone(); + let handle = handle_copy.clone(); + tokio::spawn(async move { + show_toast( + format!("Switching to {}. Logout required.", mode_str).into(), + "Failed to set mode".into(), + handle, + proxy.set_mode(&mode_str).await, + ); + }); + }); + }); + } + + // Fetch Initial State + // Vendor + if let Ok(vendor) = proxy.vendor().await { + let _ = ui_weak.upgrade_in_event_loop(move |ui| { + ui.global::().set_vendor(vendor.into()) + }); + } + + // Supported Modes + if let Ok(supported) = proxy.supported().await { + let modes: Vec = supported + .iter() + .map(|s| SharedString::from(s.as_str())) + .collect(); + let _ = ui_weak.upgrade_in_event_loop(move |ui| { + let mode_model = Rc::new(VecModel::from(modes)); + ui.global::() + .set_supported_modes(mode_model.into()) + }); + } + + // Current Mode + if let Ok(mode) = proxy.mode().await { + let _ = ui_weak.upgrade_in_event_loop(move |ui| { + let g = ui.global::(); + g.set_current_mode(mode.clone().into()); + // Update selection index + let model = g.get_supported_modes(); + for (i, m) in model.iter().enumerate() { + if m == mode.as_str() { + g.set_selected_index(i as i32); + break; + } + } + }); + } + + // No signal monitoring implemented as supergfxctl state changes usually require user action/logout + }); +} diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index fb41168c..96b8029e 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -47,7 +47,6 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { ui.global::().set_screen_auto_brightness(-1); ui.global::().set_mcu_powersave(-1); ui.global::().set_mini_led_mode(-1); - ui.global::().set_screenpad_brightness(-1); ui.global::().set_ppt_pl1_spl(MINMAX); ui.global::().set_ppt_pl2_sppt(MINMAX); ui.global::().set_ppt_pl3_fppt(MINMAX); @@ -296,7 +295,7 @@ pub fn setup_system_page_callbacks(ui: &MainWindow, _states: Arc>) log::error!("Failed to create platform proxy: {}", e); }) .unwrap(); - let backlight = BacklightProxy::builder(&conn) + let _backlight = BacklightProxy::builder(&conn) .build() .await .map_err(|e| { @@ -397,23 +396,7 @@ pub fn setup_system_page_callbacks(ui: &MainWindow, _states: Arc>) set_ui_props_async!(handle, platform, SystemPageData, enable_ppt_group); - set_ui_props_async!(handle, backlight, SystemPageData, screenpad_brightness); - if let Ok(value) = backlight.screenpad_gamma().await { - handle - .upgrade_in_event_loop(move |handle| { - handle - .global::() - .set_screenpad_gamma(value.parse().unwrap_or(1.0)); - }) - .ok(); - } - - set_ui_props_async!( - handle, - backlight, - SystemPageData, - screenpad_sync_with_primary - ); + set_ui_props_async!(handle, platform, SystemPageData, enable_ppt_group); let platform_copy = platform.clone(); handle @@ -536,25 +519,11 @@ pub fn setup_system_page_callbacks(ui: &MainWindow, _states: Arc>) "Setting Throttle policy on AC failed" ); - set_ui_callbacks!(handle, - SystemPageData(as i32), - backlight.screenpad_brightness(as i32), - "Screenpad successfully set to {}", - "Setting screenpad brightness failed" - ); - set_ui_callbacks!(handle, SystemPageData(as bool), - backlight.screenpad_sync_with_primary(as bool), - "Screenpad successfully set to {}", - "Setting screenpad brightness failed" - ); - - set_ui_callbacks!(handle, - SystemPageData(.parse().unwrap_or(1.0)), - backlight.screenpad_gamma(.to_string().as_str()), - "Screenpad successfully set to {}", - "Setting screenpad brightness failed" + platform_copy.change_platform_profile_on_battery(.into()), + "Throttle policy on battery enabled: {}", + "Setting Throttle policy on AC failed" ); }) .ok(); diff --git a/rog-control-center/ui/main_window.slint b/rog-control-center/ui/main_window.slint index 0623c1dc..7b119e6b 100644 --- a/rog-control-center/ui/main_window.slint +++ b/rog-control-center/ui/main_window.slint @@ -1,9 +1,12 @@ -import { Palette, Button, VerticalBox } from "std-widgets.slint"; +import { Button, VerticalBox } from "std-widgets.slint"; import { AppSize } from "globals.slint"; import { PageSystem, SystemPageData, AttrMinMax } from "pages/system.slint"; import { SideBar } from "widgets/sidebar.slint"; import { PageAbout } from "pages/about.slint"; import { PageFans } from "pages/fans.slint"; +import { PageSlash, SlashPageData } from "pages/slash.slint"; +import { PageSupergfx, SupergfxPageData } from "pages/supergfx.slint"; +import { PageScreenpad, ScreenpadPageData } from "pages/screenpad.slint"; import { PageAnime, AnimePageData } from "pages/anime.slint"; import { RogItem } from "widgets/common.slint"; import { PageAura } from "pages/aura.slint"; @@ -14,8 +17,18 @@ export { FanPageData, FanType, Profile } import { AuraPageData, AuraDevType, LaptopAuraPower, AuraPowerState, PowerZones, AuraEffect } from "types/aura_types.slint"; export { AuraPageData, AuraDevType, LaptopAuraPower, AuraPowerState, PowerZones, AuraEffect } import { PageAppSettings, AppSettingsPageData } from "pages/app_settings.slint"; +import { StatusBar, SystemStatus } from "widgets/status_bar.slint"; +import { TrayTooltip } from "windows/tray_tooltip.slint"; +export { TrayTooltip } -export { AppSize, AttrMinMax, SystemPageData, AnimePageData, AppSettingsPageData } +import { RogPalette } from "themes/rog_theme.slint"; + +export { AppSize, AttrMinMax, SystemPageData, AnimePageData, AppSettingsPageData, SystemStatus, SlashPageData, SupergfxPageData, ScreenpadPageData } + +export global SomeError { + in property error_message: ""; + in property error_help: ""; +} export component MainWindow inherits Window { title: "ROG Control"; @@ -24,93 +37,133 @@ export component MainWindow inherits Window { default-font-size: 14px; default-font-weight: 400; icon: @image-url("../data/rog-control-center.png"); - in property <[bool]> sidebar_items_avilable: [true, true, true, true, true, true]; + in property <[bool]> sidebar_items_avilable: [true, true, true, true, true, true, true, true, true]; private property show_notif; private property fade_cover; private property toast: false; private property toast_text: "I show when something is waiting"; + callback show_toast(string); + callback start_toast_timer(); + callback hide_toast(); + + hide_toast() => { + toast = false; + } + show_toast(text) => { toast = text != ""; toast_text = text; + if (toast) { + start_toast_timer(); + } } + callback exit-app(); callback show_notification(bool); + show_notification(yes) => { show_notif = yes; fade_cover = yes; } + callback external_colour_change(); external_colour_change() => { aura.external_colour_change(); aura.external_colour_change(); } + min-height: AppSize.height; min-width: AppSize.width; - background: Colors.black; - HorizontalLayout { - padding: 0px; - VerticalLayout { - side-bar := SideBar { - title: @tr("ROG"); - model: [ - @tr("Menu1" => "System Control"), - @tr("Menu2" => "Keyboard Aura"), - @tr("Menu3" => "AniMe Matrix"), - @tr("Menu4" => "Fan Curves"), - @tr("Menu5" => "App Settings"), - @tr("Menu6" => "About"), - ]; - available: root.sidebar_items_avilable; - } + background: RogPalette.background; - Rectangle { - max-height: 40px; - width: side-bar.width; - background: Palette.control-background; - Text { - vertical-alignment: center; - horizontal-alignment: center; - text: @tr("Quit App"); + VerticalLayout { + HorizontalLayout { + padding: 0px; + + // Left Column: Sidebar + Quit Button + VerticalLayout { + side-bar := SideBar { + title: @tr("ROG"); + model: [ + @tr("Menu1" => "System Control"), + @tr("Menu2" => "Keyboard Aura"), + @tr("Menu3" => "AniMe Matrix"), + @tr("Menu7" => "Slash Lighting"), + @tr("Menu8" => "Graphics Control"), + @tr("Menu9" => "Screenpad Control"), + @tr("Menu4" => "Fan Curves"), + @tr("Menu5" => "App Settings"), + @tr("Menu6" => "About"), + ]; + available: root.sidebar_items_avilable; } - TouchArea { - clicked => { - root.exit-app(); + Rectangle { + max-height: 40px; + width: side-bar.width; + background: RogPalette.control-background; + Text { + vertical-alignment: center; + horizontal-alignment: center; + text: @tr("Quit App"); + color: RogPalette.text-primary; + } + + TouchArea { + clicked => { + root.exit-app(); + } } } } - } - Rectangle { - background: Palette.background; - if(side-bar.current-item == 0): page := PageSystem { - width: root.width - side-bar.width; - height: root.height + 12px; - } + // Right Column: Content Pages + Rectangle { + background: RogPalette.background; + if(side-bar.current-item == 0): page := PageSystem { + width: root.width - side-bar.width; + height: root.height + 12px; + } - aura := PageAura { - width: root.width - side-bar.width; - visible: side-bar.current-item == 1; - } + aura := PageAura { + width: root.width - side-bar.width; + visible: side-bar.current-item == 1; + } - if(side-bar.current-item == 2): PageAnime { - width: root.width - side-bar.width; - } + if(side-bar.current-item == 2): PageAnime { + width: root.width - side-bar.width; + } - fans := PageFans { - width: root.width - side-bar.width; - visible: side-bar.current-item == 3; - } + if(side-bar.current-item == 3): PageSlash { + width: root.width - side-bar.width; + } - if(side-bar.current-item == 4): PageAppSettings { - width: root.width - side-bar.width; - } + if(side-bar.current-item == 4): PageSupergfx { + width: root.width - side-bar.width; + } - if(side-bar.current-item == 5): PageAbout { - width: root.width - side-bar.width; + if(side-bar.current-item == 5): PageScreenpad { + width: root.width - side-bar.width; + } + + fans := PageFans { + width: root.width - side-bar.width; + visible: side-bar.current-item == 6; + } + + if(side-bar.current-item == 7): PageAppSettings { + width: root.width - side-bar.width; + } + + if(side-bar.current-item == 8): PageAbout { + width: root.width - side-bar.width; + } } } + + // Bottom: Status Bar + StatusBar {} } if fade_cover: Rectangle { @@ -118,7 +171,7 @@ export component MainWindow inherits Window { y: 0px; width: root.width; height: root.height; - background: Colors.rgba(25,33,23,20); + background: Colors.rgba(0,0,0,0.7); opacity: 0.7; TouchArea { height: 100%; @@ -133,13 +186,24 @@ export component MainWindow inherits Window { } } - if toast: Rectangle { - x: 0px; - y: 0px; - width: root.width; - height: 32px; - opacity: 1.0; - background: Colors.grey; + // Modern floating toast/snackbar notification + // Shows at the bottom center, non-intrusive + Rectangle { + visible: self.opacity > 0; + opacity: root.toast ? 1 : 0; + animate opacity { duration: 300ms; } + + x: (root.width - 400px) / 2; // Center horizontally + y: root.height - 80px; // Bottom padding + width: 400px; + height: 48px; + border-radius: RogPalette.border-radius; + border-width: 1px; + border-color: RogPalette.accent; + background: RogPalette.control-background; + drop-shadow-blur: 10px; + drop-shadow-color: Colors.black; + TouchArea { height: 100%; width: 100%; @@ -148,13 +212,23 @@ export component MainWindow inherits Window { } } - Rectangle { - height: 100%; - width: 100%; - background: Palette.control-background; + HorizontalLayout { + padding-left: 16px; + padding-right: 16px; + alignment: space-between; + Text { - color: Palette.control-foreground; + vertical-alignment: center; + color: RogPalette.text-primary; text: root.toast_text; + overflow: elide; + } + + Text { + vertical-alignment: center; + text: "Dismiss"; + color: RogPalette.text-secondary; + font-size: 12px; } } } @@ -174,13 +248,20 @@ export component MainWindow inherits Window { } } - // TODO: add properties to display Rectangle { height: 100%; width: 100%; - background: Palette.background; - Text { - text: "Click here to exit"; + background: RogPalette.control-background; + border-radius: 8px; + + VerticalLayout { + alignment: center; + Text { + horizontal-alignment: center; + text: "Click here to exit"; + color: RogPalette.text-primary; + font-size: 16px; + } } } } @@ -190,35 +271,45 @@ export component MainWindow inherits Window { y: 0px; width: root.width; height: root.height; - - //padding only has effect on layout elements - //padding: 10px; - background: Palette.background; - border-color: Palette.border; - border-width: 3px; - border-radius: 10px; + background: RogPalette.background; + border-color: RogPalette.accent; + border-width: 2px; + border-radius: 8px; VerticalBox { - RogItem { - min-height: 50px; - max-height: 100px; + padding: 20px; + spacing: 15px; + alignment: center; + + Text { + text: "Error"; + font-size: 22px; + font-weight: 700; + color: RogPalette.accent; + horizontal-alignment: center; + } + + Rectangle { + background: RogPalette.control-background; + border-radius: 8px; + min-height: 60px; + Text { text <=> SomeError.error_message; - font-size: 18px; + font-size: 16px; + color: RogPalette.text-primary; + horizontal-alignment: center; + vertical-alignment: center; } } Text { text <=> SomeError.error_help; - horizontal-alignment: TextHorizontalAlignment.center; - vertical-alignment: TextVerticalAlignment.center; + color: RogPalette.text-secondary; + horizontal-alignment: center; + vertical-alignment: center; } } } } - -export global SomeError { - in property error_message: ""; - in property error_help: ""; -} diff --git a/rog-control-center/ui/pages/about.slint b/rog-control-center/ui/pages/about.slint index 621c2365..d1e43ca6 100644 --- a/rog-control-center/ui/pages/about.slint +++ b/rog-control-center/ui/pages/about.slint @@ -1,62 +1,128 @@ -import { AboutSlint, VerticalBox, HorizontalBox } from "std-widgets.slint"; +import { VerticalBox, HorizontalBox, ScrollView } from "std-widgets.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; -export component PageAbout inherits VerticalLayout { - padding: 10px; - spacing: 10px; - Text { - vertical-alignment: TextVerticalAlignment.center; - horizontal-alignment: TextHorizontalAlignment.center; - text: "A UI for asusctl made with slint"; - font-size: 22px; - } +export component PageAbout inherits Rectangle { + background: RogPalette.background; - HorizontalBox { - alignment: LayoutAlignment.center; + ScrollView { VerticalBox { - alignment: LayoutAlignment.center; + padding: 30px; + spacing: 20px; + alignment: center; + // Title Text { - wrap: TextWrap.word-wrap; - text: "You need to use kernel version 6.19 to use this software"; + horizontal-alignment: center; + text: "ROG Control Center"; + font-size: 28px; + font-weight: 800; + color: RogPalette.accent; } Text { - vertical-alignment: TextVerticalAlignment.center; - horizontal-alignment: TextHorizontalAlignment.center; - text: "Todo:"; - font-size: 22px; + horizontal-alignment: center; + text: "A modern UI for asusctl built with Slint"; + font-size: 16px; + color: RogPalette.text-secondary; } - Text { - text: "- [ ] Theme the widgets"; + // Version info + Rectangle { + height: 60px; + background: RogPalette.control-background; + border-radius: 8px; + border-width: 1px; + border-color: RogPalette.control-border; + + HorizontalBox { + padding: 15px; + alignment: center; + Text { + text: "Version 6.3.0"; + font-size: 14px; + color: RogPalette.text-primary; + } + Text { + text: " | "; + color: RogPalette.text-secondary; + } + Text { + text: "Requires kernel 6.10+"; + font-size: 14px; + color: RogPalette.text-secondary; + } + } } - Text { - text: "- [ ] Add a cpu/gpu temp/fan speed info bar"; + // Features section + Rectangle { + background: RogPalette.control-background; + border-radius: 8px; + border-width: 1px; + border-color: RogPalette.control-border; + + VerticalBox { + padding: 20px; + spacing: 12px; + + Text { + text: "Features"; + font-size: 18px; + font-weight: 700; + color: RogPalette.accent; + } + + // Completed features + Text { text: "[x] ROG-themed dark UI"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] System status bar (CPU/GPU temps & fan speeds)"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] Power profile management"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] Aura RGB keyboard lighting"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] AniMe Matrix display"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] Slash LED control"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] Supergfx graphics switching"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] Screenpad brightness & gamma"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] Custom fan curves"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] Desktop notifications (KDE OSD)"; color: RogPalette.text-primary; font-size: 13px; } + Text { text: "[x] System tray integration"; color: RogPalette.text-primary; font-size: 13px; } + + // Pending features + Rectangle { height: 10px; } + Text { text: "Planned:"; font-size: 14px; font-weight: 600; color: RogPalette.text-secondary; } + Text { text: "[ ] ROG Ally specific settings"; color: RogPalette.text-secondary; font-size: 13px; } + Text { text: "[ ] Advanced Aura zone editing"; color: RogPalette.text-secondary; font-size: 13px; } + } } - Text { - text: "- [ ] Include fan speeds, temps in a bottom bar"; - } + // Credits + Rectangle { + background: RogPalette.control-background; + border-radius: 8px; + border-width: 1px; + border-color: RogPalette.control-border; - Text { - text: "- [ ] Slash control"; - } + VerticalBox { + padding: 20px; + spacing: 8px; - Text { - text: "- [ ] Screenpad controls"; - } + Text { + text: "Credits"; + font-size: 18px; + font-weight: 700; + color: RogPalette.accent; + } - Text { - text: "- [ ] ROG Ally specific settings"; + Text { + text: "asusctl & asusd by Luke Jones"; + font-size: 13px; + color: RogPalette.text-primary; + } + Text { + text: "UI built with Slint"; + font-size: 13px; + color: RogPalette.text-secondary; + } + } } } } - - Text { - vertical-alignment: TextVerticalAlignment.center; - horizontal-alignment: TextHorizontalAlignment.center; - text: "Work in progress"; - font-size: 22px; - } } diff --git a/rog-control-center/ui/pages/anime.slint b/rog-control-center/ui/pages/anime.slint index aef04ea2..6536b339 100644 --- a/rog-control-center/ui/pages/anime.slint +++ b/rog-control-center/ui/pages/anime.slint @@ -1,5 +1,6 @@ import { SystemDropdown, SystemToggle } from "../widgets/common.slint"; import { Palette, GroupBox, VerticalBox, Button, HorizontalBox } from "std-widgets.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; export global AnimePageData { in-out property <[string]> brightness_names: [ @@ -109,7 +110,7 @@ export component PageAnime inherits Rectangle { if root.show_fade_cover: Rectangle { width: 100%; height: 100%; - background: Palette.background; + background: RogPalette.background; opacity: 0.8; TouchArea { height: 100%; @@ -142,7 +143,7 @@ export component PageAnime inherits Rectangle { alignment: LayoutAlignment.start; Text { font-size: 18px; - color: Palette.control-foreground; + color: RogPalette.text-primary; horizontal-alignment: TextHorizontalAlignment.center; text: @tr("Set which builtin animations are played"); } @@ -216,7 +217,7 @@ export component PageAnime inherits Rectangle { alignment: LayoutAlignment.start; Text { font-size: 18px; - color: Palette.control-foreground; + color: RogPalette.text-primary; horizontal-alignment: TextHorizontalAlignment.center; text: @tr("Advanced Display Settings"); } diff --git a/rog-control-center/ui/pages/app_settings.slint b/rog-control-center/ui/pages/app_settings.slint index d1a5e417..07fcd338 100644 --- a/rog-control-center/ui/pages/app_settings.slint +++ b/rog-control-center/ui/pages/app_settings.slint @@ -1,5 +1,6 @@ -import { Palette } from "std-widgets.slint"; -import { SystemToggle } from "../widgets/common.slint"; +import { VerticalBox, ScrollView, HorizontalBox, Button } from "std-widgets.slint"; +import { SystemToggle, RogItem } from "../widgets/common.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; export global AppSettingsPageData { in-out property run_in_background; @@ -8,56 +9,134 @@ export global AppSettingsPageData { callback set_startup_in_background(bool); in-out property enable_tray_icon; callback set_enable_tray_icon(bool); - in-out property enable_dgpu_notifications; - callback set_enable_dgpu_notifications(bool); + + // Master notification toggle + in-out property notifications_enabled; + callback set_notifications_enabled(bool); + + // Granular notification toggles + in-out property notify_gfx_switch; + callback set_notify_gfx_switch(bool); + in-out property notify_gfx_status; + callback set_notify_gfx_status(bool); + in-out property notify_platform_profile; + callback set_notify_platform_profile(bool); } -export component PageAppSettings inherits VerticalLayout { - Rectangle { - clip: true; - // TODO: slow with border-radius - //padding only has effect on layout elements - //padding: 8px; +export component PageAppSettings inherits Rectangle { + background: RogPalette.background; + + ScrollView { + VerticalBox { + padding: 20px; + spacing: 20px; + alignment: start; - // height: parent.height - infobar.height - mainview.padding - self.padding * 2; - // TODO: border-radius: 8px; - mainview := VerticalLayout { - padding: 10px; - spacing: 10px; - SystemToggle { - text: @tr("Run in background after closing"); - checked <=> AppSettingsPageData.run_in_background; - toggled => { - AppSettingsPageData.set_run_in_background(AppSettingsPageData.run_in_background) + // General Section + VerticalBox { + spacing: 10px; + padding: 0px; + + Rectangle { + height: 30px; + background: RogPalette.control-background; + border-radius: 4px; + border-width: 1px; + border-color: RogPalette.control-border; + + Text { + x: 10px; + vertical-alignment: center; + text: "General Settings"; + color: RogPalette.accent; + font-weight: 700; + } + } + + SystemToggle { + text: @tr("Run in background after closing"); + checked <=> AppSettingsPageData.run_in_background; + toggled => { + AppSettingsPageData.set_run_in_background(AppSettingsPageData.run_in_background) + } + } + + SystemToggle { + text: @tr("Start app in background (UI closed)"); + checked <=> AppSettingsPageData.startup_in_background; + toggled => { + AppSettingsPageData.set_startup_in_background(AppSettingsPageData.startup_in_background) + } + } + + SystemToggle { + text: @tr("Enable system tray icon"); + checked <=> AppSettingsPageData.enable_tray_icon; + toggled => { + AppSettingsPageData.set_enable_tray_icon(AppSettingsPageData.enable_tray_icon) + } } } - SystemToggle { - text: @tr("Start app in background (UI closed)"); - checked <=> AppSettingsPageData.startup_in_background; - toggled => { - AppSettingsPageData.set_startup_in_background(AppSettingsPageData.startup_in_background) - } - } + // Notifications Section + VerticalBox { + spacing: 10px; + padding: 0px; - SystemToggle { - text: @tr("Enable system tray icon"); - checked <=> AppSettingsPageData.enable_tray_icon; - toggled => { - AppSettingsPageData.set_enable_tray_icon(AppSettingsPageData.enable_tray_icon) - } - } + Rectangle { + height: 30px; + background: RogPalette.control-background; + border-radius: 4px; + border-width: 1px; + border-color: RogPalette.control-border; - SystemToggle { - text: @tr("Enable dGPU notifications"); - checked <=> AppSettingsPageData.enable_dgpu_notifications; - toggled => { - AppSettingsPageData.set_enable_dgpu_notifications(AppSettingsPageData.enable_dgpu_notifications) + Text { + x: 10px; + vertical-alignment: center; + text: "Notifications"; + color: RogPalette.accent; + font-weight: 700; + } + } + + SystemToggle { + text: @tr("Enable Notifications"); + checked <=> AppSettingsPageData.notifications_enabled; + toggled => { + AppSettingsPageData.set_notifications_enabled(AppSettingsPageData.notifications_enabled) + } + } + + // Sub-toggles container + VerticalBox { + padding-left: 30px; // Indent + spacing: 10px; + visible: AppSettingsPageData.notifications_enabled; + + SystemToggle { + text: @tr("Notify on Graphics Switch"); + checked <=> AppSettingsPageData.notify_gfx_switch; + toggled => { + AppSettingsPageData.set_notify_gfx_switch(AppSettingsPageData.notify_gfx_switch) + } + } + + SystemToggle { + text: @tr("Notify on GPU Status Change"); + checked <=> AppSettingsPageData.notify_gfx_status; + toggled => { + AppSettingsPageData.set_notify_gfx_status(AppSettingsPageData.notify_gfx_status) + } + } + + SystemToggle { + text: @tr("Notify on Power Profile Change"); + checked <=> AppSettingsPageData.notify_platform_profile; + toggled => { + AppSettingsPageData.set_notify_platform_profile(AppSettingsPageData.notify_platform_profile) + } + } } - } - - Text { - text: "WIP: some features like notifications are not complete"; } } } diff --git a/rog-control-center/ui/pages/aura.slint b/rog-control-center/ui/pages/aura.slint index 71cb5281..be7f4e6b 100644 --- a/rog-control-center/ui/pages/aura.slint +++ b/rog-control-center/ui/pages/aura.slint @@ -1,5 +1,6 @@ import { SystemDropdown, RogItem, SystemToggle, SystemToggleVert } from "../widgets/common.slint"; -import { Palette, Button, ComboBox, VerticalBox, GroupBox } from "std-widgets.slint"; +import { Button, ComboBox, VerticalBox, GroupBox } from "std-widgets.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; import { StyleMetrics, Slider, HorizontalBox, TextEdit, SpinBox, LineEdit, ScrollView } from "std-widgets.slint"; import { ColourSlider } from "../widgets/colour_picker.slint"; import { AuraPageData, AuraDevType, PowerZones, LaptopAuraPower, AuraEffect } from "../types/aura_types.slint"; @@ -183,6 +184,57 @@ export component PageAura inherits Rectangle { } } + // Software Animation Controls (for Static-only keyboards) + if AuraPageData.soft_animation_available: RogItem { + min-height: 100px; + VerticalLayout { + padding: 10px; + spacing: 8px; + + Text { + text: @tr("Software Animation (Static-only keyboards)"); + font-size: 14px; + font-weight: 600; + color: RogPalette.accent; + } + + HorizontalLayout { + spacing: 20px; + + VerticalLayout { + Text { + text: @tr("Animation Mode"); + color: RogPalette.text-secondary; + } + ComboBox { + current_index <=> AuraPageData.soft_animation_mode; + current_value: AuraPageData.soft_animation_modes[self.current-index]; + model <=> AuraPageData.soft_animation_modes; + selected => { + AuraPageData.cb_soft_animation_mode(AuraPageData.soft_animation_mode); + } + } + } + + VerticalLayout { + horizontal-stretch: 1; + Text { + text: @tr("Speed: ") + Math.round(AuraPageData.soft_animation_speed) + "ms"; + color: RogPalette.text-secondary; + } + Slider { + minimum: 150; + maximum: 1000; + value <=> AuraPageData.soft_animation_speed; + released => { + AuraPageData.cb_soft_animation_speed(Math.round(AuraPageData.soft_animation_speed)); + } + } + } + } + } + } + HorizontalLayout { Button { text: @tr("Power Settings"); @@ -195,11 +247,15 @@ export component PageAura inherits Rectangle { } if root.show_fade_cover: Rectangle { - background: Palette.background; + background: RogPalette.background; opacity: 0.8; TouchArea { height: 100%; width: 100%; + clicked => { + root.show_fade_cover = false; + root.show_aura_power = false; + } } } } @@ -266,7 +322,10 @@ export component PageAura inherits Rectangle { alignment: LayoutAlignment.start; Text { - text: "TODO: In progress"; + text: "LED Power Zones (Legacy)"; + font-size: 16px; + font-weight: 600; + color: #ff0033; } for state[idx] in AuraPageData.led_power.states: old_zone := AuraPowerGroupOld { diff --git a/rog-control-center/ui/pages/fans.slint b/rog-control-center/ui/pages/fans.slint index fc3f703c..340edd6c 100644 --- a/rog-control-center/ui/pages/fans.slint +++ b/rog-control-center/ui/pages/fans.slint @@ -1,7 +1,10 @@ -import { Palette, TabWidget, Button, CheckBox } from "std-widgets.slint"; +import { Palette, TabWidget, Button, CheckBox, Slider } from "std-widgets.slint"; import { Graph, Node } from "../widgets/graph.slint"; import { SystemToggle } from "../widgets/common.slint"; import { Profile, FanType, FanPageData } from "../types/fan_types.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; + + component FanTab inherits Rectangle { in-out property enabled: false; @@ -16,10 +19,81 @@ component FanTab inherits Rectangle { in-out property <[Node]> nodes; VerticalLayout { + private property local_busy: + (root.fan_type == FanType.CPU && FanPageData.is_busy_cpu) || + (root.fan_type == FanType.GPU && FanPageData.is_busy_gpu) || + (root.fan_type == FanType.Middle && FanPageData.is_busy_mid); + + if FanPageData.show_custom_warning: Rectangle { + background: RogPalette.control-background; + border-radius: 4px; + height: 48px; + HorizontalLayout { + padding: 10px; + Text { + color: #ffd700; // Gold/Yellow + text: @tr("Zero RPM Mode Enabled: Fans will take ~25s to spin down entirely."); + vertical-alignment: TextVerticalAlignment.center; + horizontal-alignment: TextHorizontalAlignment.center; + wrap: word-wrap; + } + } + } + HorizontalLayout { + spacing: 10px; if root.tab_enabled: Graph { nodes <=> root.nodes; } + if root.tab_enabled: VerticalLayout { + width: 40px; + alignment: center; + spacing: 10px; + + Button { + text: "+"; + height: 40px; + width: 40px; + clicked => { + root.nodes = [ + { x: root.nodes[0].x, y: min(255px, root.nodes[0].y + 13px) }, + { x: root.nodes[1].x, y: min(255px, root.nodes[1].y + 13px) }, + { x: root.nodes[2].x, y: min(255px, root.nodes[2].y + 13px) }, + { x: root.nodes[3].x, y: min(255px, root.nodes[3].y + 13px) }, + { x: root.nodes[4].x, y: min(255px, root.nodes[4].y + 13px) }, + { x: root.nodes[5].x, y: min(255px, root.nodes[5].y + 13px) }, + { x: root.nodes[6].x, y: min(255px, root.nodes[6].y + 13px) }, + { x: root.nodes[7].x, y: min(255px, root.nodes[7].y + 13px) } + ]; + } + } + + Text { + text: "All"; + font-size: 10px; + horizontal-alignment: center; + color: white; + } + + Button { + text: "-"; + height: 40px; + width: 40px; + clicked => { + root.nodes = [ + { x: root.nodes[0].x, y: max(0px, root.nodes[0].y - 13px) }, + { x: root.nodes[1].x, y: max(0px, root.nodes[1].y - 13px) }, + { x: root.nodes[2].x, y: max(0px, root.nodes[2].y - 13px) }, + { x: root.nodes[3].x, y: max(0px, root.nodes[3].y - 13px) }, + { x: root.nodes[4].x, y: max(0px, root.nodes[4].y - 13px) }, + { x: root.nodes[5].x, y: max(0px, root.nodes[5].y - 13px) }, + { x: root.nodes[6].x, y: max(0px, root.nodes[6].y - 13px) }, + { x: root.nodes[7].x, y: max(0px, root.nodes[7].y - 13px) } + ]; + } + } + } + if !root.tab_enabled: Rectangle { Text { font-size: 24px; @@ -29,19 +103,20 @@ component FanTab inherits Rectangle { } HorizontalLayout { + spacing: 20px; alignment: LayoutAlignment.end; CheckBox { - text: @tr("Enabled"); + text: @tr("Enable Manual Control"); checked <=> root.enabled; - enabled <=> root.tab_enabled; + enabled: root.tab_enabled && !local_busy; toggled => { root.toggled(); } } Button { - text: @tr("Apply"); - enabled <=> root.tab_enabled; + text: local_busy ? @tr("Applying...") : @tr("Apply Curve"); + enabled: root.tab_enabled && root.enabled && !local_busy; clicked => { root.apply(); } @@ -49,7 +124,7 @@ component FanTab inherits Rectangle { Button { text: @tr("Cancel"); - enabled <=> root.tab_enabled; + enabled: root.tab_enabled && !local_busy; clicked => { root.cancel() } @@ -57,7 +132,7 @@ component FanTab inherits Rectangle { Button { text: @tr("Factory Default (all fans)"); - enabled <=> root.tab_enabled; + enabled: root.tab_enabled && !local_busy; clicked => { root.default(); } @@ -86,6 +161,9 @@ export component PageFans inherits VerticalLayout { default => { FanPageData.set_profile_default(Profile.Balanced); } + cancel => { + FanPageData.cancel(FanType.CPU, Profile.Balanced); + } } } @@ -104,6 +182,9 @@ export component PageFans inherits VerticalLayout { default => { FanPageData.set_profile_default(Profile.Balanced); } + cancel => { + FanPageData.cancel(FanType.Middle, Profile.Balanced); + } } } @@ -122,6 +203,9 @@ export component PageFans inherits VerticalLayout { default => { FanPageData.set_profile_default(Profile.Balanced); } + cancel => { + FanPageData.cancel(FanType.GPU, Profile.Balanced); + } } } } @@ -145,6 +229,9 @@ export component PageFans inherits VerticalLayout { default => { FanPageData.set_profile_default(Profile.Performance); } + cancel => { + FanPageData.cancel(FanType.CPU, Profile.Performance); + } } } @@ -163,6 +250,9 @@ export component PageFans inherits VerticalLayout { default => { FanPageData.set_profile_default(Profile.Performance); } + cancel => { + FanPageData.cancel(FanType.Middle, Profile.Performance); + } } } @@ -181,6 +271,9 @@ export component PageFans inherits VerticalLayout { default => { FanPageData.set_profile_default(Profile.Performance); } + cancel => { + FanPageData.cancel(FanType.GPU, Profile.Performance); + } } } } @@ -204,6 +297,9 @@ export component PageFans inherits VerticalLayout { default => { FanPageData.set_profile_default(Profile.Quiet); } + cancel => { + FanPageData.cancel(FanType.CPU, Profile.Quiet); + } } } @@ -222,6 +318,9 @@ export component PageFans inherits VerticalLayout { default => { FanPageData.set_profile_default(Profile.Quiet); } + cancel => { + FanPageData.cancel(FanType.Middle, Profile.Quiet); + } } } @@ -240,6 +339,9 @@ export component PageFans inherits VerticalLayout { default => { FanPageData.set_profile_default(Profile.Quiet); } + cancel => { + FanPageData.cancel(FanType.GPU, Profile.Quiet); + } } } } diff --git a/rog-control-center/ui/pages/screenpad.slint b/rog-control-center/ui/pages/screenpad.slint new file mode 100644 index 00000000..db01dc60 --- /dev/null +++ b/rog-control-center/ui/pages/screenpad.slint @@ -0,0 +1,102 @@ +import { Button, VerticalBox, Slider, Switch } from "std-widgets.slint"; +import { ScreenpadPageData } from "../types/screenpad_types.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; +import { RogItem, SystemSlider } from "../widgets/common.slint"; + +export { ScreenpadPageData } + +export component PageScreenpad inherits Rectangle { + background: RogPalette.background; + + VerticalBox { + alignment: LayoutAlignment.start; + padding: 20px; + spacing: 20px; + + Text { + text: @tr("Screenpad Controls"); + font-size: 24px; + font-weight: 700; + color: RogPalette.accent; + } + + RogItem { + HorizontalLayout { + padding: 15px; + spacing: 20px; + alignment: LayoutAlignment.space-between; + + Text { + text: @tr("Enable Screenpad"); + font-size: 16px; + vertical-alignment: TextVerticalAlignment.center; + color: RogPalette.text-primary; + } + + Switch { + checked <=> ScreenpadPageData.power; + toggled => { + ScreenpadPageData.cb_power(self.checked); + } + } + } + } + + VerticalLayout { + spacing: 15px; + + // Brightness Slider + SystemSlider { + enabled: ScreenpadPageData.power; + text: @tr("Brightness"); + minimum: 0; + maximum: 255; + value: ScreenpadPageData.brightness; + help_text: ScreenpadPageData.brightness == -1 ? @tr("Not available") : ""; + released => { + ScreenpadPageData.cb_brightness(Math.round(self.value)); + } + } + + // Gamma Slider (New) + SystemSlider { + enabled: ScreenpadPageData.power; + text: @tr("Gamma"); + minimum: 0.1; + maximum: 2.5; + value: ScreenpadPageData.gamma; + help_text: @tr("Adjust color intensity"); + released => { + ScreenpadPageData.cb_gamma(self.value); + } + } + + RogItem { + enabled: ScreenpadPageData.power; + HorizontalLayout { + padding: 15px; + spacing: 20px; + alignment: LayoutAlignment.space-between; + + Text { + text: @tr("Sync with Primary Display"); + font-size: 16px; + vertical-alignment: TextVerticalAlignment.center; + color: RogPalette.text-primary; + } + + Switch { + enabled: ScreenpadPageData.power; + checked <=> ScreenpadPageData.sync_with_primary; + toggled => { + ScreenpadPageData.cb_sync_with_primary(self.checked); + } + } + } + } + } + + // Spacer + Rectangle {} + } +} diff --git a/rog-control-center/ui/pages/slash.slint b/rog-control-center/ui/pages/slash.slint new file mode 100644 index 00000000..fc44d933 --- /dev/null +++ b/rog-control-center/ui/pages/slash.slint @@ -0,0 +1,114 @@ +import { SystemToggle, SystemSlider, SystemDropdown, RogItem } from "../widgets/common.slint"; +import { VerticalBox, ScrollView, GroupBox } from "std-widgets.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; +import { SlashPageData } from "../types/slash_types.slint"; + +export { SlashPageData } + +export component PageSlash inherits Rectangle { + background: RogPalette.background; + + ScrollView { + VerticalBox { + padding: 20px; + spacing: 20px; + alignment: start; + + // Header + Rectangle { + height: 40px; + background: RogPalette.control-background; + border-radius: RogPalette.border-radius; + border-width: 1px; + border-color: RogPalette.control-border; + + Text { + text: @tr("Slash Lighting Control"); + color: RogPalette.accent; + font-size: 18px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + // Main Control + RogItem { + VerticalBox { + SystemToggle { + text: @tr("Enable Slash Lighting"); + checked <=> SlashPageData.enabled; + toggled => { SlashPageData.cb_enabled(self.checked); } + } + + SystemDropdown { + text: @tr("Lighting Mode"); + model <=> SlashPageData.modes; + current_index <=> SlashPageData.mode_index; + current_value: SlashPageData.modes[SlashPageData.mode_index]; + selected => { + SlashPageData.cb_mode_index(self.current_index); + } + } + + SystemSlider { + title: @tr("Brightness"); + text: @tr("Brightness"); + value <=> SlashPageData.brightness; + minimum: 0; + maximum: 255; + help_text: ""; + released(val) => { SlashPageData.cb_brightness(val); } + } + + SystemSlider { + title: @tr("Interval / Speed"); + text: @tr("Interval / Speed"); + value <=> SlashPageData.interval; + minimum: 0; + maximum: 255; + help_text: ""; + released(val) => { SlashPageData.cb_interval(val); } + } + } + } + + // Behaviors + GroupBox { + title: @tr("Behavior Settings"); + VerticalBox { + SystemToggle { + text: @tr("Show Battery Warning"); + checked <=> SlashPageData.show_battery_warning; + toggled => { SlashPageData.cb_show_battery_warning(self.checked); } + } + SystemToggle { + text: @tr("Active on Battery"); + checked <=> SlashPageData.show_on_battery; + toggled => { SlashPageData.cb_show_on_battery(self.checked); } + } + SystemToggle { + text: @tr("Active on Boot"); + checked <=> SlashPageData.show_on_boot; + toggled => { SlashPageData.cb_show_on_boot(self.checked); } + } + SystemToggle { + text: @tr("Active on Shutdown"); + checked <=> SlashPageData.show_on_shutdown; + toggled => { SlashPageData.cb_show_on_shutdown(self.checked); } + } + SystemToggle { + text: @tr("Active on Sleep"); + checked <=> SlashPageData.show_on_sleep; + toggled => { SlashPageData.cb_show_on_sleep(self.checked); } + } + SystemToggle { + text: @tr("Active when Lid Closed"); + checked <=> SlashPageData.show_on_lid_closed; + toggled => { SlashPageData.cb_show_on_lid_closed(self.checked); } + } + } + } + } + } +} diff --git a/rog-control-center/ui/pages/supergfx.slint b/rog-control-center/ui/pages/supergfx.slint new file mode 100644 index 00000000..1cf84b0d --- /dev/null +++ b/rog-control-center/ui/pages/supergfx.slint @@ -0,0 +1,73 @@ +import { SystemDropdown, RogItem } from "../widgets/common.slint"; +import { VerticalBox, ScrollView, HorizontalBox } from "std-widgets.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; +import { SupergfxPageData } from "../types/supergfx_types.slint"; + +export { SupergfxPageData } + +export component PageSupergfx inherits Rectangle { + background: RogPalette.background; + + ScrollView { + VerticalBox { + padding: 20px; + spacing: 20px; + alignment: start; + + // Header + Rectangle { + height: 40px; + background: RogPalette.control-background; + border-radius: RogPalette.border-radius; + border-width: 1px; + border-color: RogPalette.control-border; + + Text { + text: @tr("Graphics Control (supergfx)"); + color: RogPalette.accent; + font-size: 18px; + font-weight: 700; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + RogItem { + HorizontalBox { + Text { + text: @tr("Vendor: ") + SupergfxPageData.vendor; + color: RogPalette.text-secondary; + vertical-alignment: center; + } + } + } + + // Main Control + RogItem { + VerticalBox { + Text { + text: @tr("Current Mode: ") + SupergfxPageData.current_mode; + color: RogPalette.text-primary; + } + + SystemDropdown { + text: @tr("Graphics Mode"); + model <=> SupergfxPageData.supported_modes; + current_index <=> SupergfxPageData.selected_index; + current_value: SupergfxPageData.supported_modes[SupergfxPageData.selected_index]; + selected => { + SupergfxPageData.set_mode(self.current_value); + } + } + + Text { + text: @tr("Note: Changing modes requires a logout."); + color: RogPalette.text-secondary; + font-size: 12px; + wrap: word-wrap; + } + } + } + } + } +} diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint index 1e8a43d2..2588fa4d 100644 --- a/rog-control-center/ui/pages/system.slint +++ b/rog-control-center/ui/pages/system.slint @@ -1,5 +1,6 @@ import { SystemSlider, SystemDropdown, SystemToggle, SystemToggleInt, RogItem } from "../widgets/common.slint"; import { Palette, HorizontalBox , VerticalBox, ScrollView, Slider, Button, Switch, ComboBox, GroupBox, StandardButton} from "std-widgets.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; export struct AttrMinMax { min: int, @@ -66,13 +67,6 @@ export global SystemPageData { in-out property mini_led_mode; callback cb_mini_led_mode(int); - 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; @@ -157,18 +151,22 @@ export component PageSystem inherits Rectangle { ScrollView { VerticalLayout { padding: 10px; + padding-top: 40px; + padding-bottom: 40px; spacing: 10px; alignment: LayoutAlignment.start; Rectangle { - background: Palette.alternate-background; - border-color: Palette.accent-background; - border-width: 3px; - border-radius: 10px; - height: 40px; + background: RogPalette.control-background; + border-color: RogPalette.control-border; + border-width: 1px; + border-radius: 8px; + height: 46px; Text { font-size: 18px; - color: Palette.control-foreground; + color: RogPalette.accent; horizontal-alignment: TextHorizontalAlignment.center; + vertical-alignment: TextVerticalAlignment.center; + font-weight: 700; text: @tr("Power settings"); } } @@ -207,62 +205,18 @@ export component PageSystem inherits Rectangle { } } - if SystemPageData.screenpad_brightness != -1: RogItem { - HorizontalLayout { - padding-left: 10px; - padding-right: 20px; - HorizontalLayout { - width: 38%; - alignment: LayoutAlignment.space-between; - padding-right: 15px; - Text { - font-size: 16px; - vertical-alignment: TextVerticalAlignment.center; - color: Palette.control-foreground; - text: @tr("Screenpad brightness"); - } - } - - HorizontalLayout { - width: 38%; - alignment: LayoutAlignment.stretch; - screen_bright := Slider { - enabled: true; - minimum: 0; - maximum: 100; - value: SystemPageData.screenpad_brightness; - released(value) => { - // SystemPageData.screenpad_brightness = self.value; - SystemPageData.cb_screenpad_brightness(Math.floor(self.value)); - } - } - } - - HorizontalLayout { - width: 20%; - padding-left: 10px; - alignment: LayoutAlignment.stretch; - Switch { - text: @tr("Sync with primary"); - checked <=> SystemPageData.screenpad_sync_with_primary; - toggled => { - SystemPageData.cb_screenpad_sync_with_primary(self.checked); - } - } - } - } - } - Rectangle { - background: Palette.alternate-background; - border-color: Palette.accent-background; - border-width: 3px; - border-radius: 10px; - height: 40px; + background: RogPalette.control-background; + border-color: RogPalette.control-border; + border-width: 1px; + border-radius: 8px; + height: 46px; Text { font-size: 18px; - color: Palette.control-foreground; + color: RogPalette.accent; horizontal-alignment: TextHorizontalAlignment.center; + vertical-alignment: TextVerticalAlignment.center; + font-weight: 700; text: @tr("Armoury settings"); } } @@ -377,6 +331,7 @@ export component PageSystem inherits Rectangle { Text { font-size: 16px; text: @tr("ppt_warning" => "The following settings are not applied until the toggle is enabled."); + color: RogPalette.text-primary; } } @@ -545,7 +500,7 @@ export component PageSystem inherits Rectangle { if root.show_fade_cover: Rectangle { width: 100%; height: 100%; - background: Palette.background; + background: RogPalette.background; opacity: 0.9; TouchArea { height: 100%; @@ -578,6 +533,7 @@ export component PageSystem inherits Rectangle { horizontal-alignment: TextHorizontalAlignment.center; vertical-alignment: TextVerticalAlignment.center; text: @tr("Energy Performance Preference linked to Throttle Policy"); + color: RogPalette.text-primary; } SystemToggle { @@ -628,6 +584,7 @@ export component PageSystem inherits Rectangle { horizontal-alignment: TextHorizontalAlignment.center; vertical-alignment: TextVerticalAlignment.center; text: @tr("Throttle Policy for power state"); + color: RogPalette.text-primary; } HorizontalLayout { diff --git a/rog-control-center/ui/themes/rog_theme.slint b/rog-control-center/ui/themes/rog_theme.slint new file mode 100644 index 00000000..692e0674 --- /dev/null +++ b/rog-control-center/ui/themes/rog_theme.slint @@ -0,0 +1,13 @@ +export global RogPalette { + out property background: #0a0a0a; + out property alternate-background: #111111; + out property control-background: #1e1e1e; + out property control-border: #333333; + out property control-border-hover: #555555; + out property control-border-checked: #ff0033; // ROG Red + out property text-primary: #ffffff; + out property text-secondary: #aaaaaa; + out property accent: #ff0033; + out property accent-hover: #d60000; + out property border-radius: 4px; +} diff --git a/rog-control-center/ui/types/aura_types.slint b/rog-control-center/ui/types/aura_types.slint index 78df8330..3bcb6e7e 100644 --- a/rog-control-center/ui/types/aura_types.slint +++ b/rog-control-center/ui/types/aura_types.slint @@ -8,6 +8,13 @@ export enum AuraDevType { AnimeOrSlash, } +// Software animation modes for keyboards that only support Static +export enum SoftAnimationMode { + None, + Rainbow, + ColorCycle, +} + export struct AuraEffect { /// The effect type mode: int, @@ -166,4 +173,16 @@ export global AuraPageData { }] }; callback cb_led_power(LaptopAuraPower); + + // Software animation properties (for Static-only keyboards) + in-out property <[string]> soft_animation_modes: [ + @tr("Animation mode" => "None"), + @tr("Animation mode" => "Rainbow"), + @tr("Animation mode" => "Color Cycle"), + ]; + in-out property soft_animation_mode: 0; + in-out property soft_animation_speed: 200; // ms between updates + in-out property soft_animation_available: false; // Set true when only Static mode is supported + callback cb_soft_animation_mode(int); + callback cb_soft_animation_speed(int); } diff --git a/rog-control-center/ui/types/fan_types.slint b/rog-control-center/ui/types/fan_types.slint index 5577f3b6..2bd8e365 100644 --- a/rog-control-center/ui/types/fan_types.slint +++ b/rog-control-center/ui/types/fan_types.slint @@ -35,314 +35,64 @@ export global FanPageData { in-out property quiet_gpu_enabled: true; in-out property quiet_mid_enabled: false; + in-out property is_busy_cpu: false; + in-out property is_busy_gpu: false; + in-out property is_busy_mid: false; + in-out property show_custom_warning: false; + callback set_fan_data(FanType, Profile, bool, [Node]); callback set_profile_default(Profile); + callback set_is_busy(FanType, bool); + // Last applied cache for Cancel button + in-out property <[Node]> last_applied_cpu_balanced: []; + in-out property <[Node]> last_applied_gpu_balanced: []; + in-out property <[Node]> last_applied_mid_balanced: []; + in-out property <[Node]> last_applied_cpu_performance: []; + in-out property <[Node]> last_applied_gpu_performance: []; + in-out property <[Node]> last_applied_mid_performance: []; + in-out property <[Node]> last_applied_cpu_quiet: []; + in-out property <[Node]> last_applied_gpu_quiet: []; + in-out property <[Node]> last_applied_mid_quiet: []; + + callback cancel(FanType, Profile); in-out property <[Node]> balanced_cpu: [ - { - x: 10px, - y: 10px, - }, - { - x: 40px, - y: 30px, - }, - { - x: 50px, - y: 50px, - }, - { - x: 55px, - y: 50px, - }, - { - x: 60px, - y: 60px, - }, - { - x: 65px, - y: 70px, - }, - { - x: 70px, - y: 80px, - }, - { - x: 90px, - y: 100px, - }, + { x: 30px, y: 0px }, { x: 40px, y: 0px }, { x: 50px, y: 0px }, { x: 60px, y: 0px }, + { x: 70px, y: 25px }, { x: 80px, y: 55px }, { x: 90px, y: 85px }, { x: 98px, y: 100px }, ]; in-out property <[Node]> balanced_mid: [ - { - x: 10px, - y: 10px, - }, - { - x: 40px, - y: 30px, - }, - { - x: 50px, - y: 50px, - }, - { - x: 55px, - y: 50px, - }, - { - x: 60px, - y: 60px, - }, - { - x: 65px, - y: 70px, - }, - { - x: 70px, - y: 80px, - }, - { - x: 90px, - y: 100px, - }, + { x: 30px, y: 0px }, { x: 40px, y: 0px }, { x: 50px, y: 0px }, { x: 60px, y: 0px }, + { x: 70px, y: 25px }, { x: 80px, y: 55px }, { x: 90px, y: 85px }, { x: 98px, y: 100px }, ]; in-out property <[Node]> balanced_gpu: [ - { - x: 10px, - y: 10px, - }, - { - x: 40px, - y: 30px, - }, - { - x: 50px, - y: 50px, - }, - { - x: 55px, - y: 50px, - }, - { - x: 60px, - y: 60px, - }, - { - x: 65px, - y: 70px, - }, - { - x: 70px, - y: 80px, - }, - { - x: 90px, - y: 100px, - }, + { x: 30px, y: 0px }, { x: 40px, y: 0px }, { x: 50px, y: 0px }, { x: 60px, y: 0px }, + { x: 70px, y: 25px }, { x: 80px, y: 55px }, { x: 90px, y: 85px }, { x: 98px, y: 100px }, ]; + in-out property <[Node]> performance_cpu: [ - { - x: 10px, - y: 10px, - }, - { - x: 40px, - y: 30px, - }, - { - x: 50px, - y: 50px, - }, - { - x: 55px, - y: 50px, - }, - { - x: 60px, - y: 60px, - }, - { - x: 65px, - y: 70px, - }, - { - x: 70px, - y: 80px, - }, - { - x: 90px, - y: 100px, - }, + { x: 30px, y: 0px }, { x: 40px, y: 10px }, { x: 50px, y: 30px }, { x: 60px, y: 50px }, + { x: 70px, y: 70px }, { x: 80px, y: 85px }, { x: 90px, y: 95px }, { x: 98px, y: 100px }, ]; in-out property <[Node]> performance_mid: [ - { - x: 10px, - y: 10px, - }, - { - x: 40px, - y: 30px, - }, - { - x: 50px, - y: 50px, - }, - { - x: 55px, - y: 50px, - }, - { - x: 60px, - y: 60px, - }, - { - x: 65px, - y: 70px, - }, - { - x: 70px, - y: 80px, - }, - { - x: 90px, - y: 100px, - }, + { x: 30px, y: 0px }, { x: 40px, y: 10px }, { x: 50px, y: 30px }, { x: 60px, y: 50px }, + { x: 70px, y: 70px }, { x: 80px, y: 85px }, { x: 90px, y: 95px }, { x: 98px, y: 100px }, ]; in-out property <[Node]> performance_gpu: [ - { - x: 10px, - y: 10px, - }, - { - x: 40px, - y: 30px, - }, - { - x: 50px, - y: 50px, - }, - { - x: 55px, - y: 50px, - }, - { - x: 60px, - y: 60px, - }, - { - x: 65px, - y: 70px, - }, - { - x: 70px, - y: 80px, - }, - { - x: 90px, - y: 100px, - }, + { x: 30px, y: 0px }, { x: 40px, y: 10px }, { x: 50px, y: 30px }, { x: 60px, y: 50px }, + { x: 70px, y: 70px }, { x: 80px, y: 85px }, { x: 90px, y: 95px }, { x: 98px, y: 100px }, ]; + in-out property <[Node]> quiet_cpu: [ - { - x: 10px, - y: 10px, - }, - { - x: 40px, - y: 30px, - }, - { - x: 50px, - y: 50px, - }, - { - x: 55px, - y: 50px, - }, - { - x: 60px, - y: 60px, - }, - { - x: 65px, - y: 70px, - }, - { - x: 70px, - y: 80px, - }, - { - x: 90px, - y: 100px, - }, + { x: 30px, y: 0px }, { x: 40px, y: 0px }, { x: 50px, y: 0px }, { x: 60px, y: 0px }, + { x: 70px, y: 20px }, { x: 80px, y: 40px }, { x: 90px, y: 70px }, { x: 98px, y: 90px }, ]; in-out property <[Node]> quiet_mid: [ - { - x: 10px, - y: 10px, - }, - { - x: 40px, - y: 30px, - }, - { - x: 50px, - y: 50px, - }, - { - x: 55px, - y: 50px, - }, - { - x: 60px, - y: 60px, - }, - { - x: 65px, - y: 70px, - }, - { - x: 70px, - y: 80px, - }, - { - x: 90px, - y: 100px, - }, + { x: 30px, y: 0px }, { x: 40px, y: 0px }, { x: 50px, y: 0px }, { x: 60px, y: 0px }, + { x: 70px, y: 20px }, { x: 80px, y: 40px }, { x: 90px, y: 70px }, { x: 98px, y: 90px }, ]; in-out property <[Node]> quiet_gpu: [ - { - x: 10px, - y: 10px, - }, - { - x: 40px, - y: 30px, - }, - { - x: 50px, - y: 50px, - }, - { - x: 55px, - y: 50px, - }, - { - x: 60px, - y: 60px, - }, - { - x: 65px, - y: 70px, - }, - { - x: 70px, - y: 80px, - }, - { - x: 90px, - y: 100px, - }, + { x: 30px, y: 0px }, { x: 40px, y: 0px }, { x: 50px, y: 0px }, { x: 60px, y: 0px }, + { x: 70px, y: 20px }, { x: 80px, y: 40px }, { x: 90px, y: 70px }, { x: 98px, y: 90px }, ]; function set_fan(profile: Profile, fan: FanType, data: [Node]) { diff --git a/rog-control-center/ui/types/screenpad_types.slint b/rog-control-center/ui/types/screenpad_types.slint new file mode 100644 index 00000000..57af665b --- /dev/null +++ b/rog-control-center/ui/types/screenpad_types.slint @@ -0,0 +1,13 @@ +import { RogPalette } from "../themes/rog_theme.slint"; + +export global ScreenpadPageData { + in-out property brightness: -1; + in-out property gamma: 1.0; + in-out property sync_with_primary: false; + in-out property power: true; + + callback cb_brightness(int); + callback cb_gamma(float); + callback cb_sync_with_primary(bool); + callback cb_power(bool); +} diff --git a/rog-control-center/ui/types/slash_types.slint b/rog-control-center/ui/types/slash_types.slint new file mode 100644 index 00000000..f7b03741 --- /dev/null +++ b/rog-control-center/ui/types/slash_types.slint @@ -0,0 +1,49 @@ +export global SlashPageData { + in-out property enabled; + callback cb_enabled(bool); + + in-out property brightness; + callback cb_brightness(float); + + in-out property interval; + callback cb_interval(float); + + in-out property <[string]> modes: [ + @tr("Static"), + @tr("Bounce"), + @tr("Slash"), + @tr("Loading"), + @tr("BitStream"), + @tr("Transmission"), + @tr("Flow"), + @tr("Flux"), + @tr("Phantom"), + @tr("Spectrum"), + @tr("Hazard"), + @tr("Interfacing"), + @tr("Ramp"), + @tr("GameOver"), + @tr("Start"), + @tr("Buzzer"), + ]; + in-out property mode_index; + callback cb_mode_index(int); + + in-out property show_battery_warning; + callback cb_show_battery_warning(bool); + + in-out property show_on_battery; + callback cb_show_on_battery(bool); + + in-out property show_on_boot; + callback cb_show_on_boot(bool); + + in-out property show_on_shutdown; + callback cb_show_on_shutdown(bool); + + in-out property show_on_sleep; + callback cb_show_on_sleep(bool); + + in-out property show_on_lid_closed; + callback cb_show_on_lid_closed(bool); +} diff --git a/rog-control-center/ui/types/supergfx_types.slint b/rog-control-center/ui/types/supergfx_types.slint new file mode 100644 index 00000000..24869db0 --- /dev/null +++ b/rog-control-center/ui/types/supergfx_types.slint @@ -0,0 +1,10 @@ +export global SupergfxPageData { + in-out property current_mode: "Hybrid"; + in-out property <[string]> supported_modes: ["Hybrid", "Integrated"]; + in-out property selected_index: 0; + + in-out property vendor: "Unknown"; + + callback set_mode(string); + callback refresh(); +} diff --git a/rog-control-center/ui/widgets/aura_power.slint b/rog-control-center/ui/widgets/aura_power.slint index a177db03..c9c2ea51 100644 --- a/rog-control-center/ui/widgets/aura_power.slint +++ b/rog-control-center/ui/widgets/aura_power.slint @@ -1,12 +1,14 @@ -import { Palette, VerticalBox, HorizontalBox, GroupBox } from "std-widgets.slint"; +import { VerticalBox, HorizontalBox, GroupBox } from "std-widgets.slint"; import { SystemToggleVert, SystemDropdown } from "common.slint"; import { PowerZones } from "../types/aura_types.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; export component AuraPowerGroup inherits Rectangle { min-width: row.min-width; - border-radius: 20px; - background: Palette.alternate-background; - opacity: 0.9; + border-radius: 8px; + background: RogPalette.control-background; + border-width: 1px; + border-color: RogPalette.control-border; in-out property group-title; in-out property boot_checked; in-out property awake_checked; @@ -20,7 +22,7 @@ export component AuraPowerGroup inherits Rectangle { spacing: 10px; Text { font-size: 18px; - color: Palette.alternate-foreground; + color: RogPalette.text-primary; horizontal-alignment: TextHorizontalAlignment.center; text <=> root.group-title; } @@ -72,9 +74,10 @@ export component AuraPowerGroup inherits Rectangle { export component AuraPowerGroupOld inherits Rectangle { min-width: row.min-width; - border-radius: 20px; - background: Palette.alternate-background; - opacity: 0.9; + border-radius: 8px; + background: RogPalette.control-background; + border-width: 1px; + border-color: RogPalette.control-border; in-out property current_zone; in-out property <[int]> zones; in-out property <[string]> zone_strings; @@ -90,7 +93,7 @@ export component AuraPowerGroupOld inherits Rectangle { spacing: 10px; Text { font-size: 18px; - color: Palette.alternate-foreground; + color: RogPalette.text-primary; horizontal-alignment: TextHorizontalAlignment.center; text <=> root.group-title; } diff --git a/rog-control-center/ui/widgets/common.slint b/rog-control-center/ui/widgets/common.slint index 300c3bc0..bacc90a3 100644 --- a/rog-control-center/ui/widgets/common.slint +++ b/rog-control-center/ui/widgets/common.slint @@ -1,12 +1,15 @@ -import { Palette, VerticalBox , StandardButton, Button, HorizontalBox, ComboBox, Switch, Slider} from "std-widgets.slint"; +import { VerticalBox , StandardButton, Button, HorizontalBox, ComboBox, Switch, Slider} from "std-widgets.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; export component RogItem inherits Rectangle { - background: Palette.control-background; - border-color: Palette.border; - border-width: 3px; - border-radius: 10px; + in property enabled: true; + background: root.enabled ? RogPalette.control-background : RogPalette.control-background.darker(0.5); + border-color: root.enabled ? RogPalette.control-border : RogPalette.control-border.darker(0.3); + border-width: 1px; // Thinner border for modern look + border-radius: RogPalette.border-radius; min-height: 48px; max-height: 56px; + opacity: root.enabled ? 1.0 : 0.6; } export component SystemSlider inherits RogItem { @@ -18,7 +21,6 @@ export component SystemSlider inherits RogItem { callback released(float); in property help_text; - in property enabled: true; in property has_reset: false; callback cb_do_reset(); @@ -32,7 +34,7 @@ export component SystemSlider inherits RogItem { Text { font-size: 16px; vertical-alignment: TextVerticalAlignment.center; - color: Palette.control-foreground; + color: RogPalette.text-primary; text: root.text; } @@ -40,7 +42,7 @@ export component SystemSlider inherits RogItem { font-size: 16px; horizontal-alignment: TextHorizontalAlignment.right; vertical-alignment: TextVerticalAlignment.center; - color: Palette.control-foreground; + color: RogPalette.accent; text: "\{Math.round(root.value)}"; } } @@ -64,10 +66,11 @@ export component SystemSlider inherits RogItem { y: help.y - self.height + help.height - 10px; Rectangle { drop-shadow-blur: 10px; - drop-shadow-color: black; - border-radius: 10px; - border-color: Palette.accent-background; - background: Palette.background; + drop-shadow-color: Colors.black; + border-radius: RogPalette.border-radius; + border-width: 1px; + border-color: RogPalette.accent; + background: RogPalette.control-background; Dialog { title: root.title; VerticalBox { @@ -77,12 +80,12 @@ export component SystemSlider inherits RogItem { wrap: TextWrap.word-wrap; horizontal-alignment: TextHorizontalAlignment.center; text: root.title; + color: RogPalette.text-primary; } Rectangle { height: 1px; - border-color: black; - border-width: 1px; + background: RogPalette.control-border; } Text { @@ -90,6 +93,7 @@ export component SystemSlider inherits RogItem { font-size: 16px; wrap: TextWrap.word-wrap; text: root.help_text; + color: RogPalette.text-secondary; } } @@ -114,16 +118,18 @@ export component SystemSlider inherits RogItem { y: reset.y - self.height + reset.height; Rectangle { drop-shadow-blur: 10px; - drop-shadow-color: black; - border-radius: 10px; - border-color: Palette.accent-background; - background: Palette.background; + drop-shadow-color: Colors.black; + border-radius: RogPalette.border-radius; + border-width: 1px; + border-color: RogPalette.accent; + background: RogPalette.control-background; Dialog { Text { max-width: 420px; font-size: 16px; wrap: TextWrap.word-wrap; text: @tr("confirm_reset" => "Are you sure you want to reset this?"); + color: RogPalette.text-primary; } StandardButton { @@ -164,7 +170,7 @@ export component SystemToggle inherits RogItem { Text { font-size: 16px; vertical-alignment: TextVerticalAlignment.center; - color: Palette.control-foreground; + color: RogPalette.text-primary; text: root.text; } } @@ -195,7 +201,7 @@ export component SystemToggleInt inherits RogItem { Text { font-size: 16px; vertical-alignment: TextVerticalAlignment.center; - color: Palette.control-foreground; + color: RogPalette.text-primary; text: root.text; } } @@ -226,7 +232,7 @@ export component SystemToggleVert inherits RogItem { font-size: 16px; vertical-alignment: TextVerticalAlignment.bottom; horizontal-alignment: TextHorizontalAlignment.center; - color: Palette.control-foreground; + color: RogPalette.text-primary; text: root.text; } @@ -256,7 +262,7 @@ export component SystemDropdown inherits RogItem { Text { font-size: 16px; vertical-alignment: TextVerticalAlignment.center; - color: Palette.control-foreground; + color: RogPalette.text-primary; text: root.text; } } @@ -288,9 +294,9 @@ export component PopupNotification { height: root.height; // TODO: add properties to display Rectangle { - border-width: 2px; - border-color: Palette.accent-background; - background: Palette.background; + border-width: 1px; + border-color: RogPalette.accent; + background: RogPalette.background; // TODO: drop shadows slow // drop-shadow-offset-x: 7px; // drop-shadow-offset-y: 7px; @@ -302,14 +308,14 @@ export component PopupNotification { alignment: start; Text { text: heading; - color: Palette.control-foreground; + color: RogPalette.text-primary; font-size: 32px; font-weight: 900; } Text { text: content; - color: Palette.control-foreground; + color: RogPalette.text-secondary; font-size: 18px; } } diff --git a/rog-control-center/ui/widgets/graph.slint b/rog-control-center/ui/widgets/graph.slint index f540436e..208bf7b7 100644 --- a/rog-control-center/ui/widgets/graph.slint +++ b/rog-control-center/ui/widgets/graph.slint @@ -1,4 +1,5 @@ import { Palette } from "std-widgets.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; export struct Node { x: length, y: length} @@ -44,7 +45,7 @@ export component Graph inherits Rectangle { for n in 11: Path { viewbox-width: self.width / 1px; viewbox-height: self.height / 1px; - stroke: Palette.alternate-foreground.darker(200%); + stroke: RogPalette.control-border; stroke-width: 1px; MoveTo { x: scale_x_to_graph(n * 10px) / 1px; @@ -60,7 +61,7 @@ export component Graph inherits Rectangle { } for n in 11: Text { - color: Palette.accent-background; + color: RogPalette.text-secondary; font-size <=> root.axis_font_size; text: "\{n * 10}c"; x: scale_x_to_graph(n * 10px) - self.width / 3; @@ -70,7 +71,7 @@ export component Graph inherits Rectangle { for n in 11: Path { viewbox-width: self.width / 1px; viewbox-height: self.height / 1px; - stroke: Palette.alternate-foreground.darker(200%); + stroke: RogPalette.control-border; stroke-width: 1px; MoveTo { x: 0; @@ -86,7 +87,7 @@ export component Graph inherits Rectangle { } for n in 11: Text { - color: Palette.accent-background; + color: RogPalette.text-secondary; font-size <=> root.axis_font_size; text: "\{n * 10}%"; x: - self.width; @@ -97,7 +98,7 @@ export component Graph inherits Rectangle { if idx + 1 != nodes.length: Path { viewbox-width: self.width / 1px; viewbox-height: self.height / 1px; - stroke: Palette.control-foreground; + stroke: RogPalette.accent; stroke-width: 2px; MoveTo { x: scale_x_to_graph(nodes[idx].x) / 1px; @@ -114,19 +115,19 @@ export component Graph inherits Rectangle { for n[idx] in nodes: Rectangle { states [ pressed when touch.pressed: { - point.background: Palette.selection-background; - tip.background: Palette.selection-background; + point.background: RogPalette.accent; + tip.background: RogPalette.accent; tip.opacity: 1.0; } hover when touch.has-hover: { - point.background: Palette.accent-background; - tip.background: Palette.accent-background; + point.background: RogPalette.accent; + tip.background: RogPalette.accent; tip.opacity: 1.0; } ] // point := Rectangle { - background: Palette.control-foreground; + background: RogPalette.text-primary; x: scale_x_to_graph(n.x) - self.width / 2; y: graph.height - scale_y_to_graph(n.y) - self.height / 2; width: 18px; @@ -142,10 +143,14 @@ export component Graph inherits Rectangle { } else if n.x + scale_x_to_node(self.mouse-x - self.pressed-x) < nodes[idx - 1].x { n.x = nodes[idx - 1].x + pad; } + + // Y-Axis: Monotonic Non-Decreasing if n.y + scale_y_to_node(self.height - self.mouse-y - self.pressed-y) > nodes[idx + 1].y { - n.y = nodes[idx + 1].y - pad; + n.y = nodes[idx + 1].y; // Allow equality } else if n.y + scale_y_to_node(self.height - self.mouse-y - self.pressed-y) < nodes[idx - 1].y { - n.y = nodes[idx - 1].y + pad; + n.y = nodes[idx - 1].y; // Allow equality + } else if n.y + scale_y_to_node(self.height - self.mouse-y - self.pressed-y) < 0.0 { + n.y = 0px; } } else if idx == 0 { if n.x + scale_x_to_node(self.mouse-x - self.pressed-x) < 0.0 { @@ -153,10 +158,12 @@ export component Graph inherits Rectangle { } else if n.x + scale_x_to_node(self.mouse-x - self.pressed-x) > nodes[idx + 1].x { n.x = nodes[idx + 1].x - pad; } + + // Y-Axis: <= Next Point if n.y - scale_y_to_node(self.mouse-y - self.pressed-y) < 0.0 { - n.y = 1px; + n.y = 0px; // Allow 0 RPM } else if n.y + scale_y_to_node(self.height - self.mouse-y - self.pressed-y) > nodes[idx + 1].y { - n.y = nodes[idx + 1].y - pad; + n.y = nodes[idx + 1].y; // Allow equality } } else if idx == nodes.length - 1 { if n.x + scale_x_to_node(self.mouse-x - self.pressed-x) > scale_x_to_node(graph.width) { @@ -164,10 +171,14 @@ export component Graph inherits Rectangle { } else if n.x + scale_x_to_node(self.mouse-x - self.pressed-x) < nodes[idx - 1].x { n.x = nodes[idx - 1].x + pad; } + + // Y-Axis: >= Previous Point if n.y - scale_y_to_node(self.mouse-y - self.pressed-y) > scale_y_to_node(graph.height) { - n.y = scale_y_to_node(graph.height - 1px); + n.y = scale_y_to_node(graph.height); } else if n.y + scale_y_to_node(self.height - self.mouse-y - self.pressed-y) < nodes[idx - 1].y { - n.y = nodes[idx - 1].y + pad; + n.y = nodes[idx - 1].y; // Allow equality + } else if n.y + scale_y_to_node(self.height - self.mouse-y - self.pressed-y) < 0.0 { + n.y = 0px; } } } @@ -189,7 +200,7 @@ export component Graph inherits Rectangle { } tip := Rectangle { - background: Palette.control-foreground; + background: RogPalette.control-background; opacity: 0.3; x: final_x_pos(); y: final_y_pos(); @@ -224,7 +235,7 @@ export component Graph inherits Rectangle { } // label := Text { - color: Palette.accent-foreground; + color: RogPalette.text-primary; font-size: 16px; text: "\{Math.floor(n.x / 1px)}c, \{fan_pct()}%"; } diff --git a/rog-control-center/ui/widgets/sidebar.slint b/rog-control-center/ui/widgets/sidebar.slint index b3574a3f..1784ea77 100644 --- a/rog-control-center/ui/widgets/sidebar.slint +++ b/rog-control-center/ui/widgets/sidebar.slint @@ -1,7 +1,8 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: MIT -import { Palette, HorizontalBox, VerticalBox } from "std-widgets.slint"; +import { HorizontalBox, VerticalBox } from "std-widgets.slint"; +import { RogPalette } from "../themes/rog_theme.slint"; component SideBarItem inherits Rectangle { // padding only has effect on layout elements @@ -28,12 +29,21 @@ component SideBarItem inherits Rectangle { ] state := Rectangle { opacity: 0; - border-width: 2px; - border-radius: 10px; - border-color: Palette.accent-background; - background: Palette.alternate-background; + border-width: 0px; // Modern look: no full border, maybe just a left bar? + // Or keep the ROG style border + border-color: RogPalette.accent; + background: root.selected ? RogPalette.control-background : RogPalette.alternate-background; + + // Add a red indicator line on the left for selected items + Rectangle { + x: 0; + width: 4px; + height: 100%; + background: root.selected ? RogPalette.accent : Colors.transparent; + } + animate opacity { duration: 150ms; } - animate border-width { duration: 150ms; } + // animate border-width { duration: 150ms; } height: l.preferred-height; } @@ -41,9 +51,10 @@ component SideBarItem inherits Rectangle { y: (parent.height - self.height) / 2; spacing: 0px; label := Text { - color: Palette.foreground; + color: root.selected ? RogPalette.accent : RogPalette.text-primary; vertical-alignment: center; font-size: 14px; + font-weight: root.selected ? 700 : 400; } } @@ -66,10 +77,10 @@ export component SideBar inherits Rectangle { accessible-role: tab; accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current-item; Rectangle { - border-width: 2px; - border-color: Palette.accent-background; + border-width: 0px; + // border-color: RogPalette.accent; border-radius: 0px; - background: Palette.background.darker(0.2); + background: RogPalette.alternate-background; // Darker sidebar fs := FocusScope { key-pressed(event) => { if (event.text == "\n") { @@ -104,12 +115,19 @@ export component SideBar inherits Rectangle { spacing: 4px; alignment: start; label := Text { - font-size: 16px; + font-size: 24px; // Larger brand text + font-weight: 800; horizontal-alignment: center; + color: RogPalette.accent; // ROG Red brand text + } + + // Spacer after brand text + Rectangle { + height: 20px; } navigation := VerticalLayout { - spacing: -6px; + spacing: 4px; // Spacing between items alignment: start; vertical-stretch: 0; for item[index] in root.model: SideBarItem { diff --git a/rog-control-center/ui/widgets/status_bar.slint b/rog-control-center/ui/widgets/status_bar.slint new file mode 100644 index 00000000..76874de4 --- /dev/null +++ b/rog-control-center/ui/widgets/status_bar.slint @@ -0,0 +1,58 @@ +import { RogPalette } from "../themes/rog_theme.slint"; + +export global SystemStatus { + in property cpu_temp: 0; + in property gpu_temp: 0; + in property cpu_fan: 0; + in property gpu_fan: 0; + in property power_w: "--"; + in property power_avg_w: "--"; +} + +component StatusItem inherits Rectangle { + in property label; + in property value; + in property unit; + + HorizontalLayout { + spacing: 5px; + Text { text: label; color: RogPalette.text-secondary; font-weight: 700; vertical-alignment: center; } + Text { text: value; color: RogPalette.text-primary; vertical-alignment: center; } + Text { text: unit; color: RogPalette.text-secondary; font-size: 12px; vertical-alignment: center; } + } +} + +export component StatusBar inherits Rectangle { + background: RogPalette.control-background; + height: 30px; + + // Simulated top border + Rectangle { + y: 0px; + x: 0px; + width: parent.width; + height: 1px; + background: RogPalette.control-border; + } + + HorizontalLayout { + padding-left: 20px; + padding-right: 20px; + spacing: 20px; + alignment: space-between; + + HorizontalLayout { + spacing: 20px; + StatusItem { label: "CPU"; value: SystemStatus.cpu_temp; unit: "°C"; } + StatusItem { label: "GPU"; value: SystemStatus.gpu_temp; unit: "°C"; } + } + + HorizontalLayout { + spacing: 20px; + StatusItem { label: "PWR"; value: SystemStatus.power_w; unit: "W"; } + StatusItem { label: "AVG"; value: SystemStatus.power_avg_w; unit: "W"; } + StatusItem { label: "CPU Fan"; value: SystemStatus.cpu_fan; unit: "RPM"; } + StatusItem { label: "GPU Fan"; value: SystemStatus.gpu_fan; unit: "RPM"; } + } + } +} diff --git a/rog-control-center/ui/windows/tray_tooltip.slint b/rog-control-center/ui/windows/tray_tooltip.slint new file mode 100644 index 00000000..73758128 --- /dev/null +++ b/rog-control-center/ui/windows/tray_tooltip.slint @@ -0,0 +1,76 @@ +import { RogPalette } from "../themes/rog_theme.slint"; +import { SystemStatus } from "../widgets/status_bar.slint"; + +component StatusItem inherits Rectangle { + in property label; + in property value; + in property unit; + + HorizontalLayout { + spacing: 8px; + Text { + text: label; + color: RogPalette.text-secondary; + font-weight: 700; + vertical-alignment: center; + font-size: 13px; + } + Text { + text: value; + color: RogPalette.text-primary; + vertical-alignment: center; + font-size: 14px; + } + Text { + text: unit; + color: RogPalette.text-secondary; + font-size: 11px; + vertical-alignment: center; + } + } +} + +export component TrayTooltip inherits Window { + always-on-top: true; + no-frame: true; + background: transparent; + width: 280px; + height: 160px; + + Rectangle { + background: RogPalette.control-background; + border-radius: 8px; + border-width: 1px; + border-color: RogPalette.control-border; + drop-shadow-blur: 10px; + drop-shadow-color: rgba(0,0,0,0.5); + + VerticalLayout { + padding: 15px; + spacing: 12px; + + Text { + text: "System Statistics"; + color: RogPalette.accent; + font-size: 16px; + font-weight: 800; + } + + GridLayout { + spacing: 15px; + Row { + StatusItem { label: "CPU"; value: SystemStatus.cpu_temp; unit: "°C"; } + StatusItem { label: "GPU"; value: SystemStatus.gpu_temp; unit: "°C"; } + } + Row { + StatusItem { label: "FAN"; value: SystemStatus.cpu_fan; unit: "RPM"; } + StatusItem { label: "GPU"; value: SystemStatus.gpu_fan; unit: "RPM"; } + } + Row { + StatusItem { label: "PWR"; value: SystemStatus.power_w; unit: "W"; } + StatusItem { label: "AVG"; value: SystemStatus.power_avg_w; unit: "W"; } + } + } + } + } +}