lift text-zoom into kernel, surface it in the palette (T-114)
test / unit + widget + golden + a11y (push) Failing after 30s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m1s

Workspace text-zoom (Ctrl +/-/0) was local state on _RootShellState,
reachable only via the keymap intent path. Lifted to a kernel TextZoom
ChangeNotifier so the new `view.zoomIn/Out/Reset` palette commands
mutate the same number the keymap does — closing T-114's "discoverable
in the palette" item.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-18 09:04:43 +02:00
co-authored by Claude
parent 6d4a642773
commit 044d1b2ff1
11 changed files with 174 additions and 9 deletions
+8 -9
View File
@@ -55,24 +55,23 @@ class _RootShell extends StatefulWidget {
class _RootShellState extends State<_RootShell> {
late final FocusNode _keyFocus;
double _textScale = 1.0;
static const double _scaleStep = 0.05;
static const double _scaleMin = 0.6;
static const double _scaleMax = 2.0;
@override
void initState() {
super.initState();
_keyFocus = FocusNode()..requestFocus();
widget.services.textZoom.addListener(_onZoom);
}
@override
void dispose() {
widget.services.textZoom.removeListener(_onZoom);
_keyFocus.dispose();
super.dispose();
}
void _onZoom() => setState(() {});
@override
Widget build(BuildContext context) {
final tokens = ClideTheme.of(context).surface;
@@ -86,24 +85,24 @@ class _RootShellState extends State<_RootShell> {
fontFamilyFallback: clideUiFamilyFallback,
),
child: MediaQuery(
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(_textScale)),
data: MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(widget.services.textZoom.scale)),
child: Actions(
actions: <Type, Action<Intent>>{
TextScaleIncreaseIntent: CallbackAction<TextScaleIncreaseIntent>(
onInvoke: (_) {
setState(() => _textScale = (_textScale + _scaleStep).clamp(_scaleMin, _scaleMax));
widget.services.textZoom.increase();
return null;
},
),
TextScaleDecreaseIntent: CallbackAction<TextScaleDecreaseIntent>(
onInvoke: (_) {
setState(() => _textScale = (_textScale - _scaleStep).clamp(_scaleMin, _scaleMax));
widget.services.textZoom.decrease();
return null;
},
),
TextScaleResetIntent: CallbackAction<TextScaleResetIntent>(
onInvoke: (_) {
setState(() => _textScale = 1.0);
widget.services.textZoom.reset();
return null;
},
),
+57
View File
@@ -0,0 +1,57 @@
import 'package:clide/clide.dart';
import 'package:clide/extension/extension.dart';
import 'package:clide/kernel/kernel.dart';
/// Surfaces view-level commands (currently the three text-zoom verbs)
/// in the command palette so they're discoverable. The keybindings
/// themselves are owned by the default keymap; commands here exist so
/// users browsing `Ctrl+Shift+P` see the same actions.
class ViewExtension extends ClideExtension {
ViewExtension({required this.textZoom});
final TextZoom textZoom;
@override
String get id => 'builtin.view';
@override
String get title => 'View';
@override
String get version => '0.1.0';
@override
Future<void> activate(ClideExtensionContext ctx) async {}
@override
List<ContributionPoint> get contributions => [
// Keybindings live in `assets/keymaps/default.yaml` against the
// text.scale* intents — registering a `defaultBinding` here too
// would shadow them. Palette discovery is the only goal.
CommandContribution(
id: 'view.zoomIn',
command: 'view.zoomIn',
title: 'View: Zoom In',
run: (_) async {
textZoom.increase();
return IpcResponse.ok(id: '', data: {'scale': textZoom.scale});
},
),
CommandContribution(
id: 'view.zoomOut',
command: 'view.zoomOut',
title: 'View: Zoom Out',
run: (_) async {
textZoom.decrease();
return IpcResponse.ok(id: '', data: {'scale': textZoom.scale});
},
),
CommandContribution(
id: 'view.zoomReset',
command: 'view.zoomReset',
title: 'View: Reset Zoom',
run: (_) async {
textZoom.reset();
return IpcResponse.ok(id: '', data: {'scale': textZoom.scale});
},
),
];
}
+1
View File
@@ -0,0 +1 @@
export 'src/extension.dart';
+1
View File
@@ -40,6 +40,7 @@ export 'src/os.dart';
export 'src/panels/arrangement.dart';
export 'src/project.dart';
export 'src/scheduler.dart';
export 'src/text_zoom.dart';
export 'src/secrets.dart';
export 'src/tray.dart';
export 'src/panels/drag_resize.dart';
+6
View File
@@ -5,6 +5,7 @@ import 'package:clide/clide.dart';
import 'package:clide/kernel/src/clipboard.dart';
import 'package:clide/kernel/src/commands/keybindings.dart';
import 'package:clide/kernel/src/keymap/keymap_service.dart';
import 'package:clide/kernel/src/text_zoom.dart';
import 'package:clide/kernel/src/commands/palette.dart';
import 'package:clide/kernel/src/commands/registry.dart';
import 'package:clide/kernel/src/dialog.dart';
@@ -65,6 +66,7 @@ class KernelServices {
required this.toolchain,
required this.scheduler,
required this.keymap,
required this.textZoom,
});
final Logger log;
@@ -94,6 +96,7 @@ class KernelServices {
final Toolchain toolchain;
final SchedulerService scheduler;
final KeymapService keymap;
final TextZoom textZoom;
static Future<KernelServices> boot({
required Directory appDir,
@@ -151,6 +154,7 @@ class KernelServices {
final tc = toolchain ?? Toolchain();
final scheduler = SchedulerService(events);
scheduler.start();
final textZoom = TextZoom();
final project = ProjectManager(
log: log,
events: events,
@@ -225,6 +229,7 @@ class KernelServices {
toolchain: tc,
scheduler: scheduler,
keymap: keymap,
textZoom: textZoom,
);
}
@@ -247,6 +252,7 @@ class KernelServices {
extensions.dispose();
await scheduler.dispose();
keymap.dispose();
textZoom.dispose();
await log.dispose();
messages.dispose();
await events.dispose();
+28
View File
@@ -0,0 +1,28 @@
import 'package:flutter/foundation.dart';
/// Workspace-wide text zoom factor.
///
/// Owned by the kernel rather than the root widget so command-palette
/// entries, the keymap layer, and any future menu/CLI surface can mutate
/// the same number. The root `MediaQuery` listens via [ChangeNotifier].
class TextZoom extends ChangeNotifier {
TextZoom();
static const double minScale = 0.6;
static const double maxScale = 2.0;
static const double stepScale = 0.05;
double _scale = 1.0;
double get scale => _scale;
void increase() => _setScale(_scale + stepScale);
void decrease() => _setScale(_scale - stepScale);
void reset() => _setScale(1.0);
void _setScale(double next) {
final clamped = next.clamp(minScale, maxScale);
if (clamped == _scale) return;
_scale = clamped;
notifyListeners();
}
}
+2
View File
@@ -20,6 +20,7 @@ import 'package:clide/builtin/problems/problems.dart';
import 'package:clide/builtin/settings_ui/settings_ui.dart';
import 'package:clide/builtin/terminal/terminal.dart';
import 'package:clide/builtin/theme_picker/theme_picker.dart';
import 'package:clide/builtin/view/view.dart';
import 'package:clide/builtin/tickets/tickets.dart';
import 'package:clide/builtin/todos/todos.dart';
import 'package:clide/builtin/welcome/welcome.dart';
@@ -145,6 +146,7 @@ Future<void> main() async {
..register(CanvasExtension())
..register(GraphExtension())
// UI extensions
..register(ViewExtension(textZoom: services.textZoom))
..register(SettingsUiExtension())
..register(ExtensionsUiExtension())
..register(KeybindingsUiExtension())