panel splitters get keyboard parity and Semantics (T-111)
Drag handles for sidebar / context / editor split were pure pointer-Listeners — no Tab focus, no arrow-key adjust, no Semantics. Each now wraps in a FocusableActionDetector with arrow shortcuts (10 px fine / 50 px coarse for the column handles, 2% / 10% for the editor split) and a slider Semantics node that announces the current size. The CLI verb half is split out as T-119 and waits on T-99. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1808,3 +1808,4 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by,
|
|||||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-114', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 20:13:36', '2026-05-17 20:13:36', '2026-05-17 20:13:36', NULL, '0bad40fa7f165f828bb0c0757044491e', 1) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-114', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 20:13:36', '2026-05-17 20:13:36', '2026-05-17 20:13:36', NULL, '0bad40fa7f165f828bb0c0757044491e', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-118', 'status', 'backlog', 'done', NULL, '2026-05-17 20:53:41', '2026-05-17 20:53:41', '2026-05-17 20:53:41', NULL, 'ff0126655805fed0ebe45f313c5ceff7', 1) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-118', 'status', 'backlog', 'done', NULL, '2026-05-17 20:53:41', '2026-05-17 20:53:41', '2026-05-17 20:53:41', NULL, 'ff0126655805fed0ebe45f313c5ceff7', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-114', 'status', 'in_progress', 'done', NULL, '2026-05-18 07:04:38', '2026-05-18 07:04:38', '2026-05-18 07:04:38', NULL, '8cf198f8c824fa8f46277b7e22e68f0c', 1) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-114', 'status', 'in_progress', 'done', NULL, '2026-05-18 07:04:38', '2026-05-18 07:04:38', '2026-05-18 07:04:38', NULL, '8cf198f8c824fa8f46277b7e22e68f0c', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-111', 'status', 'in_progress', 'done', NULL, '2026-05-18 07:43:45', '2026-05-18 07:43:45', '2026-05-18 07:43:45', NULL, '792efcf7c772de7cda7069ea3fc3ac0a', 1) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
|||||||
@@ -1837,3 +1837,13 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority,
|
|||||||
4. Surface text-zoom (Ctrl +/-/0) in the palette so it''s discoverable.
|
4. Surface text-zoom (Ctrl +/-/0) in the palette so it''s discoverable.
|
||||||
|
|
||||||
Source: consultants.md "UX — Findings — [Minor]" + Strengths section.', 'done', 'low', NULL, NULL, NULL, '2026-05-17 18:48:28', '2026-05-18 07:04:38', NULL, '70c65844ee3e52bfcd6220b77ab8957a', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
Source: consultants.md "UX — Findings — [Minor]" + Strengths section.', 'done', 'low', NULL, NULL, NULL, '2026-05-17 18:48:28', '2026-05-18 07:04:38', NULL, '70c65844ee3e52bfcd6220b77ab8957a', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-111', 'story', 'T-97', 'drag-resize keyboard parity + clide panel resize CLI', '`drag_resize.dart` and `app.dart:870-912` are pure `Listener` pointer handlers — no keyboard equivalent, no Semantics node. Per the user/Claude parity rule (D-6), panel sizing should have a CLI affordance. Neither exists.
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
1. Add Semantics to drag-resize handles + keyboard activation (focus → arrow keys adjust width).
|
||||||
|
2. Add `clide panel resize <slot> <delta-or-absolute>` CLI verb.
|
||||||
|
|
||||||
|
Coordinate with T-99 (IPC implementation) if the CLI verb needs new dispatch wiring.
|
||||||
|
|
||||||
|
Source: consultants.md "UX — Findings — [Major] Drag-resize handles".', 'done', 'medium', NULL, NULL, NULL, '2026-05-17 18:48:14', '2026-05-18 07:43:45', NULL, '594d1cbcc8daa115246e2398d9ba9506', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-119', 'task', 'T-99', 'clide panel resize CLI verb (split from T-111)', 'Once T-99 lands an IPC dispatch path, register a ''panel.resize'' command that takes <slot> and <delta-or-absolute> args and calls LayoutArrangement.setSize / setEditorRatio. Keyboard parity already landed in T-111; this completes user/Claude parity per D-6.', 'backlog', 'low', NULL, NULL, NULL, '2026-05-18 07:43:50', '2026-05-18 07:43:50', NULL, '29df32cde1a30168a27b763f5e6ce81d', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
|||||||
softer ink-tinted shadow (T-114).
|
softer ink-tinted shadow (T-114).
|
||||||
- Text-zoom (Ctrl +/-/0) is now a kernel `TextZoom` service and shows
|
- Text-zoom (Ctrl +/-/0) is now a kernel `TextZoom` service and shows
|
||||||
up in the palette as `View: Zoom In/Out/Reset Zoom` (T-114).
|
up in the palette as `View: Zoom In/Out/Reset Zoom` (T-114).
|
||||||
|
- Panel splitters (sidebar / context / editor-split) are tab-focusable;
|
||||||
|
arrow keys nudge by 10 px, Shift+arrow by 50 px (2% / 10% for the
|
||||||
|
editor split). Exposed as slider Semantics nodes so screen readers
|
||||||
|
announce the current size. CLI verb deferred to T-99 (T-111).
|
||||||
- Changelog gate is binary — dropped the soft 40-word warning, kept
|
- Changelog gate is binary — dropped the soft 40-word warning, kept
|
||||||
the 60-word hard cap. Warnings that never blocked just normalised
|
the 60-word hard cap. Warnings that never blocked just normalised
|
||||||
drift.
|
drift.
|
||||||
|
|||||||
+62
-19
@@ -967,36 +967,79 @@ class _EditorDragHandle extends StatefulWidget {
|
|||||||
|
|
||||||
class _EditorDragHandleState extends State<_EditorDragHandle> {
|
class _EditorDragHandleState extends State<_EditorDragHandle> {
|
||||||
bool _hovered = false;
|
bool _hovered = false;
|
||||||
|
bool _focused = false;
|
||||||
double? _dragStartRatio;
|
double? _dragStartRatio;
|
||||||
double? _dragStartY;
|
double? _dragStartY;
|
||||||
|
|
||||||
|
// Editor split is a 0..1 fraction; the kernel clamps to 0.15..0.70.
|
||||||
|
// 2% per fine step, 10% per Shift step keeps keyboard feel close to
|
||||||
|
// the pixel-based DragResizeHandle.
|
||||||
|
static const double _stepFine = 0.02;
|
||||||
|
static const double _stepCoarse = 0.10;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = ClideTheme.of(context).surface;
|
final tokens = ClideTheme.of(context).surface;
|
||||||
return MouseRegion(
|
final lineColor = (_hovered || _focused) ? tokens.panelActiveBorder : tokens.panelBorder;
|
||||||
cursor: SystemMouseCursors.resizeRow,
|
|
||||||
onEnter: (_) => setState(() => _hovered = true),
|
return Semantics(
|
||||||
onExit: (_) => setState(() => _hovered = false),
|
container: true,
|
||||||
child: Listener(
|
slider: true,
|
||||||
onPointerDown: (e) {
|
label: 'Editor split',
|
||||||
_dragStartRatio = widget.arrangement.editorRatio;
|
value: '${(widget.arrangement.editorRatio * 100).round()}%',
|
||||||
_dragStartY = e.position.dy;
|
onIncrease: () => _bump(_stepFine),
|
||||||
|
onDecrease: () => _bump(-_stepFine),
|
||||||
|
child: FocusableActionDetector(
|
||||||
|
onShowFocusHighlight: (v) => setState(() => _focused = v),
|
||||||
|
shortcuts: const <ShortcutActivator, Intent>{
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowUp): _EditorBumpIntent(-_stepFine),
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowDown): _EditorBumpIntent(_stepFine),
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): _EditorBumpIntent(-_stepCoarse),
|
||||||
|
SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): _EditorBumpIntent(_stepCoarse),
|
||||||
},
|
},
|
||||||
onPointerMove: (e) {
|
actions: <Type, Action<Intent>>{
|
||||||
final startR = _dragStartRatio;
|
_EditorBumpIntent: CallbackAction<_EditorBumpIntent>(
|
||||||
final startY = _dragStartY;
|
onInvoke: (intent) {
|
||||||
if (startR == null || startY == null || widget.totalHeight <= 0) return;
|
_bump(intent.delta);
|
||||||
final deltaRatio = (e.position.dy - startY) / widget.totalHeight;
|
return null;
|
||||||
widget.arrangement.setEditorRatio(startR + deltaRatio);
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
onPointerUp: (_) {
|
child: MouseRegion(
|
||||||
_dragStartRatio = null;
|
cursor: SystemMouseCursors.resizeRow,
|
||||||
_dragStartY = null;
|
onEnter: (_) => setState(() => _hovered = true),
|
||||||
},
|
onExit: (_) => setState(() => _hovered = false),
|
||||||
child: Container(height: 4, color: _hovered ? tokens.panelActiveBorder : tokens.panelBorder),
|
child: Listener(
|
||||||
|
onPointerDown: (e) {
|
||||||
|
_dragStartRatio = widget.arrangement.editorRatio;
|
||||||
|
_dragStartY = e.position.dy;
|
||||||
|
},
|
||||||
|
onPointerMove: (e) {
|
||||||
|
final startR = _dragStartRatio;
|
||||||
|
final startY = _dragStartY;
|
||||||
|
if (startR == null || startY == null || widget.totalHeight <= 0) return;
|
||||||
|
final deltaRatio = (e.position.dy - startY) / widget.totalHeight;
|
||||||
|
widget.arrangement.setEditorRatio(startR + deltaRatio);
|
||||||
|
},
|
||||||
|
onPointerUp: (_) {
|
||||||
|
_dragStartRatio = null;
|
||||||
|
_dragStartY = null;
|
||||||
|
},
|
||||||
|
child: Container(height: 4, color: lineColor),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _bump(double delta) {
|
||||||
|
widget.arrangement.setEditorRatio(widget.arrangement.editorRatio + delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditorBumpIntent extends Intent {
|
||||||
|
const _EditorBumpIntent(this.delta);
|
||||||
|
final double delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ContextSlot extends StatelessWidget {
|
class _ContextSlot extends StatelessWidget {
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import 'package:clide/kernel/src/panels/arrangement.dart';
|
import 'package:clide/kernel/src/panels/arrangement.dart';
|
||||||
import 'package:clide/kernel/src/panels/slot_id.dart';
|
import 'package:clide/kernel/src/panels/slot_id.dart';
|
||||||
import 'package:clide/kernel/src/theme/controller.dart';
|
import 'package:clide/kernel/src/theme/controller.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
/// A 4-px draggable splitter that adjusts the size of [slot] in the
|
/// A 4-px splitter that adjusts the size of [slot] in [arrangement].
|
||||||
/// given [arrangement]. Slot hosts wrap this around their edges to make
|
/// Slot hosts wrap this around their edges to make the three-column
|
||||||
/// the three-column layout resizable.
|
/// layout resizable.
|
||||||
|
///
|
||||||
|
/// Drag with the mouse, or tab to it and use the arrow keys (Shift =
|
||||||
|
/// coarse step). Exposes a `slider` Semantics node so screen readers
|
||||||
|
/// announce the current width.
|
||||||
class DragResizeHandle extends StatefulWidget {
|
class DragResizeHandle extends StatefulWidget {
|
||||||
const DragResizeHandle({
|
const DragResizeHandle({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -21,6 +26,8 @@ class DragResizeHandle extends StatefulWidget {
|
|||||||
final double thickness;
|
final double thickness;
|
||||||
|
|
||||||
static const defaultThickness = 8.0;
|
static const defaultThickness = 8.0;
|
||||||
|
static const double stepFine = 10.0;
|
||||||
|
static const double stepCoarse = 50.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DragResizeHandle> createState() => _DragResizeHandleState();
|
State<DragResizeHandle> createState() => _DragResizeHandleState();
|
||||||
@@ -28,32 +35,57 @@ class DragResizeHandle extends StatefulWidget {
|
|||||||
|
|
||||||
class _DragResizeHandleState extends State<DragResizeHandle> {
|
class _DragResizeHandleState extends State<DragResizeHandle> {
|
||||||
bool _hovered = false;
|
bool _hovered = false;
|
||||||
|
bool _focused = false;
|
||||||
double? _dragStartSize;
|
double? _dragStartSize;
|
||||||
Offset? _dragStartPointer;
|
Offset? _dragStartPointer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tokens = ClideTheme.of(context).surface;
|
final tokens = ClideTheme.of(context).surface;
|
||||||
final lineColor = _hovered ? tokens.panelActiveBorder : tokens.dividerColor;
|
final lineColor = (_hovered || _focused) ? tokens.panelActiveBorder : tokens.dividerColor;
|
||||||
|
|
||||||
return MouseRegion(
|
final size = widget.arrangement.sizeOf(widget.slot);
|
||||||
cursor: widget.axis == Axis.horizontal ? SystemMouseCursors.resizeColumn : SystemMouseCursors.resizeRow,
|
|
||||||
onEnter: (_) => setState(() => _hovered = true),
|
return Semantics(
|
||||||
onExit: (_) => setState(() => _hovered = false),
|
container: true,
|
||||||
child: Listener(
|
slider: true,
|
||||||
onPointerDown: _onDown,
|
label: _semanticLabel(),
|
||||||
onPointerMove: _onMove,
|
value: size == null ? null : '${size.round()} pixels',
|
||||||
onPointerUp: _onUp,
|
increasedValue: size == null ? null : '${(size + DragResizeHandle.stepFine).round()} pixels',
|
||||||
child: Container(
|
decreasedValue: size == null ? null : '${(size - DragResizeHandle.stepFine).round()} pixels',
|
||||||
width: widget.axis == Axis.horizontal ? widget.thickness : null,
|
onIncrease: () => _bump(DragResizeHandle.stepFine),
|
||||||
height: widget.axis == Axis.vertical ? widget.thickness : null,
|
onDecrease: () => _bump(-DragResizeHandle.stepFine),
|
||||||
color: tokens.chromeBackground,
|
child: FocusableActionDetector(
|
||||||
child: Align(
|
onShowFocusHighlight: (v) => setState(() => _focused = v),
|
||||||
alignment: widget.slot == Slots.sidebar ? Alignment.centerRight : Alignment.centerLeft,
|
shortcuts: _shortcuts(),
|
||||||
|
actions: <Type, Action<Intent>>{
|
||||||
|
_BumpIntent: CallbackAction<_BumpIntent>(
|
||||||
|
onInvoke: (intent) {
|
||||||
|
_bump(intent.delta);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: widget.axis == Axis.horizontal ? SystemMouseCursors.resizeColumn : SystemMouseCursors.resizeRow,
|
||||||
|
onEnter: (_) => setState(() => _hovered = true),
|
||||||
|
onExit: (_) => setState(() => _hovered = false),
|
||||||
|
child: Listener(
|
||||||
|
onPointerDown: _onDown,
|
||||||
|
onPointerMove: _onMove,
|
||||||
|
onPointerUp: _onUp,
|
||||||
child: Container(
|
child: Container(
|
||||||
width: widget.axis == Axis.horizontal ? 1 : null,
|
width: widget.axis == Axis.horizontal ? widget.thickness : null,
|
||||||
height: widget.axis == Axis.vertical ? 1 : null,
|
height: widget.axis == Axis.vertical ? widget.thickness : null,
|
||||||
color: lineColor,
|
color: tokens.chromeBackground,
|
||||||
|
child: Align(
|
||||||
|
alignment: widget.slot == Slots.sidebar ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
|
child: Container(
|
||||||
|
width: widget.axis == Axis.horizontal ? (_focused ? 2 : 1) : null,
|
||||||
|
height: widget.axis == Axis.vertical ? (_focused ? 2 : 1) : null,
|
||||||
|
color: lineColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -61,6 +93,43 @@ class _DragResizeHandleState extends State<DragResizeHandle> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _semanticLabel() {
|
||||||
|
final axis = widget.axis == Axis.horizontal ? 'width' : 'height';
|
||||||
|
if (widget.slot == Slots.sidebar) return 'Sidebar $axis';
|
||||||
|
if (widget.slot == Slots.contextPanel) return 'Context panel $axis';
|
||||||
|
return '${widget.slot.value} $axis';
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<ShortcutActivator, Intent> _shortcuts() {
|
||||||
|
final horiz = widget.axis == Axis.horizontal;
|
||||||
|
final fine = DragResizeHandle.stepFine;
|
||||||
|
final coarse = DragResizeHandle.stepCoarse;
|
||||||
|
return <ShortcutActivator, Intent>{
|
||||||
|
if (horiz) ...{
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowLeft): _BumpIntent(-fine),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowRight): _BumpIntent(fine),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): _BumpIntent(-coarse),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): _BumpIntent(coarse),
|
||||||
|
} else ...{
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowUp): _BumpIntent(-fine),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowDown): _BumpIntent(fine),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): _BumpIntent(-coarse),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true): _BumpIntent(coarse),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a raw delta in the natural axis direction. The drag handler
|
||||||
|
/// inverts the sign for context-panel because its handle sits on the
|
||||||
|
/// left edge of the right-anchored slot; arrow keys follow the same
|
||||||
|
/// convention so right-arrow always moves the boundary rightward.
|
||||||
|
void _bump(double rawDelta) {
|
||||||
|
final current = widget.arrangement.sizeOf(widget.slot);
|
||||||
|
if (current == null) return;
|
||||||
|
final delta = widget.slot == Slots.contextPanel ? -rawDelta : rawDelta;
|
||||||
|
widget.arrangement.setSize(widget.slot, current + delta);
|
||||||
|
}
|
||||||
|
|
||||||
void _onDown(PointerDownEvent e) {
|
void _onDown(PointerDownEvent e) {
|
||||||
_dragStartSize = widget.arrangement.sizeOf(widget.slot);
|
_dragStartSize = widget.arrangement.sizeOf(widget.slot);
|
||||||
_dragStartPointer = e.position;
|
_dragStartPointer = e.position;
|
||||||
@@ -80,3 +149,8 @@ class _DragResizeHandleState extends State<DragResizeHandle> {
|
|||||||
_dragStartPointer = null;
|
_dragStartPointer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _BumpIntent extends Intent {
|
||||||
|
const _BumpIntent(this.delta);
|
||||||
|
final double delta;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ library;
|
|||||||
import 'package:clide/extension/extension.dart';
|
import 'package:clide/extension/extension.dart';
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/semantics.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
@@ -83,6 +84,40 @@ void main() {
|
|||||||
expect(arr.sizeOf(Slots.contextPanel), 150);
|
expect(arr.sizeOf(Slots.contextPanel), 150);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('exposes a slider Semantics node with the current size', (tester) async {
|
||||||
|
final arr = LayoutArrangement();
|
||||||
|
arr.applyPreset(const LayoutPresetContribution(
|
||||||
|
id: 'test-preset',
|
||||||
|
displayName: 'Test',
|
||||||
|
slots: [
|
||||||
|
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 240),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
final semHandle = tester.ensureSemantics();
|
||||||
|
await tester.pumpWidget(harness(
|
||||||
|
f,
|
||||||
|
Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 40,
|
||||||
|
height: 200,
|
||||||
|
child: DragResizeHandle(
|
||||||
|
arrangement: arr,
|
||||||
|
slot: Slots.sidebar,
|
||||||
|
axis: Axis.horizontal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
final data = tester.getSemantics(find.byType(DragResizeHandle));
|
||||||
|
expect(data.label, 'Sidebar width');
|
||||||
|
expect(data.value, '240 pixels');
|
||||||
|
final actions = data.getSemanticsData().actions;
|
||||||
|
expect(actions & SemanticsAction.increase.index, isNot(0));
|
||||||
|
expect(actions & SemanticsAction.decrease.index, isNot(0));
|
||||||
|
semHandle.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('hovered state flips the line colour without throwing', (tester) async {
|
testWidgets('hovered state flips the line colour without throwing', (tester) async {
|
||||||
final arr = LayoutArrangement();
|
final arr = LayoutArrangement();
|
||||||
arr.applyPreset(const LayoutPresetContribution(
|
arr.applyPreset(const LayoutPresetContribution(
|
||||||
|
|||||||
Reference in New Issue
Block a user