Files
clide/lib/builtin/theme_picker/src/appearance_control.dart
T
jpmschweitzerandClaude Opus 4.8 18cd945380 refactor(theme): route consumer reads through ClideSettings.theme (T-473)
Migrate ClideTheme.of(context) reads across the widget, feature, and shell
layers to the unified ClideSettings.theme.of(context) facade (D-101), so
theme/i18n/fonts/settings share one widget-facing entry. The facade delegates
straight to ClideTheme, so behaviour is unchanged — goldens are unmoved.

The low-level theme provider keeps its direct ClideTheme.of: the facade is
built on it, and the two kernel sites (ClideTheme's own definition + the
panels drag-resize widget) stay direct to avoid a widgets→kernel import cycle.
Dead controller.dart/kernel.dart imports left by the sweep removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 21:47:15 +02:00

127 lines
4.5 KiB
Dart

import 'package:clide/builtin/theme_picker/src/theme_families.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:clide/widgets/widgets.dart';
import 'package:flutter/widgets.dart';
/// The Appearance category's custom theme control (T-452) — the one bespoke
/// settings control the schema engine defers to (registered as
/// `SettingsControlContribution('theme.picker')`).
///
/// Base themes render as selectable chips; a High-contrast toggle maps to the
/// `-hc` sibling (D-69) via the shared [theme_families] helpers. Selecting
/// applies live through [ThemeController] (which persists via theme_persistence)
/// — so this control owns its own apply + scope, unlike schema-keyed fields.
class AppearanceThemeControl extends StatelessWidget {
const AppearanceThemeControl({super.key});
static const ns = 'builtin.theme-picker';
@override
Widget build(BuildContext context) {
final controller = ClideKernel.of(context).theme;
final i = ClideSettings.i18n.of(context);
return ListenableBuilder(
listenable: controller,
builder: (context, _) {
final themes = baseThemes(controller.available);
final currentBase = baseThemeName(controller.currentName);
final hc = isHcName(controller.currentName);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final t in themes)
_ThemeChip(
label: t.displayName,
selected: t.name == currentBase,
onTap: () => controller.select(resolveThemeName(controller.available, t.name, highContrast: hc)),
),
],
),
const SizedBox(height: 12),
_HcToggle(
checked: hc,
label: i.string('toggle.highContrast', namespace: ns, placeholder: 'High contrast'),
onTap: () => controller.select(resolveThemeName(controller.available, currentBase, highContrast: !hc)),
),
],
);
},
);
}
}
class _ThemeChip extends StatelessWidget {
const _ThemeChip({required this.label, required this.selected, required this.onTap});
final String label;
final bool selected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final tokens = ClideSettings.theme.of(context).surface;
return Semantics(
button: true,
selected: selected,
label: label,
excludeSemantics: true,
child: ClideTappable(
cursor: SystemMouseCursors.click,
onTap: onTap,
builder: (ctx, hovered, _) => Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: selected ? tokens.buttonActiveBackground : (hovered ? tokens.listItemHoverBackground : tokens.panelBackground),
border: Border.all(color: selected ? tokens.panelActiveBorder : tokens.dividerColor),
borderRadius: BorderRadius.circular(5),
),
child: ClideText(label, color: selected ? tokens.globalBackground : tokens.globalForeground),
),
),
);
}
}
class _HcToggle extends StatelessWidget {
const _HcToggle({required this.checked, required this.label, required this.onTap});
final bool checked;
final String label;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final tokens = ClideSettings.theme.of(context).surface;
return Semantics(
checked: checked,
label: label,
excludeSemantics: true,
child: ClideTappable(
cursor: SystemMouseCursors.click,
onTap: onTap,
builder: (ctx, hovered, _) => Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 15,
height: 15,
decoration: BoxDecoration(
color: checked ? tokens.buttonBackground : (hovered ? tokens.listItemHoverBackground : null),
border: Border.all(color: checked ? tokens.buttonBackground : tokens.modalSurfaceBorder),
borderRadius: BorderRadius.circular(3),
),
child: checked ? ClideIcon(const CheckIcon(), size: 11, color: tokens.buttonForeground) : null,
),
const SizedBox(width: 8),
ClideText(label, color: tokens.listItemForeground),
],
),
),
);
}
}