Skip to content

Commit 23faf3e

Browse files
committed
Added tree.
1 parent 8664534 commit 23faf3e

9 files changed

Lines changed: 675 additions & 292 deletions

File tree

packages/flutter_omarchy/example/lib/gallery.dart

Lines changed: 390 additions & 288 deletions
Large diffs are not rendered by default.

packages/flutter_omarchy/lib/src/config/config.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ class OmarchyConfigData {
2020
);
2121
}
2222

23+
/// Watch for config changes and yield updated config data.
24+
///
25+
/// This only works on Linux and is based on receiving the SIGUSR2 signal.
2326
static Stream<OmarchyConfigData> watch() async* {
2427
if (kIsWeb || !Platform.isLinux) {
2528
return;

packages/flutter_omarchy/lib/src/widgets/scaffold.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import 'package:flutter/widgets.dart';
22
import 'package:flutter_omarchy/src/theme/theme.dart';
3-
import 'package:flutter_omarchy/src/widgets/side_panel.dart';
43
import 'package:flutter_omarchy/src/widgets/utils/panel_size.dart';
54
import 'package:flutter_omarchy/src/widgets/widgets.dart';
65

packages/flutter_omarchy/lib/src/widgets/side_panel.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class OmarchySidePanel extends StatefulWidget {
4040
this.direction = TextDirection.ltr,
4141
this.orientation = Axis.horizontal,
4242
this.minPanelMargin = 64.0,
43+
this.barrierColor,
4344
});
4445

4546
static OmarchySidePanelController controllerOf(BuildContext context) {
@@ -62,6 +63,7 @@ class OmarchySidePanel extends StatefulWidget {
6263
final PanelSize panelSize;
6364
final PanelSize? minPanelSize;
6465
final double minPanelMargin;
66+
final Color? barrierColor;
6567

6668
@override
6769
State<OmarchySidePanel> createState() => _OmarchySplitPanelState();
@@ -121,7 +123,7 @@ class _OmarchySplitPanelState extends State<OmarchySidePanel> {
121123
children: [
122124
Positioned.fill(
123125
child: Container(
124-
color: const Color(0x33000000),
126+
color: widget.barrierColor ?? const Color(0x33000000),
125127
child: GestureDetector(
126128
onTap: () {
127129
_controller.isVisible = false;

packages/flutter_omarchy/lib/src/widgets/toast.dart

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import 'dart:math' as math;
2+
3+
import 'package:flutter/foundation.dart';
4+
import 'package:flutter/widgets.dart';
5+
import 'package:flutter_omarchy/src/theme/theme.dart';
6+
import 'package:flutter_omarchy/src/widgets/icon_data.g.dart';
7+
import 'package:flutter_omarchy/src/widgets/utils/default_foreground.dart';
8+
import 'package:flutter_omarchy/src/widgets/utils/fade_in.dart';
9+
import 'package:flutter_omarchy/src/widgets/utils/grouping.dart';
10+
import 'package:flutter_omarchy/src/widgets/utils/pointer_area.dart';
11+
12+
class OmarchyTreeController<T> extends ChangeNotifier {
13+
OmarchyTreeController({Set<T>? expandedNodes})
14+
: _expandedNodes = expandedNodes ?? <T>{};
15+
16+
final Set<T> _expandedNodes;
17+
18+
void expand(OmarchyTreeNode<T> node) {
19+
if (_expandedNodes.add(node.id)) {
20+
notifyListeners();
21+
}
22+
}
23+
24+
void collapse(OmarchyTreeNode<T> node) {
25+
if (_expandedNodes.remove(node.id)) {
26+
notifyListeners();
27+
}
28+
}
29+
30+
bool isExpanded(OmarchyTreeNode<T> node) {
31+
return _expandedNodes.contains(node.id);
32+
}
33+
34+
void toggle(OmarchyTreeNode<T> node) {
35+
if (isExpanded(node)) {
36+
collapse(node);
37+
} else {
38+
expand(node);
39+
}
40+
}
41+
42+
void collapseAll() {
43+
if (_expandedNodes.isNotEmpty) {
44+
_expandedNodes.clear();
45+
notifyListeners();
46+
}
47+
}
48+
}
49+
50+
class OmarchyTreeScope<T> extends InheritedModel<T> {
51+
OmarchyTreeScope({
52+
super.key,
53+
required this.controller,
54+
required Set<T> selectedItems,
55+
required super.child,
56+
}) : selectedItems = Set.unmodifiable(selectedItems);
57+
58+
final OmarchyTreeController<T>? controller;
59+
final Set<T> selectedItems;
60+
61+
static OmarchyTreeController<T>? maybeOf<T>(BuildContext context) {
62+
final widget = context
63+
.dependOnInheritedWidgetOfExactType<OmarchyTreeScope<T>>();
64+
return widget?.controller;
65+
}
66+
67+
static bool? isExpanded<T>(BuildContext context, T id) {
68+
final controller = context
69+
.dependOnInheritedWidgetOfExactType<OmarchyTreeScope<T>>(aspect: id);
70+
if (controller == null) {
71+
return null;
72+
}
73+
return controller.selectedItems.contains(id);
74+
}
75+
76+
@override
77+
bool updateShouldNotify(covariant OmarchyTreeScope<T> oldWidget) {
78+
if (oldWidget.controller != controller) return true;
79+
return !setEquals(oldWidget.selectedItems, selectedItems);
80+
}
81+
82+
@override
83+
bool updateShouldNotifyDependent(
84+
covariant OmarchyTreeScope<T> oldWidget,
85+
Set<T> dependencies,
86+
) {
87+
if (oldWidget.controller != controller) return true;
88+
89+
for (final dependency in dependencies) {
90+
final wasSelected = oldWidget.selectedItems.contains(dependency);
91+
final isSelected = selectedItems.contains(dependency);
92+
if (wasSelected != isSelected) {
93+
return true;
94+
}
95+
}
96+
return false;
97+
}
98+
}
99+
100+
class OmarchyTree<T> extends StatelessWidget {
101+
const OmarchyTree({super.key, required this.children, this.controller});
102+
103+
final OmarchyTreeController<T>? controller;
104+
final List<OmarchyTreeNode<T>> children;
105+
106+
@override
107+
Widget build(BuildContext context) {
108+
final result = CustomScrollView(
109+
slivers: [
110+
for (var i = 0; i < children.length; i++)
111+
Grouping(
112+
position: (i, children.length),
113+
child: OmarchyTreeNodeSliver(node: children[i]),
114+
),
115+
],
116+
);
117+
if (controller != null) {
118+
return AnimatedBuilder(
119+
animation: controller!,
120+
builder: (context, _) {
121+
return OmarchyTreeScope<T>(
122+
controller: controller,
123+
selectedItems: controller!._expandedNodes,
124+
child: result,
125+
);
126+
},
127+
);
128+
}
129+
return result;
130+
}
131+
}
132+
133+
class OmarchyTreeNodeSliver<T> extends StatelessWidget {
134+
const OmarchyTreeNodeSliver({
135+
super.key,
136+
required this.node,
137+
this.fadeIn = false,
138+
});
139+
140+
final OmarchyTreeNode<T> node;
141+
final bool fadeIn;
142+
143+
@override
144+
Widget build(BuildContext context) {
145+
final theme = OmarchyTheme.of(context);
146+
final isExpanded =
147+
node.isExpanded ??
148+
OmarchyTreeScope.isExpanded<T>(context, node.id) == true;
149+
return SliverMainAxisGroup(
150+
slivers: [
151+
SliverToBoxAdapter(child: fadeIn ? FadeIn(child: node) : node),
152+
if (isExpanded)
153+
SliverPadding(
154+
padding: const EdgeInsets.only(left: 6.0),
155+
sliver: DecoratedSliver(
156+
decoration: BoxDecoration(
157+
border: Border(
158+
left: BorderSide(color: theme.colors.normal.black, width: 1),
159+
),
160+
),
161+
sliver: SliverPadding(
162+
padding: const EdgeInsets.only(left: 12.0),
163+
sliver: SliverMainAxisGroup(
164+
slivers: [
165+
for (var i = 0; i < node.children.length; i++)
166+
Grouping(
167+
position: (i, node.children.length),
168+
child: OmarchyTreeNodeSliver(
169+
fadeIn: true,
170+
node: node.children[i],
171+
),
172+
),
173+
],
174+
),
175+
),
176+
),
177+
),
178+
],
179+
);
180+
}
181+
}
182+
183+
class OmarchyTreeNode<T> extends StatelessWidget {
184+
const OmarchyTreeNode({
185+
super.key,
186+
required this.id,
187+
this.children = const [],
188+
this.isExpanded,
189+
this.icon,
190+
this.title,
191+
this.trailing,
192+
this.onTap,
193+
});
194+
195+
final T id;
196+
final bool? isExpanded;
197+
final Widget? title;
198+
final Widget? trailing;
199+
final Widget? icon;
200+
final List<OmarchyTreeNode<T>> children;
201+
final VoidCallback? onTap;
202+
203+
@override
204+
Widget build(BuildContext context) {
205+
final theme = OmarchyTheme.of(context);
206+
final icon = this.icon;
207+
final title = this.title;
208+
final trailing = this.trailing;
209+
final isExpanded =
210+
this.isExpanded ?? OmarchyTreeScope.isExpanded(context, id) == true;
211+
return PointerArea(
212+
onTap: () {
213+
final controller = OmarchyTreeScope.maybeOf<T>(context);
214+
if (controller != null) {
215+
controller.toggle(this);
216+
}
217+
onTap?.call();
218+
},
219+
builder: (context, state, _) => Padding(
220+
padding: const EdgeInsets.symmetric(vertical: 4.0),
221+
child: Row(
222+
spacing: 4,
223+
children: [
224+
if (children.isNotEmpty)
225+
AnimatedContainer(
226+
duration: const Duration(milliseconds: 200),
227+
transform: Matrix4.rotationZ(!isExpanded ? 0 : math.pi / 2),
228+
transformAlignment: Alignment.center,
229+
child: Icon(
230+
OmarchyIcons.codChevronRight,
231+
size: 12,
232+
color: switch (state) {
233+
_ when isExpanded => theme.colors.normal.blue,
234+
PointerState(isPressed: true) => theme.colors.normal.blue,
235+
PointerState(isHovering: true) => theme.colors.normal.white,
236+
_ => theme.colors.normal.black,
237+
},
238+
),
239+
)
240+
else
241+
SizedBox(width: 12, height: 12),
242+
if (icon != null)
243+
IconTheme(
244+
data: IconThemeData(color: theme.colors.normal.blue, size: 18),
245+
child: icon,
246+
),
247+
if (title != null)
248+
DefaultForeground(
249+
foreground: switch (state) {
250+
PointerState(isPressed: true) => theme.colors.bright.white,
251+
PointerState(isHovering: true) => theme.colors.selectedText,
252+
_ => theme.colors.bright.white,
253+
},
254+
textStyle: theme.text.normal,
255+
child: title,
256+
),
257+
const Spacer(),
258+
if (trailing != null)
259+
DefaultForeground(
260+
foreground: switch (state) {
261+
PointerState(
262+
isEnabled: true,
263+
isPressed: false,
264+
isHovering: true,
265+
) =>
266+
theme.colors.normal.white,
267+
_ => theme.colors.normal.black,
268+
},
269+
textStyle: theme.text.italic,
270+
child: trailing,
271+
),
272+
],
273+
),
274+
),
275+
);
276+
}
277+
}

packages/flutter_omarchy/lib/src/widgets/tree_view.dart

Whitespace-only changes.

packages/flutter_omarchy/lib/src/widgets/utils/fade_in.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class FadeIn extends StatefulWidget {
44
const FadeIn({
55
super.key,
66
required this.child,
7-
this.duration = const Duration(milliseconds: 300),
7+
this.duration = const Duration(milliseconds: 250),
88
});
99

1010
final Widget child;

packages/flutter_omarchy/lib/src/widgets/widgets.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export 'icon_data.g.dart';
2424
export 'tooltip.dart';
2525
export 'input_container.dart';
2626
export 'text_input.dart';
27+
export 'tree.dart';
2728
export 'select.dart';
2829
export 'split_panel.dart';
2930
export 'side_panel.dart';

0 commit comments

Comments
 (0)