The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme, docs under docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import "dart:ui" show lerpDouble;
|
||||
|
||||
import "package:flutter/widgets.dart";
|
||||
|
||||
import "package:garage_ui/theme/garage_theme.dart";
|
||||
|
||||
// Rest -> hover geometry. A hairline: at rest its barely a mark on the edge
|
||||
// of the pane, and hover only just thickens it. The pill radius falls out of
|
||||
// the thickness so the two never drift.
|
||||
const double _restThickness = 2;
|
||||
const double _hoverThickness = 3.5;
|
||||
|
||||
const double _restAlpha = 0.35;
|
||||
const double _hoverAlpha = 0.55;
|
||||
const double _dragAlpha = 0.7;
|
||||
|
||||
// how long the fatten/brighten takes. short - this is a pointer response, not
|
||||
// a transition.
|
||||
const Duration _grow = Duration(milliseconds: 140);
|
||||
|
||||
/// The Garage scroll thumb.
|
||||
///
|
||||
/// Every scrollable in a [GarageApp] gets one of these through
|
||||
/// GarageScrollBehavior, so nothing has to wrap itself in a scrollbar by hand.
|
||||
/// Flutters own default is a [RawScrollbar] in stock grey - square ended, 8px,
|
||||
/// no relation to the scheme - which is what you were seeing before this
|
||||
/// existed.
|
||||
///
|
||||
/// It floats OVER the content (no track, no gutter) so turning it on doesnt
|
||||
/// reflow anything, and it holds itself visible while the pointer is anywhere
|
||||
/// in the pane rather than only while youre actually scrolling.
|
||||
class GarageScrollbar extends StatefulWidget {
|
||||
const GarageScrollbar({super.key, required this.child, this.controller});
|
||||
|
||||
final Widget child;
|
||||
|
||||
/// The controller of the scrollable underneath. Comes straight off
|
||||
/// ScrollableDetails when this is built by the scroll behaviour; null falls
|
||||
/// back to the PrimaryScrollController the same way [RawScrollbar] does.
|
||||
final ScrollController? controller;
|
||||
|
||||
@override
|
||||
State<GarageScrollbar> createState() => _GarageScrollbarState();
|
||||
}
|
||||
|
||||
class _GarageScrollbarState extends State<GarageScrollbar> {
|
||||
bool _hovered = false;
|
||||
bool _dragging = false;
|
||||
|
||||
ScrollController? get _controller =>
|
||||
widget.controller ?? PrimaryScrollController.maybeOf(context);
|
||||
|
||||
// RawScrollbar only tolerates thumbVisibility when its controller is wired
|
||||
// to exactly one live position - it asserts on everything else. We flip that
|
||||
// flag on hover rather than at construction, so the same checks have to
|
||||
// happen here first: a pane whose controller hasnt attached yet would
|
||||
// otherwise throw the moment the pointer crossed it, and one whose content
|
||||
// fits would paint a full height thumb over nothing.
|
||||
bool get _canHold {
|
||||
final c = _controller;
|
||||
if (c == null || !c.hasClients || c.positions.length != 1) return false;
|
||||
|
||||
final p = c.position;
|
||||
if (!p.hasContentDimensions) return false;
|
||||
return p.maxScrollExtent > p.minScrollExtent;
|
||||
}
|
||||
|
||||
void _setHovered(bool value) {
|
||||
if (_hovered == value) return;
|
||||
setState(() {
|
||||
_hovered = value;
|
||||
// a drag that ends outside the pane never reports back (the recognisers
|
||||
// cancel path doesnt route through handleThumbPressEnd), so dont let the
|
||||
// flag stick.
|
||||
if (!value) _dragging = false;
|
||||
});
|
||||
}
|
||||
|
||||
void _setDragging(bool value) {
|
||||
if (_dragging == value) return;
|
||||
setState(() => _dragging = value);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scheme = GarageTheme.maybeOf(context)?.colorScheme;
|
||||
// outside a GarageTheme (a bare test host, mostly) fall back to flutters
|
||||
// own grey rather than asserting - a scrollbar is never worth a red screen.
|
||||
final base = scheme?.mutedForeground ?? const Color(0xffbcbcbc);
|
||||
|
||||
final lit = _hovered || _dragging;
|
||||
|
||||
return MouseRegion(
|
||||
opaque: false,
|
||||
onEnter: (_) => _setHovered(true),
|
||||
onExit: (_) => _setHovered(false),
|
||||
child: Listener(
|
||||
onPointerUp: (_) => _setDragging(false),
|
||||
onPointerCancel: (_) => _setDragging(false),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween<double>(end: lit ? 1 : 0),
|
||||
duration: _grow,
|
||||
curve: Curves.easeOut,
|
||||
builder: (context, t, child) {
|
||||
final thickness = lerpDouble(_restThickness, _hoverThickness, t)!;
|
||||
final alpha = _dragging
|
||||
? _dragAlpha
|
||||
: lerpDouble(_restAlpha, _hoverAlpha, t)!;
|
||||
|
||||
return _GarageRawScrollbar(
|
||||
controller: widget.controller,
|
||||
thumbColor: base.withValues(alpha: alpha),
|
||||
thickness: thickness,
|
||||
radius: Radius.circular(thickness / 2),
|
||||
// null, not false: false would mean "definitely hidden" and
|
||||
// kill the fade in on scroll.
|
||||
thumbVisibility: lit && _canHold ? true : null,
|
||||
crossAxisMargin: 2,
|
||||
mainAxisMargin: 2,
|
||||
minThumbLength: 24,
|
||||
onDragChanged: _setDragging,
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The only reason this subclass exists: RawScrollbar keeps its drag state to
|
||||
// itself, and the thumb is meant to go full strength while youre holding it.
|
||||
class _GarageRawScrollbar extends RawScrollbar {
|
||||
const _GarageRawScrollbar({
|
||||
required super.child,
|
||||
required this.onDragChanged,
|
||||
super.controller,
|
||||
super.thumbVisibility,
|
||||
super.thumbColor,
|
||||
super.thickness,
|
||||
super.radius,
|
||||
super.crossAxisMargin,
|
||||
super.mainAxisMargin,
|
||||
super.minThumbLength,
|
||||
});
|
||||
|
||||
final ValueChanged<bool> onDragChanged;
|
||||
|
||||
@override
|
||||
RawScrollbarState<_GarageRawScrollbar> createState() =>
|
||||
_GarageRawScrollbarState();
|
||||
}
|
||||
|
||||
class _GarageRawScrollbarState extends RawScrollbarState<_GarageRawScrollbar> {
|
||||
@override
|
||||
void handleThumbPressStart(Offset localPosition) {
|
||||
super.handleThumbPressStart(localPosition);
|
||||
widget.onDragChanged(true);
|
||||
}
|
||||
|
||||
@override
|
||||
void handleThumbPressEnd(Offset localPosition, Velocity velocity) {
|
||||
super.handleThumbPressEnd(localPosition, velocity);
|
||||
widget.onDragChanged(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user