Skip to content

native-theme v0.7.0 — whole-codebase review, macOS Tahoe preset, GPUI Kit 0.8.1

Latest

Choose a tag to compare

@tiborgats tiborgats released this 27 Oct 21:27
· 68 commits to main since this release

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) and ios-13 replace material, macos-sonoma and ios. The new macos-tahoe preset is macOS 26's Liquid Glass look, and the platform preset is picked by the running OS version (SystemTheme::version_match reports the match).
  • The -live preset tier is gone: the platform readers merge over the platform preset.
  • Connectors: from_system() returns a named Loaded struct, and the accessors take the model's field names. The gpui and egui connectors run a platform reader only with their native feature, 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-fast feature 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 -live presets are gone: kde-breeze-live, adwaita-live, macos-sonoma-live and windows-11-live (Theme::preset now returns UnknownPreset for them), with PlatformPreset::is_live and PlatformPreset::live_name(). The platform readers merge over the platform preset (kde-breeze, adwaita, macos-monterey, windows-11). Every key of a -live file stated its platform preset's value, so the layer changed no resolved value.
  • native-theme: the material preset is now material-3: Theme::preset("material") returns UnknownPreset, with no alias.
  • native-theme: the macos-sonoma preset is now macos-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") returns UnknownPreset, with no alias. Its display name and TOML name are "macOS Monterey", where they were "macOS Sonoma".
  • native-theme: the ios preset is now ios-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") returns UnknownPreset, with no alias.
  • native-theme: the data-model types have one public path, native_theme::theme: the model module is private, and the sub-module paths theme::font::FontSize, theme::widgets::ButtonTheme and the like are gone (theme::FontSize, theme::ButtonTheme). theme lists its types by name, so docs.rs shows them on the theme page instead of a pub use crate::model::* line. detect_icon_theme(), reachable only as model::icons::detect_icon_theme, is detect::detect_icon_theme (see the detect line below).
  • native-theme: FontSize no longer implements Default: its Px(0.0) was a size validation always rejects, and nothing used it.
  • native-theme: PresetInfo is #[non_exhaustive] and has no light_only field, which every bundled preset set to false (each one has both variants).
  • native-theme: ResolvedTheme, ResolvedDefaults, ResolvedTextScale, ResolvedTextScaleEntry, ResolvedIconSizes, ResolvedDefaultsBorder and Resolved are #[non_exhaustive], as the generated Resolved*Theme widget 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, ResolvedPadding and ResolvedFontSpec stay constructible.
  • native-theme: AccessibilityPreferences is #[non_exhaustive], so a flag added to it later is not a breaking change; outside the crate it is built from AccessibilityPreferences::DEFAULT or default() with its fields set, no longer by struct literal or ..Default::default().
  • native-theme: Error::Io is Io { path, source }: Theme::from_file names the file it could not read, displayed as "cannot read theme file {path}: {source}" (it was "I/O error: {source}", without the path). The blanket impl From<std::io::Error> for Error is gone, so ? no longer turns an io::Error into a native_theme::Error.
  • native-theme: IconRole::ALL is 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 with IconRole::ALL.iter().copied().
  • native-theme: IconSetChoice is #[non_exhaustive], so a choice added later is not a breaking change; a match on it outside the crate needs a _ arm.
  • native-theme: IconData::Rgba holds an RgbaData whose fields are private (IconData::Rgba(rgba), read with rgba.width(), rgba.height(), rgba.data() and rgba.into_data()), built with IconData::rgba(width, height, data), which returns None unless data holds width × height × 4 bytes; deserialising an Rgba with a buffer of another length fails. The serialised shape is unchanged in JSON and TOML. It was a struct variant with public fields, so IconData::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; use AnimatedIcon::frames.
  • native-theme: the platform modules are private: freedesktop, macos, sficons, windows, winicons, kde::colors and kde::fonts had no public item and showed as empty pages on docs.rs, and gnome'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_choice and list_freedesktop_themes are no longer re-exported at the crate root; their one path is native_theme::icons.
  • native-theme: system_icon_theme(), detect_icon_theme() and system_icon_set() are in detect, beside system_is_dark() and detect_is_dark() and with the DetectionContext::icon_theme() cache they share: native_theme::detect::system_icon_theme (it was theme::system_icon_theme, and system_icon_set was theme::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, TransformAnimation and TransformData (they were in theme: native_theme::theme::IconRole is native_theme::icons::IconRole), with RgbaData; one use native_theme::icons::{IconRole, MaterialLoader}; imports a loader and its role. IconSet and IconSizes, which a theme states, stay in theme.
  • native-theme: IconSet::SegoeIcons is IconSet::SegoeFluent and SegoeIconsLoader is SegoeFluentLoader, matching the set's serialised name segoe-fluent, which is unchanged (TOML icon_set = "segoe-fluent", the native-theme-build mapping key and its segoe-fluent/ directory); code native-theme-build generates names IconSet::SegoeFluent.
  • native-theme: FreedesktopLoader::size takes a u32, the pixel type of rasterize_svg and IconData::rgba (it was u16), clamped as the other loaders' sizes are (the MAX_ICON_SIZE line below).
  • native-theme: the system loaders' color and color_opt (FreedesktopLoader, SfSymbolsLoader, SegoeFluentLoader) take an Rgba and an Option<Rgba>, the crate's colour type that colorize_monochrome_svg takes (they took [u8; 3]); the alpha is ignored, as the icon keeps its own coverage.
  • native-theme: FreedesktopLoader::theme(name) looks an icon up in name only where that is an installed icon theme, one whose index.theme lists Directories: .theme("default"), a theme that only inherits Adwaita, loads None, where it loaded Adwaita's icons through default's Inherits= (see Fixed).
  • native-theme: icons::load_icon_indicator(set, theme) takes the freedesktop theme to take the spinner from (None for 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 GenerateOutput and BuildErrors is private, read through accessors: GenerateOutput::{output_path, warnings, role_count, bundled_theme_count, svg_count, total_svg_bytes, rerun_paths, code}() and BuildErrors::{errors, rerun_paths}() (output.code is output.code(); rerun_paths was a public field on BuildErrors and a method on GenerateOutput).
  • native-theme-build: build errors name the file that holds the mistake: BundledDeAware has a mapping_file, DuplicateTheme a file, ThemeOverlap and InvalidIdentifier a source_file: Option<String> (None for a check on the merged configs of several files), and MissingSvg the role whose mapping names the SVG. InvalidIconName has a reason, 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 its offending character is now the one that breaks that rule.
  • native-theme-iced, native-theme-gpui, native-theme-egui: from_system() returns a Loaded, 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_set and icon_theme, the OS colour mode's icon theme (SystemTheme::icon_theme_for) that the iced Spinner::new and 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 the is_dark they carried. The iced and gpui SystemThemeExt::to_iced_theme and to_gpui_theme return the same Loaded (they returned (theme, resolved) and the gpui Theme alone); egui's to_egui_atlas keeps returning the ThemeAtlas, which carries all of it already. from_preset is unchanged.
  • native-theme-iced: the accessors are named after the ResolvedTheme leaf they read, without the leading defaults. 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 ResolvedTheme leaf 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 ResolvedTheme leaf 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, DialogTheme and ListTheme gain a field each, show_delay, entrance and header_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: SwitchTheme gains 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_color and thumb_border_line_width. SwitchTheme is not #[non_exhaustive], so a struct literal of it outside the crate needs the new fields or ..Default::default(); ResolvedSwitchTheme is #[non_exhaustive] already.
  • native-theme: TabTheme gains active_outward_corner_radius (see Added); TabTheme is 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_animation takes the period as a NonZeroU32, the type of TransformAnimation::Spin's duration_ms, which it is passed as it is (no .get()); AnimatedImageSources::frame_duration_ms is a NonZeroU32 as the icon data's frame duration is. A period of 0 made gpui's rotation NaN, a panic in a debug build (gpui-pre's debug_assert! that the animated value is finite), and no longer compiles.
  • native-theme-gpui: CheckboxLook, SwitchLook, SliderLook, ProgressBarLook, SeparatorLook, SpinnerLook, TabLook, TabIndicator, ScrollbarGeometry and AnimatedImageSources are #[non_exhaustive], as Loaded is, 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 ... A ScrollbarGeometry for apply_overrides or scrollbar_styles is made by scrollbar_geometry(&resolved) and edited field by field.
  • native-theme-iced: AnimatedSvgHandles is #[non_exhaustive], as the gpui connector's AnimatedImageSources is.
  • native-theme-gpui: is_dark(resolved) is gone: it was a second name for is_dark_resolved(resolved), which stays for a caller holding only a ResolvedTheme.
  • native-theme-gpui: ActiveNativeTheme::native(): cx.native() gives the Native view the geometry builders take in one call, as cx.theme() gives gpui-component's theme (it was cx.native_theme().and_then(|t| t.native(cx))). It is a required method: a type outside the crate that implements ActiveNativeTheme must add it (the trait is implemented for App).
  • 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 from diameter and stroke. 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, macos or windows reader for the target it was built for; it now forwards them as the native feature, off by default as in native-theme and the iced connector, so without it from_system(), and the SystemTheme::from_system() whose result apply_system_theme takes, 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. Add features = ["native"] (the README's Quick start does), or name native-theme's kde, portal, macos or windows for one platform.
  • native-theme-egui-widgets: expander::ExpanderResponse is #[non_exhaustive], as the connector's Loaded is, so the fields egui's CollapsingResponse also 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_opacity is 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 wrap is links (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, macos or windows reader for the target it was built for; it now forwards them as the native feature, off by default as in native-theme and the iced and gpui connectors, so without it from_system(), the SystemTheme::from_system() whose result to_egui_atlas takes, and ThemeWatcher::system_rebuild 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. Add features = ["native"] to either crate (the READMEs' Quick start does), or name native-theme's kde, portal, macos or windows for one platform.
  • native-theme: icon sizes are clamped to 1..=icons::MAX_ICON_SIZE (512): FreedesktopLoader::size and rasterize::rasterize_svg, as the size of every icon loader this release adds is (SfSymbolsLoader::size, SegoeFluentLoader::size, icons::load_custom_icon and load_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_svg returns a 1 × 1 raster for it, where it returned an error. A loader that cannot draw the icon at the clamped size still returns None.
  • native-theme-iced: icons::custom_icon_to_image_handle and icons::custom_icon_to_svg_handle take (provider, icon_set, icon_theme: Option<&str>, color, size: Option<u32>), as egui's custom_icon_to_image_source takes the icon theme and size in its IconKey (they took (provider, icon_set, color)). They load through native_theme::icons::load_custom_icon_in, so a freedesktop icon comes from icon_theme (pass loaded.icon_theme.as_deref(), the icon theme the Spinner draws from), where it came from the system's even under a pinned preset whose variant names another (adwaita's Adwaita on a desktop whose icon theme is Breeze), and at size, clamped to 1..=MAX_ICON_SIZE, where every system-set icon came at its loader's default; None, None keeps the old behaviour.
  • native-theme-iced: icons::spin_rotation_radians takes the period as a NonZeroU32, the type of TransformAnimation::Spin's duration_ms, which it is passed as it is (no .get()), as gpui's with_spin_animation takes it; AnimatedSvgHandles::frame_duration_ms is a NonZeroU32 as the icon data's frame duration and gpui's AnimatedImageSources::frame_duration_ms are. A period of 0, which the function answered with an angle of 0, no longer compiles.
  • native-theme-iced: SystemThemeExt is sealed, as the egui connector's is: a private supertrait implemented for SystemTheme alone, 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_source takes (provider, icon_set, icon_theme: Option<&str>, color, size), as iced's custom_icon_to_image_handle and egui's custom_icon_to_image_source take the icon theme (it took (provider, icon_set, color, size)). It loads through native_theme::icons::load_custom_icon_in, so a freedesktop icon comes from icon_theme (pass loaded.icon_theme.as_deref(), the icon theme the Spinner draws from), where it came from the system's even under a pinned preset whose variant names another (adwaita's Adwaita on a desktop whose icon theme is Breeze); None keeps the old behaviour.
  • native-theme-gpui: SystemThemeExt is sealed, as the egui and iced connectors' are: a private supertrait implemented for SystemTheme alone, 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::PartBounds is Fn(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, and styles::segment lays only what the model lays over it: no fill at rest, hover_background as given under the pointer, active_background while selected. styles::segmented_control and styles::segment_separator take disabled: a disabled control is faded as one widget by segmented_control.disabled_opacity over the window, its group fill, outline, separators, segments and labels each composited over what it lies on (styles::segment's Status::Disabled the 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's spacing(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 are styles::segment_separator lines a consumer puts between the segments, container(space()).width(separator_width).height(Fill), in a row whose height is Shrink (the README's and styles::segmented_control's examples); a consumer who kept row(..).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-platform and its platform crates), which an application links anyway.