How the Theme System Supports Light/Dark Mode and Custom Themes in UAD Next Generation
The Universal Android Debloater Next Generation uses Iced's theme API combined with the dark_light crate to automatically detect OS color schemes, while providing built-in Light, Dark, and custom Lupin palettes that users can switch between at runtime with persistent settings.
The theme system in Universal-Debloater-Alliance/universal-android-debloater-next-generation delivers a flexible, cross-platform appearance engine that respects system preferences while allowing granular customization. Built on the Rust Iced GUI framework, it automatically detects whether your operating system is set to light or dark mode and can instantly switch between predefined color palettes including a distinctive "Lupin" custom theme.
OS-Level Color Scheme Detection
The foundation of automatic theme switching relies on the dark_light crate. In crates/uad-gui/src/theme.rs, a static LazyLock caches the result of OS detection at startup:
pub static OS_COLOR_SCHEME: LazyLock<dark_light::Mode> =
LazyLock::new(|| dark_light::detect().unwrap_or(dark_light::Mode::Unspecified));
This global static (lines 14-16) ensures that the detected mode—Dark, Light, or Unspecified—is computed once and reused throughout the application lifecycle, eliminating redundant system calls while providing the source of truth for the Auto theme variant.
Theme Enumeration and Built-In Palettes
The Theme enum in theme.rs defines four supported variants that map to specific color palettes:
pub enum Theme {
#[default] Auto,
Lupin,
Dark,
Light,
}
Lines 17-29 establish Auto as the default, which defers to the OS color scheme detected via OS_COLOR_SCHEME, while Lupin provides a custom aesthetic distinct from standard light/dark modes. Each variant corresponds to a hard-coded ColorPalette struct containing base, normal, and bright color definitions (lines 71-135).
Color Palette Implementation
Each theme variant returns its specific ColorPalette through the palette() method. The source code defines three constant palettes: DARK, LIGHT, and LUPIN (lines 72-125). When palette() executes, it matches the variant:
match self {
Self::Auto => /* OS detection logic */,
Self::Dark => DARK,
Self::Light => LIGHT,
Self::Lupin => LUPIN,
}
This architecture ensures that color values are compile-time constants, providing zero-cost runtime theme resolution while maintaining type safety. The ColorPalette struct contains nested BaseColors, NormalColors, and BrightColors that specify exact RGB values for every UI element.
Iced Framework Integration
The theme system implements iced::theme::Base to integrate with the Iced widget rendering pipeline. In theme.rs (lines 164-190), three critical methods bridge the custom theme to Iced's internals:
default(preference): Maps Iced'sModepreference to a concrete theme, falling back toOS_COLOR_SCHEMEwhen the preference isNonemode(&self): Resolves the effectiveMode(Light/Dark) for the current theme, consultingOS_COLOR_SCHEMEfor theAutovariantpalette(&self)andname(&self): Expose the color values and identifier strings that Iced uses when drawing containers, buttons, and text
This implementation allows the application to react to system theme changes dynamically when running in Auto mode, ensuring widgets always render with appropriate contrast ratios.
Runtime Theme Selection and Persistence
User interaction occurs in crates/uad-gui/src/views/settings.rs. The Settings view constructs a radio-button list from Theme::ALL, providing one-click switching:
let radio_btn_theme = Theme::ALL
.iter()
.fold(row![].spacing(10), |column, option| {
column.push(
radio(
format!("{}", option.clone()),
*option,
Some(string_to_theme(&self.general.theme)),
Message::ApplyTheme,
)
.size(24),
)
});
Lines 94-105 demonstrate how the UI iterates through all available themes, displaying the current selection and emitting Message::ApplyTheme on change.
When a user selects a new theme, handle_apply_theme (lines 33-39) persists the choice via Config::save_device_settings, ensuring the preference survives application restarts. The string_to_theme function handles the bidirectional conversion between configuration strings and enum variants.
Adding Custom Themes to the System
Extending the theme system requires modifications to theme.rs. To add a new palette like "Solar":
- Extend the enum with a new variant
- Define a constant
ColorPalettewith base, normal, and bright colors - Return the new palette in the
palette()method match arm
Because Theme::ALL is defined as a static array containing all variants, new themes automatically appear in the Settings UI without additional view code.
Summary
- Automatic OS detection uses the
dark_lightcrate cached in aLazyLockstatic to determine system preferences without blocking the UI - Four built-in variants (Auto, Dark, Light, Lupin) provide immediate theming options, with Auto dynamically following OS changes via
OS_COLOR_SCHEME - Iced integration through the
Basetrait implementation ensures widgets render with correct colors and react to mode changes throughdefault()andmode()methods - Runtime persistence saves user selections to the device configuration file via
save_device_settingsinsettings.rs - Extensible architecture allows adding new color palettes by extending the enum and adding constant definitions that automatically populate the UI
Frequently Asked Questions
How does the theme system detect if my OS is in dark mode?
The system utilizes the dark_light crate to query the operating system's color scheme preference during startup. This value is stored in the OS_COLOR_SCHEME static variable using LazyLock, ensuring the detection runs exactly once and remains available for the Auto theme variant to reference whenever the application needs to determine whether to render in light or dark mode.
Can I add my own custom color theme to UAD Next Generation?
Yes, the architecture supports custom themes by extending the Theme enum in crates/uad-gui/src/theme.rs. You must define a new variant, create a constant ColorPalette with your desired base, normal, and bright colors, and add the corresponding match arm in the palette() method. Since the settings UI iterates over Theme::ALL, your new theme automatically appears in the radio button list without modifying the view code.
Where does the application store my selected theme preference?
The selected theme is persisted through the Config::save_device_settings method called in handle_apply_theme within crates/uad-gui/src/views/settings.rs. This writes the theme string to the application's configuration file, which is loaded on startup and converted back to a Theme variant via string_to_theme when the GUI initializes.
What is the "Lupin" theme and how is it different from Light and Dark?
Lupin is a custom color palette hard-coded in theme.rs (lines 72-125) alongside the standard Dark and Light themes. While Light and Dark follow traditional high-contrast aesthetics suitable for accessibility and standard OS integration, Lupin provides a distinct visual identity with custom base, normal, and bright color values that offer an alternative to the standard binary choice, demonstrating how the system supports arbitrary custom palettes beyond simple light/dark mode.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →