rust_widgets 2.8.4

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Quality manager for dynamic quality adjustment.
use super::config::QualityConfig;
use super::level::QualityLevel;
use super::monitor::FrameTimeMonitor;
use crate::compat::Duration;
/// GPU capability detection based on adapter information.
#[derive(Debug, Clone, Copy)]
pub struct GpuCapability {
    /// Whether the GPU supports high-quality rendering.
    pub supports_high_quality: bool,
    /// Whether the GPU is integrated (vs discrete).
    pub is_integrated: bool,
    /// Estimated performance tier (1-5, higher is better).
    pub performance_tier: u8,
}
impl Default for GpuCapability {
    fn default() -> Self {
        Self { supports_high_quality: true, is_integrated: false, performance_tier: 3 }
    }
}
impl GpuCapability {
    /// Creates GPU capability from adapter information.
    #[cfg(feature = "gpu-wgpu")]
    pub fn from_adapter_info(adapter_info: &wgpu::AdapterInfo) -> Self {
        let supports_high_quality = matches!(
            adapter_info.device_type,
            wgpu::DeviceType::DiscreteGpu | wgpu::DeviceType::IntegratedGpu
        );
        let is_integrated = matches!(adapter_info.device_type, wgpu::DeviceType::IntegratedGpu);
        let performance_tier = match adapter_info.device_type {
            wgpu::DeviceType::DiscreteGpu => 5,
            wgpu::DeviceType::IntegratedGpu => 3,
            wgpu::DeviceType::Other => 2,
            wgpu::DeviceType::VirtualGpu => 2,
            wgpu::DeviceType::Cpu => 1,
        };
        Self { supports_high_quality, is_integrated, performance_tier }
    }
    /// Creates a default capability when GPU info is unavailable.
    pub fn default_capability() -> Self {
        Self::default()
    }
    /// Returns the recommended initial quality level.
    pub fn recommended_initial_quality(&self) -> QualityLevel {
        if self.supports_high_quality && self.performance_tier >= 4 {
            QualityLevel::High
        } else if self.supports_high_quality && self.performance_tier >= 2 {
            QualityLevel::Medium
        } else {
            QualityLevel::Low
        }
    }
}
/// Quality manager for dynamic quality adjustment with hysteresis.
///
/// # Examples
///
/// ```rust
/// use rust_widgets::quality::{QualityManager, QualityLevel};
///
/// let mut manager = QualityManager::new();
/// assert_eq!(manager.quality_level(), QualityLevel::Medium);
///
/// // Record frame durations
/// manager.finish_frame_secs(0.016); // ~60 FPS
/// assert_eq!(manager.quality_level(), QualityLevel::Medium);
/// ```
#[derive(Debug, Clone)]
pub struct QualityManager {
    current_level: QualityLevel,
    config: QualityConfig,
    frame_monitor: FrameTimeMonitor,
    gpu_capability: GpuCapability,
}
impl QualityManager {
    /// Creates a new quality manager with default configuration.
    pub fn new() -> Self {
        Self::with_config_and_capability(QualityConfig::default(), GpuCapability::default())
    }
    /// Creates a new quality manager with the specified configuration.
    pub fn with_config(config: QualityConfig) -> Self {
        Self::with_config_and_capability(config, GpuCapability::default())
    }
    /// Creates a new quality manager with the specified configuration and GPU capability.
    pub fn with_config_and_capability(
        config: QualityConfig,
        gpu_capability: GpuCapability,
    ) -> Self {
        let config = config.normalized();
        let initial_quality = gpu_capability
            .recommended_initial_quality()
            .clamp(config.min_quality, config.max_quality);
        let frame_monitor = FrameTimeMonitor::with_capacity_for_counts(
            config.target_frame_rate,
            config.degrade_frame_count,
            config.upgrade_frame_count,
        );
        Self { current_level: initial_quality, config, frame_monitor, gpu_capability }
    }
    /// Records a frame duration and updates quality level if necessary.
    pub fn finish_frame(&mut self, frame_duration: Duration) {
        let frame_duration_secs = frame_duration.as_secs_f32();
        self.finish_frame_secs(frame_duration_secs);
    }
    /// Records a frame duration in seconds and updates quality level if necessary.
    pub fn finish_frame_secs(&mut self, frame_duration: f32) {
        self.frame_monitor.record_frame(frame_duration);
        self.update_quality_level();
    }
    fn update_quality_level(&mut self) {
        match self.current_level {
            QualityLevel::High => {
                if self.frame_monitor.should_degrade(
                    self.config.degrade_frame_duration(),
                    self.config.degrade_frame_count,
                ) {
                    if let Some(lower) = self.current_level.lower() {
                        if lower >= self.config.min_quality {
                            self.current_level = lower;
                        }
                    }
                }
            }
            QualityLevel::Medium => {
                if self.frame_monitor.should_degrade(
                    self.config.degrade_frame_duration(),
                    self.config.degrade_frame_count,
                ) {
                    if let Some(lower) = self.current_level.lower() {
                        if lower >= self.config.min_quality {
                            self.current_level = lower;
                        }
                    }
                } else if self.frame_monitor.should_upgrade(
                    self.config.upgrade_frame_duration(),
                    self.config.upgrade_frame_count,
                ) {
                    if let Some(higher) = self.current_level.higher() {
                        if higher <= self.config.max_quality {
                            self.current_level = higher;
                        }
                    }
                }
            }
            QualityLevel::Low => {
                if self.frame_monitor.should_upgrade(
                    self.config.upgrade_frame_duration(),
                    self.config.upgrade_frame_count,
                ) {
                    if let Some(higher) = self.current_level.higher() {
                        if higher <= self.config.max_quality {
                            self.current_level = higher;
                        }
                    }
                }
            }
        }
    }
    /// Returns the current quality level.
    pub fn quality_level(&self) -> QualityLevel {
        self.current_level
    }
    /// Sets the quality level manually.
    pub fn set_quality_level(&mut self, level: QualityLevel) {
        self.current_level = level.clamp(self.config.min_quality, self.config.max_quality);
    }
    /// Returns the quality configuration.
    pub fn config(&self) -> &QualityConfig {
        &self.config
    }
    /// Updates the quality configuration.
    ///
    /// The stored configuration is normalized (thresholds, counts and an inverted quality range are
    /// repaired), and [`Self::current_level`] is re-clamped against the *new* range. Without the
    /// re-clamp, shrinking the range left the level outside it — the manager would report a level its
    /// own config forbids until the next change fired a branch, and a level at the old maximum never
    /// degrades because its arm only reacts to `should_degrade`.
    ///
    /// Widening the range deliberately does **not** move the level: a caller that opens up the range
    /// has not asked for a different quality, so the current choice is preserved.
    pub fn set_config(&mut self, config: QualityConfig) {
        self.config = config.normalized();
        self.frame_monitor.set_target_frame_rate(self.config.target_frame_rate);
        // Re-clamp against the new bounds. `clamp` moves the value only when it now falls outside the
        // range, so widening leaves the user's choice untouched.
        self.current_level =
            self.current_level.clamp(self.config.min_quality, self.config.max_quality);
    }
    /// Returns the GPU capability.
    pub fn gpu_capability(&self) -> &GpuCapability {
        &self.gpu_capability
    }
    /// Returns the frame time monitor.
    pub fn frame_monitor(&self) -> &FrameTimeMonitor {
        &self.frame_monitor
    }
    /// Returns the current frame rate.
    pub fn current_fps(&self) -> f32 {
        self.frame_monitor.current_fps()
    }
    /// Returns the average frame time in seconds.
    pub fn average_frame_time(&self) -> f32 {
        self.frame_monitor.average_frame_time()
    }
    /// Resets the quality manager state.
    pub fn reset(&mut self) {
        self.frame_monitor.reset();
        self.current_level = self
            .gpu_capability
            .recommended_initial_quality()
            .clamp(self.config.min_quality, self.config.max_quality);
    }
}
crate::impl_default_via_new!(QualityManager);

