Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions lib/core/ui/querya_control_tokens.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import 'package:flutter/widgets.dart';
import 'package:querya_desktop/core/layout/ui_scale.dart';

/// The raw numbers of the control scale (#1333). Constants so other token
/// classes can build `const` values from them; widgets read them through
/// [QueryaControlSize].
abstract final class QueryaControlMetrics {
static const double heightSm = 28;
static const double heightMd = 32;
static const double heightLg = 36;

static const double fontSm = 12;
static const double fontMd = 13;
static const double fontLg = 14;

static const double iconSm = 15;
static const double iconMd = 16;
static const double iconLg = 18;

/// One corner radius for buttons, icon buttons, dropdown triggers, search
/// fields and tab pills.
static const double radius = 6;
}

/// One of the three control sizes of the Querya UI kit. Buttons, icon buttons,
/// dropdown triggers, search fields and tab strips of one size share a height,
/// so a toolbar row built from them has one height (docs/ui-kit.md).
///
/// Values are logical px at `uiScale` 1; the `scaled*` helpers apply the
/// app-wide UI scale.
enum QueryaControlSize {
/// Tree headers, grid and panel toolbars, dense rows.
sm(
height: QueryaControlMetrics.heightSm,
horizontalPadding: 8,
fontSize: QueryaControlMetrics.fontSm,
iconSize: QueryaControlMetrics.iconSm,
iconGap: 6,
),

/// Top toolbars, editor toolbars, ERD, result bars.
md(
height: QueryaControlMetrics.heightMd,
horizontalPadding: 12,
fontSize: QueryaControlMetrics.fontMd,
iconSize: QueryaControlMetrics.iconMd,
iconGap: 6,
),

/// Dialog footers, forms, empty-state actions.
lg(
height: QueryaControlMetrics.heightLg,
horizontalPadding: 16,
fontSize: QueryaControlMetrics.fontLg,
iconSize: QueryaControlMetrics.iconLg,
iconGap: 8,
);

const QueryaControlSize({
required this.height,
required this.horizontalPadding,
required this.fontSize,
required this.iconSize,
required this.iconGap,
});

final double height;
final double horizontalPadding;
final double fontSize;
final double iconSize;
final double iconGap;

/// Corner radius; the same for every size.
double get radius => QueryaControlMetrics.radius;

double scaledHeight(BuildContext context) => context.scaled(height);

double scaledHorizontalPadding(BuildContext context) =>
context.scaled(horizontalPadding);

double scaledFontSize(BuildContext context) => context.scaled(fontSize);

double scaledIconSize(BuildContext context) => context.scaled(iconSize);

double scaledIconGap(BuildContext context) => context.scaled(iconGap);

double scaledRadius(BuildContext context) => context.scaled(radius);

/// The size a control without an explicit one uses here: the nearest
/// [QueryaControlScope], or [md].
static QueryaControlSize of(BuildContext context) =>
QueryaControlScope.maybeOf(context) ?? QueryaControlSize.md;
}

/// Sets the default [QueryaControlSize] of a subtree: a toolbar says its size
/// once and the controls in it follow. A control's own `size` still wins.
class QueryaControlScope extends InheritedWidget {
const QueryaControlScope({
super.key,
required this.size,
required super.child,
});

final QueryaControlSize size;

/// The size set by the nearest scope, or null when there is none.
static QueryaControlSize? maybeOf(BuildContext context) => context
.dependOnInheritedWidgetOfExactType<QueryaControlScope>()
?.size;

@override
bool updateShouldNotify(QueryaControlScope oldWidget) =>
size != oldWidget.size;
}
52 changes: 45 additions & 7 deletions lib/shared/widgets/querya_dropdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter/services.dart';
import 'package:querya_desktop/core/layout/ui_scale.dart';
import 'package:querya_desktop/core/motion/querya_motion.dart';
import 'package:querya_desktop/core/motion/querya_motion_context.dart';
import 'package:querya_desktop/core/ui/querya_control_tokens.dart';
import 'package:querya_desktop/shared/widgets/querya_dropdown_tokens.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';

Expand Down Expand Up @@ -47,6 +48,7 @@ class QueryaDropdown<T> extends material.StatefulWidget {
this.menuMaxHeight = QueryaDropdownTokens.menuMaxHeight,
this.hint,
this.compact = false,
this.size,
});

final T value;
Expand All @@ -60,9 +62,15 @@ class QueryaDropdown<T> extends material.StatefulWidget {
final double menuMaxHeight;
final String? hint;

/// Smaller trigger and rows for toolbars / panel headers.
/// Smaller trigger and rows for toolbars / panel headers (same as
/// [QueryaControlSize.sm]).
final bool compact;

/// Control size of the trigger. Without one: [QueryaControlSize.sm] when
/// [compact], else the nearest [QueryaControlScope], else
/// [QueryaControlSize.lg].
final QueryaControlSize? size;

@override
material.State<QueryaDropdown<T>> createState() => _QueryaDropdownState<T>();
}
Expand All @@ -75,6 +83,7 @@ class _QueryaDropdownState<T> extends material.State<QueryaDropdown<T>> {
List<material.Widget>? _cachedMenuChildren;
List<QueryaDropdownItem<T>>? _cachedMenuItems;
T? _cachedMenuValue;
QueryaControlSize? _cachedMenuSize;
var _closingWithExit = false;
var _exitGeneration = 0;

Expand Down Expand Up @@ -122,21 +131,35 @@ class _QueryaDropdownState<T> extends material.State<QueryaDropdown<T>> {
_controller = widget.controller ?? material.MenuController();
}
if (!listEquals(oldWidget.items, widget.items) ||
oldWidget.value != widget.value) {
oldWidget.value != widget.value ||
oldWidget.size != widget.size ||
oldWidget.compact != widget.compact) {
_cachedMenuChildren = null;
}
}

