scaffold Flutter app — kernel, extensions, widgets, Tier 0 built-ins
First real content under app/. Lays the whole Tier 0 foundation in one
commit because the pieces depend on each other circularly (kernel →
extension → widgets → built-ins all reference types from the layer
below); splitting would leave intermediate commits that don't compile.
Key shapes:
* bare WidgetsApp root — no Material, no Cupertino, no Scaffold.
ClideTheme InheritedWidget is the only source of tokens.
* ClideKernel InheritedWidget aggregates 18 services (settings,
project, extensions, theme, panels, events, ipc, commands +
palette + keybindings, clipboard, files, notify, dialog, tray,
secrets, os, net, focus, log, i18n). ExtensionContext exposes
them through a stable interface.
* ClideExtension + sealed ContributionPoint hierarchy (Tab,
StatusItem, Toolbar, Command, TrayItem, LayoutPreset). One
manifest ships N contributions into kernel slots.
* Three-tier theme pipeline: palette (named colors) → semantic
roles → ~60 surface tokens. Defaults at each layer so legacy
palette-only themes produce a complete SurfaceTokens.
* A11y baked in from day one — every interactive primitive wraps
in Semantics(label:, hint:, button:); ensureSemantics() at boot;
theme/contrast.dart helper exposes token pairs for the WCAG
gate in the a11y test suite.
* i18n ported from fframe's L10n pattern (text-driven, namespaced
JSON catalogs, caller-supplied placeholders) with a proper
locale fallback chain (exact → language → default → placeholder)
fframe lacks.
* Four Tier-0 built-ins live (default-layout, welcome, ipc-status,
theme-picker) plus 17 id-reserving stubs so later tiers fill in
without rename churn.
.gitignore extended to cover app/ sub-package artefacts and the
Playwright harness scratch dirs.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:clide_app/widgets/src/clide_text.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum ClideButtonVariant { normal, primary, subtle }
|
||||
|
||||
class ClideButton extends StatefulWidget {
|
||||
const ClideButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onPressed,
|
||||
this.variant = ClideButtonVariant.normal,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
this.semanticLabel,
|
||||
this.semanticHint,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final VoidCallback? onPressed;
|
||||
final ClideButtonVariant variant;
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
/// Overrides [label] for screen readers (use when the visible label is
|
||||
/// an icon-only glyph or a noun that reads oddly when announced).
|
||||
final String? semanticLabel;
|
||||
|
||||
/// Screen-reader hint describing the button's effect. Optional.
|
||||
final String? semanticHint;
|
||||
|
||||
@override
|
||||
State<ClideButton> createState() => _ClideButtonState();
|
||||
}
|
||||
|
||||
class _ClideButtonState extends State<ClideButton> {
|
||||
bool _hovered = false;
|
||||
bool _pressed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final enabled = widget.onPressed != null;
|
||||
|
||||
Color bg;
|
||||
Color fg;
|
||||
switch (widget.variant) {
|
||||
case ClideButtonVariant.normal:
|
||||
bg = _pressed
|
||||
? tokens.buttonActiveBackground
|
||||
: _hovered
|
||||
? tokens.buttonHoverBackground
|
||||
: tokens.buttonBackground;
|
||||
fg = tokens.buttonForeground;
|
||||
case ClideButtonVariant.primary:
|
||||
bg = _pressed
|
||||
? tokens.panelActiveBorder
|
||||
: _hovered
|
||||
? tokens.buttonActiveBackground
|
||||
: tokens.buttonActiveBackground;
|
||||
fg = tokens.globalBackground;
|
||||
case ClideButtonVariant.subtle:
|
||||
bg = _hovered
|
||||
? tokens.listItemHoverBackground
|
||||
: tokens.listItemBackground;
|
||||
fg = tokens.listItemForeground;
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: enabled,
|
||||
label: widget.semanticLabel ?? widget.label,
|
||||
hint: widget.semanticHint,
|
||||
onTap: enabled ? widget.onPressed : null,
|
||||
excludeSemantics: true,
|
||||
child: MouseRegion(
|
||||
cursor:
|
||||
enabled ? SystemMouseCursors.click : SystemMouseCursors.forbidden,
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => setState(() => _pressed = true),
|
||||
onTapCancel: () => setState(() => _pressed = false),
|
||||
onTapUp: (_) {
|
||||
setState(() => _pressed = false);
|
||||
widget.onPressed?.call();
|
||||
},
|
||||
child: Container(
|
||||
padding: widget.padding,
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
border: Border.all(color: tokens.buttonBorder, width: 1),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: ClideText(
|
||||
widget.label,
|
||||
color: fg,
|
||||
fontWeight: widget.variant == ClideButtonVariant.primary
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ClideDivider extends StatelessWidget {
|
||||
const ClideDivider({
|
||||
super.key,
|
||||
this.axis = Axis.horizontal,
|
||||
this.thickness = 1,
|
||||
});
|
||||
|
||||
final Axis axis;
|
||||
final double thickness;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = ClideTheme.of(context).surface.dividerColor;
|
||||
return Container(
|
||||
width: axis == Axis.vertical ? thickness : null,
|
||||
height: axis == Axis.horizontal ? thickness : null,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Stateless painter producing a single-color icon. Every icon in
|
||||
/// [icons/] subclasses this; widgets wrap with [ClideIcon] for size +
|
||||
/// color-from-theme.
|
||||
abstract class ClideIconPainter {
|
||||
const ClideIconPainter();
|
||||
|
||||
/// Paint the icon into a unit square (0,0 .. 1,1).
|
||||
void paint(Canvas canvas, Color color);
|
||||
}
|
||||
|
||||
class ClideIcon extends StatelessWidget {
|
||||
const ClideIcon(
|
||||
this.painter, {
|
||||
super.key,
|
||||
this.size = 14,
|
||||
this.color,
|
||||
});
|
||||
|
||||
final ClideIconPainter painter;
|
||||
final double size;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final resolved = color ?? ClideTheme.of(context).surface.globalForeground;
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CustomPaint(
|
||||
size: Size(size, size),
|
||||
painter: _IconPainterAdapter(painter: painter, color: resolved),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconPainterAdapter extends CustomPainter {
|
||||
_IconPainterAdapter({required this.painter, required this.color});
|
||||
|
||||
final ClideIconPainter painter;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas.save();
|
||||
canvas.scale(size.width, size.height);
|
||||
painter.paint(canvas, color);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _IconPainterAdapter old) =>
|
||||
old.painter != painter || old.color != color;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Thin themed scrollbar. Tier-0 shell; more refined scrolling (velocity
|
||||
/// multiplier, keyboard nav) lands with the editor.
|
||||
class ClideScrollbar extends StatelessWidget {
|
||||
const ClideScrollbar({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.child,
|
||||
this.axis = Axis.vertical,
|
||||
});
|
||||
|
||||
final ScrollController controller;
|
||||
final Widget child;
|
||||
final Axis axis;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return ScrollbarTheme(
|
||||
slider: tokens.scrollbarSlider,
|
||||
sliderHover: tokens.scrollbarSliderHover,
|
||||
track: tokens.scrollbarTrack,
|
||||
child: RawScrollbar(
|
||||
controller: controller,
|
||||
thumbColor: tokens.scrollbarSlider,
|
||||
thickness: 8,
|
||||
radius: const Radius.circular(4),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollbarTheme extends InheritedWidget {
|
||||
const ScrollbarTheme({
|
||||
super.key,
|
||||
required this.slider,
|
||||
required this.sliderHover,
|
||||
required this.track,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
final Color slider;
|
||||
final Color sliderHover;
|
||||
final Color track;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ScrollbarTheme old) =>
|
||||
slider != old.slider ||
|
||||
sliderHover != old.sliderHover ||
|
||||
track != old.track;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Themed container. Replaces Material's Card / Scaffold body surfaces
|
||||
/// for clide widgets — pulls color, border, and padding from the
|
||||
/// current theme's surface tokens.
|
||||
class ClideSurface extends StatelessWidget {
|
||||
const ClideSurface({
|
||||
super.key,
|
||||
this.child,
|
||||
this.color,
|
||||
this.border,
|
||||
this.padding = EdgeInsets.zero,
|
||||
this.width,
|
||||
this.height,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
final Color? color;
|
||||
final Color? border;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Container(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: padding,
|
||||
decoration: BoxDecoration(
|
||||
color: color ?? tokens.panelBackground,
|
||||
border: border == null ? null : Border.all(color: border!, width: 1),
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:clide_app/widgets/src/clide_icon.dart';
|
||||
import 'package:clide_app/widgets/src/clide_text.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
@immutable
|
||||
class ClideTabItem {
|
||||
const ClideTabItem({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final ClideIconPainter? icon;
|
||||
}
|
||||
|
||||
class ClideTabBar extends StatelessWidget {
|
||||
const ClideTabBar({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.activeId,
|
||||
required this.onSelect,
|
||||
this.height = 28,
|
||||
this.semanticContainerLabel,
|
||||
});
|
||||
|
||||
final List<ClideTabItem> items;
|
||||
final String? activeId;
|
||||
final ValueChanged<String> onSelect;
|
||||
final double height;
|
||||
|
||||
/// Optional container-level label for screen readers (e.g. "Sidebar tabs").
|
||||
final String? semanticContainerLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Semantics(
|
||||
container: true,
|
||||
label: semanticContainerLabel,
|
||||
explicitChildNodes: true,
|
||||
child: Container(
|
||||
height: height,
|
||||
color: tokens.tabBarBackground,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final item in items)
|
||||
_Tab(
|
||||
item: item,
|
||||
active: item.id == activeId,
|
||||
onTap: () => onSelect(item.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Tab extends StatefulWidget {
|
||||
const _Tab({required this.item, required this.active, required this.onTap});
|
||||
|
||||
final ClideTabItem item;
|
||||
final bool active;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
State<_Tab> createState() => _TabState();
|
||||
}
|
||||
|
||||
class _TabState extends State<_Tab> {
|
||||
bool _hovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final bg = widget.active
|
||||
? tokens.tabActive
|
||||
: (_hovered ? tokens.tabInactive : tokens.tabInactive);
|
||||
final fg = widget.active
|
||||
? tokens.tabActiveForeground
|
||||
: tokens.tabInactiveForeground;
|
||||
|
||||
return Semantics(
|
||||
button: true,
|
||||
selected: widget.active,
|
||||
label: widget.item.title,
|
||||
onTap: widget.onTap,
|
||||
excludeSemantics: true,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _hovered = true),
|
||||
onExit: (_) => setState(() => _hovered = false),
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: widget.active
|
||||
? tokens.tabActiveBorder
|
||||
: const Color(0x00000000),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.item.icon != null) ...[
|
||||
ClideIcon(widget.item.icon!, size: 12, color: fg),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
ClideText(widget.item.title, color: fg),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Theme-aware Text. Defaults pull from the global foreground token.
|
||||
class ClideText extends StatelessWidget {
|
||||
const ClideText(
|
||||
this.data, {
|
||||
super.key,
|
||||
this.color,
|
||||
this.fontSize = 13,
|
||||
this.fontWeight = FontWeight.w400,
|
||||
this.muted = false,
|
||||
this.maxLines,
|
||||
this.overflow,
|
||||
this.textAlign,
|
||||
});
|
||||
|
||||
final String data;
|
||||
final Color? color;
|
||||
final double fontSize;
|
||||
final FontWeight fontWeight;
|
||||
final bool muted;
|
||||
final int? maxLines;
|
||||
final TextOverflow? overflow;
|
||||
final TextAlign? textAlign;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final resolved =
|
||||
color ?? (muted ? tokens.globalTextMuted : tokens.globalForeground);
|
||||
return Text(
|
||||
data,
|
||||
maxLines: maxLines,
|
||||
overflow: overflow,
|
||||
textAlign: textAlign,
|
||||
style: TextStyle(
|
||||
color: resolved,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
fontFamilyFallback: const ['Inter', 'Helvetica', 'Arial', 'sans-serif'],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import 'package:clide_app/kernel/src/theme/controller.dart';
|
||||
import 'package:clide_app/widgets/src/clide_text.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Very small tooltip — hover a child to reveal a label. Uses an
|
||||
/// OverlayEntry to draw above everything else without Material.
|
||||
class ClideTooltip extends StatefulWidget {
|
||||
const ClideTooltip({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.child,
|
||||
this.showDelay = const Duration(milliseconds: 500),
|
||||
});
|
||||
|
||||
final String message;
|
||||
final Widget child;
|
||||
final Duration showDelay;
|
||||
|
||||
@override
|
||||
State<ClideTooltip> createState() => _ClideTooltipState();
|
||||
}
|
||||
|
||||
class _ClideTooltipState extends State<ClideTooltip> {
|
||||
OverlayEntry? _entry;
|
||||
|
||||
void _show() {
|
||||
final overlay = Overlay.maybeOf(context);
|
||||
if (overlay == null) return;
|
||||
_entry?.remove();
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
final offset = box.localToGlobal(Offset(0, box.size.height + 4));
|
||||
_entry = OverlayEntry(
|
||||
builder: (ctx) {
|
||||
final tokens = ClideTheme.of(ctx).surface;
|
||||
return Positioned(
|
||||
left: offset.dx,
|
||||
top: offset.dy,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.tooltipBackground,
|
||||
border: Border.all(color: tokens.tooltipBorder),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: ClideText(widget.message, color: tokens.tooltipForeground),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
overlay.insert(_entry!);
|
||||
}
|
||||
|
||||
void _hide() {
|
||||
_entry?.remove();
|
||||
_entry = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hide();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
tooltip: widget.message,
|
||||
child: MouseRegion(
|
||||
onEnter: (_) async {
|
||||
await Future<void>.delayed(widget.showDelay);
|
||||
if (mounted) _show();
|
||||
},
|
||||
onExit: (_) => _hide(),
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:clide_app/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CheckIcon extends ClideIconPainter {
|
||||
const CheckIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 0.14
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
final path = Path()
|
||||
..moveTo(0.18, 0.52)
|
||||
..lineTo(0.42, 0.74)
|
||||
..lineTo(0.82, 0.30);
|
||||
canvas.drawPath(path, p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import 'package:clide_app/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class ChevronRightIcon extends ClideIconPainter {
|
||||
const ChevronRightIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.12
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
final path = Path()
|
||||
..moveTo(0.36, 0.22)
|
||||
..lineTo(0.66, 0.50)
|
||||
..lineTo(0.36, 0.78);
|
||||
canvas.drawPath(path, p);
|
||||
}
|
||||
}
|
||||
|
||||
class ChevronDownIcon extends ClideIconPainter {
|
||||
const ChevronDownIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.12
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round;
|
||||
final path = Path()
|
||||
..moveTo(0.22, 0.36)
|
||||
..lineTo(0.50, 0.66)
|
||||
..lineTo(0.78, 0.36);
|
||||
canvas.drawPath(path, p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:clide_app/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class DotIcon extends ClideIconPainter {
|
||||
const DotIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
canvas.drawCircle(const Offset(0.5, 0.5), 0.18, Paint()..color = color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:clide_app/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class FolderIcon extends ClideIconPainter {
|
||||
const FolderIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.08;
|
||||
final path = Path()
|
||||
..moveTo(0.08, 0.30)
|
||||
..lineTo(0.40, 0.30)
|
||||
..lineTo(0.48, 0.22)
|
||||
..lineTo(0.92, 0.22)
|
||||
..lineTo(0.92, 0.80)
|
||||
..lineTo(0.08, 0.80)
|
||||
..close();
|
||||
canvas.drawPath(path, p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:clide_app/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class GearIcon extends ClideIconPainter {
|
||||
const GearIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.08
|
||||
..strokeCap = StrokeCap.round;
|
||||
const center = Offset(0.5, 0.5);
|
||||
const teeth = 8;
|
||||
const innerR = 0.25;
|
||||
const outerR = 0.40;
|
||||
|
||||
final path = Path();
|
||||
for (var i = 0; i < teeth; i++) {
|
||||
final a1 = (i / teeth) * 2 * math.pi;
|
||||
final a2 = ((i + 0.5) / teeth) * 2 * math.pi;
|
||||
final p1 = center + Offset(math.cos(a1) * innerR, math.sin(a1) * innerR);
|
||||
final p2 = center + Offset(math.cos(a1) * outerR, math.sin(a1) * outerR);
|
||||
final p3 = center + Offset(math.cos(a2) * outerR, math.sin(a2) * outerR);
|
||||
final p4 = center + Offset(math.cos(a2) * innerR, math.sin(a2) * innerR);
|
||||
path
|
||||
..moveTo(p1.dx, p1.dy)
|
||||
..lineTo(p2.dx, p2.dy)
|
||||
..lineTo(p3.dx, p3.dy)
|
||||
..lineTo(p4.dx, p4.dy);
|
||||
}
|
||||
canvas.drawPath(path, p);
|
||||
canvas.drawCircle(center, 0.12, p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:clide_app/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Plug-shaped connection icon. Used by the ipc-status statusbar item
|
||||
/// and by future connection-state affordances.
|
||||
class PlugIcon extends ClideIconPainter {
|
||||
const PlugIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 0.10
|
||||
..strokeCap = StrokeCap.round;
|
||||
// body
|
||||
final body = Path()
|
||||
..moveTo(0.30, 0.30)
|
||||
..lineTo(0.60, 0.30)
|
||||
..lineTo(0.60, 0.55)
|
||||
..arcToPoint(const Offset(0.30, 0.55),
|
||||
radius: const Radius.circular(0.15), clockwise: false)
|
||||
..close();
|
||||
canvas.drawPath(body, p);
|
||||
// cord
|
||||
canvas.drawLine(const Offset(0.45, 0.60), const Offset(0.45, 0.88), p);
|
||||
// prongs
|
||||
canvas.drawLine(const Offset(0.38, 0.18), const Offset(0.38, 0.30), p);
|
||||
canvas.drawLine(const Offset(0.52, 0.18), const Offset(0.52, 0.30), p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:clide_app/widgets/src/clide_icon.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CloseIcon extends ClideIconPainter {
|
||||
const CloseIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 0.10
|
||||
..strokeCap = StrokeCap.round;
|
||||
canvas
|
||||
..drawLine(const Offset(0.22, 0.22), const Offset(0.78, 0.78), p)
|
||||
..drawLine(const Offset(0.78, 0.22), const Offset(0.22, 0.78), p);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user