Repository navigation
native-theme 0.7.0 is the release of a whole-codebase review: every crate, preset, platform reader, connector and showcase was read and fixed, and the gpui connector moves to GPUI Kit 0.8.1. It breaks the public API in many places; the full list is below. This is a pre-1.0 release, so there is no migration guide: each entry says what replaces what.
Highlights
- Presets named by the OS generation they carry:
material-3,macos-monterey(macOS 12 to 15) andios-13replacematerial,macos-sonomaandios. The newmacos-tahoepreset is macOS 26's Liquid Glass look, and the platform preset is picked by the running OS version (SystemTheme::version_matchreports the match). - The
-livepreset tier is gone: the platform readers merge over the platform preset. - Connectors:
from_system()returns a namedLoadedstruct, and the accessors take the model's field names. The gpui and egui connectors run a platform reader only with theirnativefeature, as the iced connector does. - New theme data: the tooltip delay, the dialog entrance and the list header height (
tooltip.show_delay_ms,dialog.entrance,list.header_height_px), the selected tab's folder shape, and the switch's track and thumb outlines. - Theme changes: the iced and gpui connectors gain theme-change integrations (
theme_changes(),on_theme_change). - gpui: GPUI Kit 0.8.1 on gpui-pre 0.3.10, with a
gpui-fastfeature for GPUI Fast 0.1.12, and a clippy gate for the upstream gpui functions that panic on their input.
Minimum Rust: 1.88 for native-theme, native-theme-build and native-theme-iced; 1.95 for native-theme-gpui, native-theme-egui and native-theme-egui-widgets.
Every Added, Changed and Fixed entry is in CHANGELOG.md.
Breaking Changes
- native-theme: the
-livepresets are gone:kde-breeze-live,adwaita-live,macos-sonoma-liveandwindows-11-live(Theme::presetnow returnsUnknownPresetfor them), withPlatformPreset::is_liveandPlatformPreset::live_name(). The platform readers merge over the platform preset (kde-breeze,adwaita,macos-monterey,windows-11). Every key of a-livefile stated its platform preset's value, so the layer changed no resolved value. - native-theme: the
materialpreset is nowmaterial-3:Theme::preset("material")returnsUnknownPreset, with no alias. - native-theme: the
macos-sonomapreset is nowmacos-monterey, named by the first macOS version whose look it carries: Monterey (12), whose AppKit release notes end the accent-colour highlight of a clicked button; it covers macOS 12 to 15.Theme::preset("macos-sonoma")returnsUnknownPreset, with no alias. Its display name and TOMLnameare "macOS Monterey", where they were "macOS Sonoma". - native-theme: the
iospreset is nowios-13, named by the first iOS version whose look it carries: the flat look with its dark variant that iOS 13 introduced and iOS 18 still draws.Theme::preset("ios")returnsUnknownPreset, with no alias. - native-theme: the data-model types have one public path,
native_theme::theme: themodelmodule is private, and the sub-module pathstheme::font::FontSize,theme::widgets::ButtonThemeand the like are gone (theme::FontSize,theme::ButtonTheme).themelists its types by name, so docs.rs shows them on thethemepage instead of apub use crate::model::*line.detect_icon_theme(), reachable only asmodel::icons::detect_icon_theme, isdetect::detect_icon_theme(see thedetectline below). - native-theme:
FontSizeno longer implementsDefault: itsPx(0.0)was a size validation always rejects, and nothing used it. - native-theme:
PresetInfois#[non_exhaustive]and has nolight_onlyfield, which every bundled preset set tofalse(each one has both variants). - native-theme:
ResolvedTheme,ResolvedDefaults,ResolvedTextScale,ResolvedTextScaleEntry,ResolvedIconSizes,ResolvedDefaultsBorderandResolvedare#[non_exhaustive], as the generatedResolved*Themewidget structs already were, so they can no longer be built by struct literal outside the crate; a field added to them later is not a breaking change.ResolvedWidgetBorder,ResolvedPaddingandResolvedFontSpecstay constructible. - native-theme:
AccessibilityPreferencesis#[non_exhaustive], so a flag added to it later is not a breaking change; outside the crate it is built fromAccessibilityPreferences::DEFAULTordefault()with its fields set, no longer by struct literal or..Default::default(). - native-theme:
Error::IoisIo { path, source }:Theme::from_filenames the file it could not read, displayed as "cannot read theme file{path}: {source}" (it was "I/O error: {source}", without the path). The blanketimpl From<std::io::Error> for Erroris gone, so?no longer turns anio::Errorinto anative_theme::Error. - native-theme:
IconRole::ALLis a&'static [IconRole], as the icon enums native-theme-build generates are (it was[IconRole; 42]), so a role added later does not change its type; iterate it withIconRole::ALL.iter().copied(). - native-theme:
IconSetChoiceis#[non_exhaustive], so a choice added later is not a breaking change; amatchon it outside the crate needs a_arm. - native-theme:
IconData::Rgbaholds anRgbaDatawhose fields are private (IconData::Rgba(rgba), read withrgba.width(),rgba.height(),rgba.data()andrgba.into_data()), built withIconData::rgba(width, height, data), which returnsNoneunlessdataholdswidth × height × 4bytes; deserialising anRgbawith a buffer of another length fails. The serialised shape is unchanged in JSON and TOML. It was a struct variant with public fields, soIconData::Rgba { width: 16, height: 16, data: vec![] }was a value every consumer had to check again. - native-theme:
AnimatedIcon::new_frames, deprecated since 0.5.7, is gone; useAnimatedIcon::frames. - native-theme: the platform modules are private:
freedesktop,macos,sficons,windows,winicons,kde::colorsandkde::fontshad no public item and showed as empty pages on docs.rs, andgnome's one public item,GnomePortalData, was a struct no public function takes or returns.kde::from_kde_content_pure, public for the crate's own KDE reader tests, is hidden from the documentation. - native-theme:
IconSetChoice,default_icon_choiceandlist_freedesktop_themesare no longer re-exported at the crate root; their one path isnative_theme::icons. - native-theme:
system_icon_theme(),detect_icon_theme()andsystem_icon_set()are indetect, besidesystem_is_dark()anddetect_is_dark()and with theDetectionContext::icon_theme()cache they share:native_theme::detect::system_icon_theme(it wastheme::system_icon_theme, andsystem_icon_setwastheme::system_icon_set). - native-theme: the icon data types are in
icons, beside the loaders that take and return them:IconRole,IconData,IconProvider,icon_name,AnimatedIcon,FrameList,FramesData,EmptyFrameListError,TransformAnimationandTransformData(they were intheme:native_theme::theme::IconRoleisnative_theme::icons::IconRole), withRgbaData; oneuse native_theme::icons::{IconRole, MaterialLoader};imports a loader and its role.IconSetandIconSizes, which a theme states, stay intheme. - native-theme:
IconSet::SegoeIconsisIconSet::SegoeFluentandSegoeIconsLoaderisSegoeFluentLoader, matching the set's serialised namesegoe-fluent, which is unchanged (TOMLicon_set = "segoe-fluent", the native-theme-build mapping key and itssegoe-fluent/directory); code native-theme-build generates namesIconSet::SegoeFluent. - native-theme:
FreedesktopLoader::sizetakes au32, the pixel type ofrasterize_svgandIconData::rgba(it wasu16), clamped as the other loaders' sizes are (theMAX_ICON_SIZEline below). - native-theme: the system loaders'
colorandcolor_opt(FreedesktopLoader,SfSymbolsLoader,SegoeFluentLoader) take anRgbaand anOption<Rgba>, the crate's colour type thatcolorize_monochrome_svgtakes (they took[u8; 3]); the alpha is ignored, as the icon keeps its own coverage. - native-theme:
FreedesktopLoader::theme(name)looks an icon up innameonly where that is an installed icon theme, one whoseindex.themelistsDirectories:.theme("default"), a theme that only inherits Adwaita, loadsNone, where it loaded Adwaita's icons throughdefault'sInherits=(see Fixed). - native-theme:
icons::load_icon_indicator(set, theme)takes the freedesktop theme to take the spinner from (Nonefor the system's, as it used before), so a freedesktop spinner from a chosen theme needs no dispatch by hand; the other sets do not use it. - native-theme-build: the generated enum implements
{crate_path}::icons::IconProvider(it was{crate_path}::theme::IconProvider), the trait's path in this native-theme. - native-theme-build: every field of
GenerateOutputandBuildErrorsis private, read through accessors:GenerateOutput::{output_path, warnings, role_count, bundled_theme_count, svg_count, total_svg_bytes, rerun_paths, code}()andBuildErrors::{errors, rerun_paths}()(output.codeisoutput.code();rerun_pathswas a public field onBuildErrorsand a method onGenerateOutput). - native-theme-build: build errors name the file that holds the mistake:
BundledDeAwarehas amapping_file,DuplicateThemeafile,ThemeOverlapandInvalidIdentifierasource_file: Option<String>(Nonefor a check on the merged configs of several files), andMissingSvgtherolewhose mapping names the SVG.InvalidIconNamehas areason, the rule the name breaks ("names must not contain "..""), which its message ends with; it ended with "names must be non-empty and free of control characters" whichever rule failed, and itsoffendingcharacter is now the one that breaks that rule. - native-theme-iced, native-theme-gpui, native-theme-egui:
from_system()returns aLoaded, a#[non_exhaustive]struct in each connector with the same field names, so a field added later is not a breaking change:theme(egui:atlas),resolved,mode: ColorMode,accessibility,icon_setandicon_theme, the OS colour mode's icon theme (SystemTheme::icon_theme_for) that the icedSpinner::newand a freedesktop icon take. It returned the tuples(theme, resolved, is_dark, accessibility)(iced),(theme, resolved, is_dark)(gpui) and(atlas, resolved, is_dark)(egui);loaded.mode.is_dark()is theis_darkthey carried. The iced and gpuiSystemThemeExt::to_iced_themeandto_gpui_themereturn the sameLoaded(they returned(theme, resolved)and the gpuiThemealone); egui'sto_egui_atlaskeeps returning theThemeAtlas, which carries all of it already.from_presetis unchanged. - native-theme-iced: the accessors are named after the
ResolvedThemeleaf they read, without the leadingdefaults.and a unit suffix, as in the gpui and egui connectors:border_radius→border_corner_radius,border_radius_lg→border_corner_radius_lg,scrollbar_width→scrollbar_groove_width,selection_color→selection_background,info_foreground_color→info_text_color,warning_foreground_color→warning_text_color,line_height_multiplier→line_height. The old names are gone, with no alias. - native-theme-gpui: the accessors are named after the
ResolvedThemeleaf they read, as in the iced and egui connectors:frame_width→border_line_width,shadow_enabled→border_shadow_enabled,dialog_button_spacing→dialog_button_gap,scrollbar_width→scrollbar_groove_width,selection_foreground→selection_text_color,selection_inactive→selection_inactive_background,disabled_foreground→disabled_text_color,line_height_multiplier→line_height. The old names are gone, with no alias. - native-theme-egui: the accessors are named after the
ResolvedThemeleaf they read, as in the iced and gpui connectors:border_radius→border_corner_radius,border_radius_lg→border_corner_radius_lg,scrollbar_width→scrollbar_groove_width,line_height_multiplier→line_height. The old names are gone, with no alias. - native-theme:
TooltipTheme,DialogThemeandListThemegain a field each,show_delay,entranceandheader_height(see Added). The three are not#[non_exhaustive], so a struct literal of one outside the crate needs the new field or..Default::default(); their resolved forms are#[non_exhaustive]already. - native-theme:
SwitchThemegains seven fields, the track's and the thumb's outlines (see Added):unchecked_track_border_color,checked_track_border_color,disabled_unchecked_track_border_color,disabled_checked_track_border_color,track_border_line_width,thumb_border_colorandthumb_border_line_width.SwitchThemeis not#[non_exhaustive], so a struct literal of it outside the crate needs the new fields or..Default::default();ResolvedSwitchThemeis#[non_exhaustive]already. - native-theme:
TabThemegainsactive_outward_corner_radius(see Added);TabThemeis not#[non_exhaustive], so a struct literal of it outside the crate needs the field or..Default::default(); its resolved form is#[non_exhaustive]already. - native-theme-gpui:
icons::with_spin_animationtakes the period as aNonZeroU32, the type ofTransformAnimation::Spin'sduration_ms, which it is passed as it is (no.get());AnimatedImageSources::frame_duration_msis aNonZeroU32as the icon data's frame duration is. A period of 0 made gpui's rotation NaN, a panic in a debug build (gpui-pre'sdebug_assert!that the animated value is finite), and no longer compiles. - native-theme-gpui:
CheckboxLook,SwitchLook,SliderLook,ProgressBarLook,SeparatorLook,SpinnerLook,TabLook,TabIndicator,ScrollbarGeometryandAnimatedImageSourcesare#[non_exhaustive], asLoadedis, so a field added to them later is not a breaking change; outside the crate they can no longer be built by struct literal or destructured without... AScrollbarGeometryforapply_overridesorscrollbar_stylesis made byscrollbar_geometry(&resolved)and edited field by field. - native-theme-iced:
AnimatedSvgHandlesis#[non_exhaustive], as the gpui connector'sAnimatedImageSourcesis. - native-theme-gpui:
is_dark(resolved)is gone: it was a second name foris_dark_resolved(resolved), which stays for a caller holding only aResolvedTheme. - native-theme-gpui:
ActiveNativeTheme::native():cx.native()gives theNativeview thegeometrybuilders take in one call, ascx.theme()gives gpui-component's theme (it wascx.native_theme().and_then(|t| t.native(cx))). It is a required method: a type outside the crate that implementsActiveNativeThememust add it (the trait is implemented forApp). - native-theme-gpui:
SpinnerLook::path_radius()is gone from the public API (crate-private, for the connector's tests): nothing draws with it, the arc takes its radii fromdiameterandstroke. It is also no less than 0 now; it went negative for a stroke wider than the diameter, where the drawn arc fills the disc. - native-theme-gpui: a default build no longer reads the OS theme through a platform reader. The connector turned on native-theme's
linux,macosorwindowsreader for the target it was built for; it now forwards them as thenativefeature, off by default as in native-theme and the iced connector, so without itfrom_system(), and theSystemTheme::from_system()whose resultapply_system_themetakes, give the platform preset on Linux, in the light or dark mode the OS reports and with its reduced-motion preference (the two settings Linux reads without a reader), and an error on macOS and Windows. Addfeatures = ["native"](the README's Quick start does), or name native-theme'skde,portal,macosorwindowsfor one platform. - native-theme-egui-widgets:
expander::ExpanderResponseis#[non_exhaustive], as the connector'sLoadedis, so the fields egui'sCollapsingResponsealso carries (body_response,openness) can be added later without a breaking change; its fields are read as before, but outside the crate it can no longer be built by struct literal or destructured without... - native-theme-egui:
convert::to_color32_with_opacityis gone: nothing in the connector, the widgets crate or the showcase called it;to_color32(c).gamma_multiply(opacity)is egui's own spelling. - native-theme-egui-widgets: the module
wrapislinks(links::link,links::hyperlink), named for what it holds: in this crate and in egui "wrap" means line-wrapping (SegmentedControl::wrap,ComboBox::wrap). The old path is gone, with no alias. - native-theme-egui, native-theme-egui-widgets: a default build no longer reads the OS theme through a platform reader. The connector turned on native-theme's
linux,macosorwindowsreader for the target it was built for; it now forwards them as thenativefeature, off by default as in native-theme and the iced and gpui connectors, so without itfrom_system(), theSystemTheme::from_system()whose resultto_egui_atlastakes, andThemeWatcher::system_rebuildgive the platform preset on Linux, in the light or dark mode the OS reports and with its reduced-motion preference (the two settings Linux reads without a reader), and an error on macOS and Windows. Addfeatures = ["native"]to either crate (the READMEs' Quick start does), or name native-theme'skde,portal,macosorwindowsfor one platform. - native-theme: icon sizes are clamped to
1..=icons::MAX_ICON_SIZE(512):FreedesktopLoader::sizeandrasterize::rasterize_svg, as the size of every icon loader this release adds is (SfSymbolsLoader::size,SegoeFluentLoader::size,icons::load_custom_iconandload_custom_icon_in). A size above 512 used to be looked up or drawn at the size asked and now comes back at 512: a freedesktop icon from the theme's directory nearest 512, an SVG rasterised at 512 × 512. A size of 0 is taken as 1:rasterize_svgreturns a 1 × 1 raster for it, where it returned an error. A loader that cannot draw the icon at the clamped size still returnsNone. - native-theme-iced:
icons::custom_icon_to_image_handleandicons::custom_icon_to_svg_handletake(provider, icon_set, icon_theme: Option<&str>, color, size: Option<u32>), as egui'scustom_icon_to_image_sourcetakes the icon theme and size in itsIconKey(they took(provider, icon_set, color)). They load throughnative_theme::icons::load_custom_icon_in, so a freedesktop icon comes fromicon_theme(passloaded.icon_theme.as_deref(), the icon theme theSpinnerdraws from), where it came from the system's even under a pinned preset whose variant names another (adwaita'sAdwaitaon a desktop whose icon theme is Breeze), and atsize, clamped to1..=MAX_ICON_SIZE, where every system-set icon came at its loader's default;None, Nonekeeps the old behaviour. - native-theme-iced:
icons::spin_rotation_radianstakes the period as aNonZeroU32, the type ofTransformAnimation::Spin'sduration_ms, which it is passed as it is (no.get()), as gpui'swith_spin_animationtakes it;AnimatedSvgHandles::frame_duration_msis aNonZeroU32as the icon data's frame duration and gpui'sAnimatedImageSources::frame_duration_msare. A period of 0, which the function answered with an angle of 0, no longer compiles. - native-theme-iced:
SystemThemeExtis sealed, as the egui connector's is: a private supertrait implemented forSystemThemealone, so a type outside the crate can no longer implement it, and a method added to it later is not a breaking change. - native-theme-gpui:
icons::custom_icon_to_image_sourcetakes(provider, icon_set, icon_theme: Option<&str>, color, size), as iced'scustom_icon_to_image_handleand egui'scustom_icon_to_image_sourcetake the icon theme (it took(provider, icon_set, color, size)). It loads throughnative_theme::icons::load_custom_icon_in, so a freedesktop icon comes fromicon_theme(passloaded.icon_theme.as_deref(), the icon theme theSpinnerdraws from), where it came from the system's even under a pinned preset whose variant names another (adwaita'sAdwaitaon a desktop whose icon theme is Breeze);Nonekeeps the old behaviour. - native-theme-gpui:
SystemThemeExtis sealed, as the egui and iced connectors' are: a private supertrait implemented forSystemThemealone, so a type outside the crate can no longer implement it, and a method added to it later is not a breaking change. - native-theme-gpui:
widgets::PartBoundsisFn(Part, Bounds<Pixels>, &mut Window, &mut App): the observer gets the window being drawn, as gpui's own prepaint callbacks do (it got(Part, Bounds<Pixels>, &mut App)). GPUI Fast draws a view nothing changed for from the last frame without laying the control out again, so the observer is not called on every frame; with the window, it can keep element state that tells whether the frame still draws the part. Nor is it called while GPUI Fast composites a scroll area the control is in as it scrolls: the area's content is drawn moved by the scroll without being laid out again, so the bounds last reported there are off by the scroll since. - native-theme-iced:
styles::segmented_control's fill is the group,segmented_control.background_color, inside its outline, andstyles::segmentlays only what the model lays over it: no fill at rest,hover_backgroundas given under the pointer,active_backgroundwhile selected.styles::segmented_controlandstyles::segment_separatortakedisabled: a disabled control is faded as one widget bysegmented_control.disabled_opacityover the window, its group fill, outline, separators, segments and labels each composited over what it lies on (styles::segment'sStatus::Disabledthe same), as the model defines the opacity, for the entire widget (docs/platform-facts.md§2.1.6), where it faded the segments alone, also on adwaita. The container was filled with the border colour, which a row'sspacing(separator_width)let show as the separators, and every segment painted the group fill again over it, so windows-11's translucent group fill (#6 / #00000019) came out darker inside than the outline. The separators arestyles::segment_separatorlines a consumer puts between the segments,container(space()).width(separator_width).height(Fill), in a row whose height isShrink(the README's andstyles::segmented_control's examples); a consumer who keptrow(..).spacing(separator_width)draws no separators, as the gap now shows the group fill. Every other preset draws as before, within one level of anti-aliasing. - native-theme-gpui: requires gpui-component, gpui-base and gpui-kit 0.8.1 and gpui-pre 0.3.10, and on GPUI Fast gpui-fast 0.1.12 or later, which gpui-kit 0.8.1 asks for (MSRV unchanged, 1.95.0). gpui-kit is a normal dependency, without its default features, since the crate names GPUI through it, so the library's build includes gpui-kit's platform layer (
gpui-pre-platformand its platform crates), which an application links anyway.