List<material.Widget> _menuChildren(ColorScheme cs) {
/// The size this dropdown draws at: its own [QueryaDropdown.size], else
/// [QueryaControlSize.sm] when compact, else the nearest control scope, else
/// [QueryaControlSize.lg].
QueryaControlSize _controlSize(material.BuildContext context) {
final explicit = widget.size;
if (explicit != null) return explicit;
if (widget.compact) return QueryaControlSize.sm;
return QueryaControlScope.maybeOf(context) ?? QueryaControlSize.lg;
}

List<material.Widget> _menuChildren(ColorScheme cs, QueryaControlSize size) {
if (_cachedMenuChildren != null &&
listEquals(_cachedMenuItems, widget.items) &&
_cachedMenuValue == widget.value) {
_cachedMenuValue == widget.value &&
_cachedMenuSize == size) {
return _cachedMenuChildren!;
}
_cachedMenuItems = List<QueryaDropdownItem<T>>.from(widget.items);
_cachedMenuValue = widget.value;
_cachedMenuSize = size;
_cachedMenuChildren = [
for (final item in widget.items) _menuItem(item, cs),
for (final item in widget.items) _menuItem(item, cs, size),
];
return _cachedMenuChildren!;
}
Expand All @@ -146,6 +169,7 @@ class _QueryaDropdownState<T> extends material.State<QueryaDropdown<T>> {
required String label,
required ColorScheme cs,
required bool expand,
required QueryaControlSize size,
}) {
final textColor =
widget.enabled ? cs.popoverForeground : cs.mutedForeground;
Expand All @@ -157,6 +181,7 @@ class _QueryaDropdownState<T> extends material.State<QueryaDropdown<T>> {
context,
textColor,
compact: widget.compact,
size: size,
),
);
if (expand) {
Expand All @@ -172,13 +197,18 @@ class _QueryaDropdownState<T> extends material.State<QueryaDropdown<T>> {
return widget.hint ?? '';
}

material.Widget _menuItem(QueryaDropdownItem<T> item, ColorScheme cs) {
material.Widget _menuItem(
QueryaDropdownItem<T> item,
ColorScheme cs,
QueryaControlSize size,
) {
return _QueryaDropdownMenuItem<T>(
item: item,
selected: item.value == widget.value,
enabled: widget.enabled && item.enabled,
colorScheme: cs,
compact: widget.compact,
size: size,
onPick: () {
widget.onSelected(item.value);
unawaited(_closeWithExit());
Expand All @@ -196,9 +226,11 @@ class _QueryaDropdownState<T> extends material.State<QueryaDropdown<T>> {
final borderColor = widget.enabled
? (_triggerHovered || _triggerFocused ? cs.ring : cs.border)
: cs.border.withValues(alpha: 0.4);
final size = _controlSize(context);
final triggerHeight = QueryaDropdownTokens.scaledTriggerHeight(
context,
compact: widget.compact,
size: size,
);
final chevronGap = context.scaled(QueryaDropdownTokens.triggerChevronGap);
final chevronSize = context.scaled(QueryaDropdownTokens.triggerChevronSize);
Expand All @@ -220,6 +252,7 @@ class _QueryaDropdownState<T> extends material.State<QueryaDropdown<T>> {
padding: QueryaDropdownTokens.scaledTriggerPadding(
context,
compact: widget.compact,
size: size,
),
decoration: material.BoxDecoration(
color: _triggerHovered
Expand All @@ -239,6 +272,7 @@ class _QueryaDropdownState<T> extends material.State<QueryaDropdown<T>> {
label: label,
cs: cs,
expand: widget.expandToParent || fieldWidth != null,
size: size,
),
material.SizedBox(width: chevronGap),
material.Icon(
Expand Down Expand Up @@ -310,7 +344,7 @@ class _QueryaDropdownState<T> extends material.State<QueryaDropdown<T>> {
final fieldWidth = widget.expandToParent
? null
: (widget.width != null ? context.scaled(widget.width!) : null);
final menuChildren = _menuChildren(cs);
final menuChildren = _menuChildren(cs, _controlSize(context));
final scaledMaxHeight = context.scaled(widget.menuMaxHeight);
final effectiveMaxHeight =
widget.items.length > QueryaDropdownTokens.menuScrollItemThreshold
Expand Down Expand Up @@ -460,6 +494,7 @@ class _QueryaDropdownMenuItem<T> extends material.StatefulWidget {
required this.enabled,
required this.colorScheme,
required this.compact,
required this.size,
required this.onPick,
});

Expand All @@ -468,6 +503,7 @@ class _QueryaDropdownMenuItem<T> extends material.StatefulWidget {
final bool enabled;
final ColorScheme colorScheme;
final bool compact;
final QueryaControlSize size;
final material.VoidCallback onPick;

@override
Expand Down Expand Up @@ -507,6 +543,7 @@ class _QueryaDropdownMenuItemState<T>
final itemHeight = QueryaDropdownTokens.scaledMenuItemHeight(
context,
compact: widget.compact,
size: widget.size,
);
final radius = context.scaled(QueryaDropdownTokens.menuBorderRadius);
final slot = context.scaled(QueryaDropdownTokens.selectedCheckSlotWidth);
Expand Down Expand Up @@ -564,6 +601,7 @@ class _QueryaDropdownMenuItemState<T>
cs.popoverForeground,
selected: widget.selected,
compact: widget.compact,
size: widget.size,
),
),
),
Expand Down
Loading
Loading