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>
127 lines
4.5 KiB
Dart
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),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|