dissolve app/ into repo root (D-056)
Single Flutter package at the repo root. All code, tests, assets, and platform directories moved from app/ to root. Package renamed from clide_app to clide — all imports rewritten. Merged pubspec combines core (ffi) and app (flutter, yaml, xterm) dependencies. Makefile simplified: no APP_PRESENT conditionals, no cd, no daemon lifecycle. 317 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
import 'package:clide/extension/src/contribution.dart';
|
||||
import 'package:clide/kernel/src/panels/registry.dart';
|
||||
import 'package:clide/kernel/src/panels/slot_id.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class LayoutArrangement extends ChangeNotifier {
|
||||
LayoutArrangement();
|
||||
|
||||
final Map<SlotId, _SlotState> _state = {};
|
||||
|
||||
Map<SlotId, _SlotState>? _focusModeSnapshot;
|
||||
SlotId? _focusModeSlot;
|
||||
|
||||
bool _editorOpen = false;
|
||||
double _editorRatio = 0.35;
|
||||
|
||||
void applyPreset(LayoutPresetContribution preset) {
|
||||
_state.clear();
|
||||
_focusModeSnapshot = null;
|
||||
_focusModeSlot = null;
|
||||
for (final slot in preset.slots) {
|
||||
_state[slot.slot] = _SlotState(
|
||||
position: slot.position,
|
||||
size: slot.defaultSize,
|
||||
minSize: slot.minSize,
|
||||
maxSize: slot.maxSize,
|
||||
visible: slot.visible,
|
||||
);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Iterable<SlotId> get slotsInOrder => _state.keys;
|
||||
|
||||
SlotPosition? positionOf(SlotId id) => _state[id]?.position;
|
||||
double? sizeOf(SlotId id) => _state[id]?.size;
|
||||
double? minSizeOf(SlotId id) => _state[id]?.minSize;
|
||||
double? maxSizeOf(SlotId id) => _state[id]?.maxSize;
|
||||
bool isVisible(SlotId id) => _state[id]?.visible ?? false;
|
||||
bool isCollapsed(SlotId id) => _state[id]?.collapsed ?? false;
|
||||
bool get isInFocusMode => _focusModeSlot != null;
|
||||
SlotId? get focusModeSlot => _focusModeSlot;
|
||||
bool get editorOpen => _editorOpen;
|
||||
double get editorRatio => _editorRatio;
|
||||
|
||||
void setSize(SlotId id, double size) {
|
||||
final s = _state[id];
|
||||
if (s == null) return;
|
||||
final clamped = size.clamp(s.minSize ?? 0, s.maxSize ?? double.infinity).toDouble();
|
||||
if (s.size == clamped) return;
|
||||
_state[id] = s.copyWith(size: clamped);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setVisible(SlotId id, bool visible) {
|
||||
final s = _state[id];
|
||||
if (s == null || s.visible == visible) return;
|
||||
_state[id] = s.copyWith(visible: visible);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setCollapsed(SlotId id, bool collapsed) {
|
||||
final s = _state[id];
|
||||
if (s == null || s.collapsed == collapsed) return;
|
||||
_state[id] = s.copyWith(collapsed: collapsed);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleCollapsed(SlotId id) {
|
||||
final s = _state[id];
|
||||
if (s == null) return;
|
||||
_state[id] = s.copyWith(collapsed: !s.collapsed);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void enterFocusMode(SlotId slot) {
|
||||
if (_focusModeSlot != null) return;
|
||||
_focusModeSnapshot = {for (final e in _state.entries) e.key: e.value};
|
||||
_focusModeSlot = slot;
|
||||
for (final id in _state.keys) {
|
||||
if (id == slot) {
|
||||
_state[id] = _state[id]!.copyWith(visible: true, collapsed: false);
|
||||
} else {
|
||||
_state[id] = _state[id]!.copyWith(visible: false);
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void exitFocusMode() {
|
||||
final snap = _focusModeSnapshot;
|
||||
if (snap == null) return;
|
||||
_state.clear();
|
||||
_state.addAll(snap);
|
||||
_focusModeSnapshot = null;
|
||||
_focusModeSlot = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleFocusMode(SlotId slot) {
|
||||
if (_focusModeSlot != null) {
|
||||
exitFocusMode();
|
||||
} else {
|
||||
enterFocusMode(slot);
|
||||
}
|
||||
}
|
||||
|
||||
void openEditor() {
|
||||
if (_editorOpen) return;
|
||||
_editorOpen = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void closeEditor() {
|
||||
if (!_editorOpen) return;
|
||||
_editorOpen = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void toggleEditor() {
|
||||
_editorOpen = !_editorOpen;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setEditorRatio(double ratio) {
|
||||
final clamped = ratio.clamp(0.15, 0.70);
|
||||
if (_editorRatio == clamped) return;
|
||||
_editorRatio = clamped;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void registerSlotsInto(PanelRegistry registry, LayoutPresetContribution preset) {
|
||||
for (final slot in preset.slots) {
|
||||
registry.registerSlot(SlotDefinition(
|
||||
id: slot.slot,
|
||||
position: slot.position,
|
||||
defaultSize: slot.defaultSize,
|
||||
minSize: slot.minSize,
|
||||
maxSize: slot.maxSize,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SlotState {
|
||||
const _SlotState({
|
||||
required this.position,
|
||||
this.size,
|
||||
this.minSize,
|
||||
this.maxSize,
|
||||
this.visible = true,
|
||||
this.collapsed = false,
|
||||
});
|
||||
|
||||
final SlotPosition position;
|
||||
final double? size;
|
||||
final double? minSize;
|
||||
final double? maxSize;
|
||||
final bool visible;
|
||||
final bool collapsed;
|
||||
|
||||
_SlotState copyWith({
|
||||
SlotPosition? position,
|
||||
double? size,
|
||||
double? minSize,
|
||||
double? maxSize,
|
||||
bool? visible,
|
||||
bool? collapsed,
|
||||
}) {
|
||||
return _SlotState(
|
||||
position: position ?? this.position,
|
||||
size: size ?? this.size,
|
||||
minSize: minSize ?? this.minSize,
|
||||
maxSize: maxSize ?? this.maxSize,
|
||||
visible: visible ?? this.visible,
|
||||
collapsed: collapsed ?? this.collapsed,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:clide/kernel/src/panels/arrangement.dart';
|
||||
import 'package:clide/kernel/src/panels/slot_id.dart';
|
||||
import 'package:clide/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// A 4-px draggable splitter that adjusts the size of [slot] in the
|
||||
/// given [arrangement]. Slot hosts wrap this around their edges to make
|
||||
/// the three-column layout resizable.
|
||||
class DragResizeHandle extends StatefulWidget {
|
||||
const DragResizeHandle({
|
||||
super.key,
|
||||
required this.arrangement,
|
||||
required this.slot,
|
||||
required this.axis,
|
||||
this.thickness = 4.0,
|
||||
});
|
||||
|
||||
final LayoutArrangement arrangement;
|
||||
final SlotId slot;
|
||||
final Axis axis;
|
||||
final double thickness;
|
||||
|
||||
@override
|
||||
State<DragResizeHandle> createState() => _DragResizeHandleState();
|
||||
}
|
||||
|
||||
class _DragResizeHandleState extends State<DragResizeHandle> {
|
||||
bool _hovered = false;
|
||||
double? _dragStartSize;
|
||||
Offset? _dragStartPointer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final color = _hovered ? tokens.panelActiveBorder : tokens.panelBorder;
|
||||
|
||||
return 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(
|
||||
width: widget.axis == Axis.horizontal ? widget.thickness : null,
|
||||
height: widget.axis == Axis.vertical ? widget.thickness : null,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDown(PointerDownEvent e) {
|
||||
_dragStartSize = widget.arrangement.sizeOf(widget.slot);
|
||||
_dragStartPointer = e.position;
|
||||
}
|
||||
|
||||
void _onMove(PointerMoveEvent e) {
|
||||
final start = _dragStartSize;
|
||||
final startPt = _dragStartPointer;
|
||||
if (start == null || startPt == null) return;
|
||||
final delta = widget.axis == Axis.horizontal
|
||||
? e.position.dx - startPt.dx
|
||||
: e.position.dy - startPt.dy;
|
||||
widget.arrangement.setSize(widget.slot, start + delta);
|
||||
}
|
||||
|
||||
void _onUp(PointerUpEvent _) {
|
||||
_dragStartSize = null;
|
||||
_dragStartPointer = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:clide/extension/src/contribution.dart';
|
||||
import 'package:clide/kernel/src/panels/slot_id.dart';
|
||||
|
||||
/// Canonical "three-column + statusbar" preset — the default-layout
|
||||
/// extension contributes this at Tier 0. Split out so tests and the
|
||||
/// default-layout extension share one source of truth.
|
||||
///
|
||||
/// Columns (px):
|
||||
/// sidebar 240 (drag 180–400)
|
||||
/// center flex (workspace on top, statusbar below)
|
||||
/// context 280 (drag 220–420)
|
||||
/// statusbar 26 (fixed height strip)
|
||||
LayoutPresetContribution classicPreset() => const LayoutPresetContribution(
|
||||
id: 'builtin.default-layout.classic',
|
||||
displayName: 'Classic',
|
||||
slots: [
|
||||
LayoutSlot(
|
||||
slot: Slots.sidebar,
|
||||
position: SlotPosition.left,
|
||||
defaultSize: 240,
|
||||
minSize: 180,
|
||||
maxSize: 400,
|
||||
),
|
||||
LayoutSlot(
|
||||
slot: Slots.workspace,
|
||||
position: SlotPosition.center,
|
||||
),
|
||||
LayoutSlot(
|
||||
slot: Slots.contextPanel,
|
||||
position: SlotPosition.right,
|
||||
defaultSize: 280,
|
||||
minSize: 220,
|
||||
maxSize: 420,
|
||||
),
|
||||
LayoutSlot(
|
||||
slot: Slots.statusbar,
|
||||
position: SlotPosition.bottom,
|
||||
defaultSize: 26,
|
||||
minSize: 26,
|
||||
maxSize: 26,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:clide/extension/src/contribution.dart';
|
||||
import 'package:clide/kernel/src/panels/slot_id.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@immutable
|
||||
class SlotDefinition {
|
||||
const SlotDefinition({
|
||||
required this.id,
|
||||
required this.position,
|
||||
this.defaultSize,
|
||||
this.minSize,
|
||||
this.maxSize,
|
||||
});
|
||||
|
||||
final SlotId id;
|
||||
final SlotPosition position;
|
||||
final double? defaultSize;
|
||||
final double? minSize;
|
||||
final double? maxSize;
|
||||
}
|
||||
|
||||
class PanelRegistry extends ChangeNotifier {
|
||||
final Map<SlotId, SlotDefinition> _defs = {};
|
||||
final Map<SlotId, List<ContributionPoint>> _mounts = {};
|
||||
final Map<SlotId, String?> _activeTab = {};
|
||||
|
||||
void registerSlot(SlotDefinition def) {
|
||||
_defs[def.id] = def;
|
||||
_mounts.putIfAbsent(def.id, () => <ContributionPoint>[]);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void contribute(ContributionPoint point) {
|
||||
final slot = point.slot;
|
||||
if (slot == null) return; // non-slot contributions go elsewhere
|
||||
final list = _mounts.putIfAbsent(slot, () => <ContributionPoint>[]);
|
||||
list.add(point);
|
||||
list.sort((a, b) => _priority(a).compareTo(_priority(b)));
|
||||
// first tab-contribution in the sidebar/workspace/context becomes the
|
||||
// default active tab until the user picks another
|
||||
if (_activeTab[slot] == null && point is TabContribution) {
|
||||
_activeTab[slot] = point.id;
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void uncontribute(String contributionId) {
|
||||
for (final entry in _mounts.entries) {
|
||||
final before = entry.value.length;
|
||||
entry.value.removeWhere((c) => c.id == contributionId);
|
||||
if (entry.value.length != before) {
|
||||
if (_activeTab[entry.key] == contributionId) {
|
||||
_activeTab[entry.key] =
|
||||
entry.value.whereType<TabContribution>().isEmpty
|
||||
? null
|
||||
: entry.value.whereType<TabContribution>().first.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Iterable<SlotDefinition> get slots => _defs.values;
|
||||
SlotDefinition? definitionFor(SlotId id) => _defs[id];
|
||||
|
||||
List<ContributionPoint> contributionsFor(SlotId id) =>
|
||||
List.unmodifiable(_mounts[id] ?? const []);
|
||||
|
||||
List<TabContribution> tabsFor(SlotId id) =>
|
||||
contributionsFor(id).whereType<TabContribution>().toList();
|
||||
|
||||
String? activeTabIn(SlotId id) => _activeTab[id];
|
||||
|
||||
void activateTab(SlotId id, String tabId) {
|
||||
if (_activeTab[id] == tabId) return;
|
||||
_activeTab[id] = tabId;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int _priority(ContributionPoint p) {
|
||||
if (p is TabContribution) return p.priority;
|
||||
if (p is StatusItemContribution) return p.priority;
|
||||
if (p is ToolbarButtonContribution) return p.priority;
|
||||
if (p is TrayItemContribution) return p.priority;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@immutable
|
||||
class SlotId {
|
||||
const SlotId(this.value);
|
||||
final String value;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is SlotId && other.value == value;
|
||||
|
||||
@override
|
||||
int get hashCode => value.hashCode;
|
||||
|
||||
@override
|
||||
String toString() => 'SlotId($value)';
|
||||
}
|
||||
|
||||
/// Kernel-reserved slot ids. Extensions can declare new slots; these are
|
||||
/// the ones the default layout presets and the kernel services target.
|
||||
abstract class Slots {
|
||||
static const sidebar = SlotId('sidebar');
|
||||
static const workspace = SlotId('workspace');
|
||||
static const contextPanel = SlotId('context');
|
||||
static const statusbar = SlotId('statusbar');
|
||||
static const toolbar = SlotId('toolbar.main');
|
||||
static const commandPalette = SlotId('commandPalette');
|
||||
static const tray = SlotId('tray');
|
||||
static const fullscreen = SlotId('fullscreen');
|
||||
}
|
||||
|
||||
enum SlotPosition { left, right, top, bottom, center, float, popout }
|
||||
Reference in New Issue
Block a user