#[cfg(test)]
mod tests {
    use super::*;

    fn config_with_range(min: QualityLevel, max: QualityLevel) -> QualityConfig {
        QualityConfig {
            min_quality: min,
            max_quality: max,
            degrade_frame_count: 5,
            upgrade_frame_count: 10,
            ..QualityConfig::default()
        }
    }

    /// Shrinking the range through `set_config` must bring the level into it immediately, before any
    /// frame is observed. The defect was that `set_config` swapped the config but never re-clamped, so
    /// a `High` level survived a `min = max = Low` config.
    #[test]
    fn shrinking_the_range_makes_the_level_immediately_compliant() {
        let mut manager = QualityManager::new();
        manager.set_quality_level(QualityLevel::High);
        assert_eq!(manager.quality_level(), QualityLevel::High);

        manager.set_config(config_with_range(QualityLevel::Low, QualityLevel::Low));
        assert_eq!(
            manager.quality_level(),
            QualityLevel::Low,
            "the level must be reclamped the moment the range shrinks"
        );

        // And it stays compliant across normal frames rather than being corrected lazily.
        for _ in 0..100 {
            manager.finish_frame_secs(0.016);
        }
        assert_eq!(manager.quality_level(), QualityLevel::Low);
    }

    /// Widening the range must preserve the caller's current choice: opening the range is not a
    /// request to change quality.
    #[test]
    fn widening_the_range_preserves_the_current_choice() {
        let mut manager = QualityManager::with_config(config_with_range(
            QualityLevel::Medium,
            QualityLevel::Medium,
        ));
        assert_eq!(manager.quality_level(), QualityLevel::Medium);

        manager.set_config(config_with_range(QualityLevel::Low, QualityLevel::High));
        assert_eq!(
            manager.quality_level(),
            QualityLevel::Medium,
            "a widened range must not reset the user's choice"
        );
    }

    /// The history window is sized from the configured counts, so a config asking for a longer window
    /// than the old fixed 60 samples has an observable effect: enough slow frames eventually degrade.
    #[test]
    fn a_config_with_a_long_window_can_actually_degrade() {
        let config = QualityConfig {
            degrade_frame_count: 90,
            upgrade_frame_count: 90,
            ..QualityConfig::default()
        };
        let mut manager = QualityManager::with_config(config);
        manager.set_quality_level(QualityLevel::High);
        for _ in 0..89 {
            manager.finish_frame_secs(0.05);
        }
        assert_eq!(
            manager.quality_level(),
            QualityLevel::High,
            "89 of the required 90 slow frames is not yet enough"
        );
        manager.finish_frame_secs(0.05);
        assert!(
            manager.quality_level() < QualityLevel::High,
            "the 90th slow frame must complete the window and degrade"
        );
    }
}