mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-14 11:39:17 +01:00
Made build portable
This commit is contained in:
104
dependencies/fluent_ui-3.12.0/lib/fluent_ui.dart
vendored
Normal file
104
dependencies/fluent_ui-3.12.0/lib/fluent_ui.dart
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
library fluent_ui;
|
||||
|
||||
export 'package:flutter/widgets.dart' hide TextBox;
|
||||
export 'package:flutter/material.dart'
|
||||
show
|
||||
Brightness,
|
||||
VisualDensity,
|
||||
ThemeMode,
|
||||
Feedback,
|
||||
FlutterLogo,
|
||||
CircleAvatar,
|
||||
kElevationToShadow,
|
||||
DateTimeRange,
|
||||
HourFormat,
|
||||
AnimatedIcon,
|
||||
AnimatedIcons,
|
||||
AnimatedIconData,
|
||||
DateUtils,
|
||||
SelectableDayPredicate,
|
||||
DatePickerMode,
|
||||
ReorderableListView,
|
||||
ReorderableDragStartListener,
|
||||
kThemeAnimationDuration,
|
||||
TooltipVisibility,
|
||||
TooltipTriggerMode,
|
||||
TextInputAction,
|
||||
MaterialLocalizations,
|
||||
TextSelectionTheme,
|
||||
TextSelectionThemeData,
|
||||
SelectableText;
|
||||
export 'package:scroll_pos/scroll_pos.dart';
|
||||
|
||||
export 'src/app.dart';
|
||||
export 'src/icons.dart';
|
||||
export 'src/localization.dart';
|
||||
export 'src/utils.dart';
|
||||
|
||||
export 'src/navigation/route.dart';
|
||||
|
||||
export 'src/controls/navigation/bottom_navigation.dart';
|
||||
export 'src/layout/page.dart';
|
||||
|
||||
export 'src/controls/inputs/buttons/base.dart';
|
||||
export 'src/controls/inputs/buttons/theme.dart';
|
||||
export 'src/controls/inputs/buttons/button.dart';
|
||||
export 'src/controls/inputs/buttons/icon_button.dart';
|
||||
export 'src/controls/inputs/buttons/filled_button.dart';
|
||||
export 'src/controls/inputs/buttons/outlined_button.dart';
|
||||
export 'src/controls/inputs/buttons/text_button.dart';
|
||||
|
||||
export 'src/controls/inputs/checkbox.dart';
|
||||
export 'src/controls/inputs/chip.dart';
|
||||
export 'src/controls/inputs/dropdown_button.dart';
|
||||
export 'src/controls/inputs/pill_button_bar.dart';
|
||||
export 'src/controls/inputs/radio_button.dart';
|
||||
export 'src/controls/inputs/rating.dart';
|
||||
export 'src/controls/inputs/split_button.dart';
|
||||
export 'src/controls/inputs/toggle_button.dart';
|
||||
export 'src/controls/inputs/toggle_switch.dart';
|
||||
export 'src/controls/inputs/slider.dart';
|
||||
|
||||
export 'src/controls/navigation/navigation_view/view.dart';
|
||||
export 'src/controls/navigation/tab_view.dart';
|
||||
export 'src/controls/navigation/tree_view.dart';
|
||||
|
||||
export 'src/controls/surfaces/calendar/calendar_view.dart';
|
||||
export 'src/controls/surfaces/bottom_sheet.dart';
|
||||
export 'src/controls/surfaces/card.dart';
|
||||
export 'src/controls/surfaces/commandbar.dart';
|
||||
export 'src/controls/surfaces/dialog.dart';
|
||||
export 'src/controls/surfaces/expander.dart';
|
||||
export 'src/controls/surfaces/flyout/flyout.dart';
|
||||
export 'src/controls/surfaces/info_bar.dart';
|
||||
export 'src/controls/surfaces/list_tile.dart';
|
||||
export 'src/controls/surfaces/progress_indicators.dart';
|
||||
export 'src/controls/surfaces/snackbar.dart';
|
||||
export 'src/controls/surfaces/tooltip.dart';
|
||||
|
||||
export 'src/controls/utils/divider.dart';
|
||||
export 'src/controls/utils/hover_button.dart';
|
||||
export 'src/controls/utils/info_badge.dart';
|
||||
export 'src/controls/utils/scrollbar.dart';
|
||||
|
||||
export 'src/controls/form/auto_suggest_box.dart';
|
||||
export 'src/controls/form/text_box.dart';
|
||||
export 'src/controls/form/combo_box.dart';
|
||||
export 'src/controls/form/pickers/date_picker.dart';
|
||||
export 'src/controls/form/pickers/time_picker.dart';
|
||||
export 'src/controls/form/text_form_box.dart';
|
||||
export 'src/controls/form/form_row.dart';
|
||||
export 'src/controls/form/selection_controls.dart';
|
||||
|
||||
export 'src/layout/dynamic_overflow.dart';
|
||||
|
||||
export 'src/styles/motion/page_transitions.dart';
|
||||
export 'src/styles/acrylic.dart';
|
||||
export 'src/styles/color.dart' hide ColorConst;
|
||||
export 'src/styles/mica.dart';
|
||||
export 'src/styles/theme.dart';
|
||||
export 'src/styles/typography.dart';
|
||||
|
||||
export 'src/styles/focus.dart';
|
||||
export 'src/utils/horizontal_scroll_view.dart';
|
||||
export 'src/utils/label.dart';
|
||||
94
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_all.dart
vendored
Normal file
94
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_all.dart
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that looks up messages for specific locales by
|
||||
// delegating to the appropriate library.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:implementation_imports, file_names, unnecessary_new
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
|
||||
// ignore_for_file:argument_type_not_assignable, invalid_assignment
|
||||
// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
|
||||
// ignore_for_file:comment_references
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
import 'package:intl/src/intl_helpers.dart';
|
||||
|
||||
import 'messages_ar.dart' as messages_ar;
|
||||
import 'messages_de.dart' as messages_de;
|
||||
import 'messages_en.dart' as messages_en;
|
||||
import 'messages_es.dart' as messages_es;
|
||||
import 'messages_fr.dart' as messages_fr;
|
||||
import 'messages_hi.dart' as messages_hi;
|
||||
import 'messages_pt.dart' as messages_pt;
|
||||
import 'messages_ru.dart' as messages_ru;
|
||||
import 'messages_zh.dart' as messages_zh;
|
||||
|
||||
typedef Future<dynamic> LibraryLoader();
|
||||
Map<String, LibraryLoader> _deferredLibraries = {
|
||||
'ar': () => new Future.value(null),
|
||||
'de': () => new Future.value(null),
|
||||
'en': () => new Future.value(null),
|
||||
'es': () => new Future.value(null),
|
||||
'fr': () => new Future.value(null),
|
||||
'hi': () => new Future.value(null),
|
||||
'pt': () => new Future.value(null),
|
||||
'ru': () => new Future.value(null),
|
||||
'zh': () => new Future.value(null),
|
||||
};
|
||||
|
||||
MessageLookupByLibrary? _findExact(String localeName) {
|
||||
switch (localeName) {
|
||||
case 'ar':
|
||||
return messages_ar.messages;
|
||||
case 'de':
|
||||
return messages_de.messages;
|
||||
case 'en':
|
||||
return messages_en.messages;
|
||||
case 'es':
|
||||
return messages_es.messages;
|
||||
case 'fr':
|
||||
return messages_fr.messages;
|
||||
case 'hi':
|
||||
return messages_hi.messages;
|
||||
case 'pt':
|
||||
return messages_pt.messages;
|
||||
case 'ru':
|
||||
return messages_ru.messages;
|
||||
case 'zh':
|
||||
return messages_zh.messages;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// User programs should call this before using [localeName] for messages.
|
||||
Future<bool> initializeMessages(String localeName) async {
|
||||
var availableLocale = Intl.verifiedLocale(
|
||||
localeName, (locale) => _deferredLibraries[locale] != null,
|
||||
onFailure: (_) => null);
|
||||
if (availableLocale == null) {
|
||||
return new Future.value(false);
|
||||
}
|
||||
var lib = _deferredLibraries[availableLocale];
|
||||
await (lib == null ? new Future.value(false) : lib());
|
||||
initializeInternalMessageLookup(() => new CompositeMessageLookup());
|
||||
messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
|
||||
return new Future.value(true);
|
||||
}
|
||||
|
||||
bool _messagesExistFor(String locale) {
|
||||
try {
|
||||
return _findExact(locale) != null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
|
||||
var actualLocale =
|
||||
Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
|
||||
if (actualLocale == null) return null;
|
||||
return _findExact(actualLocale);
|
||||
}
|
||||
63
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_ar.dart
vendored
Normal file
63
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_ar.dart
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a ar locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'ar';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"backButtonTooltip": MessageLookupByLibrary.simpleMessage("رجوع"),
|
||||
"clickToSearch": MessageLookupByLibrary.simpleMessage("انقر للبحث"),
|
||||
"closeButtonLabel": MessageLookupByLibrary.simpleMessage("إغلاق"),
|
||||
"closeNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("إغلاق الواجهة"),
|
||||
"closeTabLabelSuffix":
|
||||
MessageLookupByLibrary.simpleMessage("إغلاق علامة التبويب"),
|
||||
"closeWindowTooltip": MessageLookupByLibrary.simpleMessage("إغلاق"),
|
||||
"copyActionLabel": MessageLookupByLibrary.simpleMessage("نسخ"),
|
||||
"copyActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"انسخ المحتوى المحدد إلى الحافظة"),
|
||||
"cutActionLabel": MessageLookupByLibrary.simpleMessage("قص"),
|
||||
"cutActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"قم بإزالة المحتوى المحدد وضعه في الحافظة"),
|
||||
"dialogLabel": MessageLookupByLibrary.simpleMessage("مربع حوار"),
|
||||
"minimizeWindowTooltip": MessageLookupByLibrary.simpleMessage("تصغير"),
|
||||
"modalBarrierDismissLabel":
|
||||
MessageLookupByLibrary.simpleMessage("استبعاد"),
|
||||
"newTabLabel":
|
||||
MessageLookupByLibrary.simpleMessage("إضافة علامة تبويب جديدة"),
|
||||
"noResultsFoundLabel":
|
||||
MessageLookupByLibrary.simpleMessage("لم يتم العثور على نتائج"),
|
||||
"openNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("فتح الواجهة"),
|
||||
"pasteActionLabel": MessageLookupByLibrary.simpleMessage("لصق"),
|
||||
"pasteActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"إدراج محتويات الحافظة إلى الموقع الحالي"),
|
||||
"restoreWindowTooltip": MessageLookupByLibrary.simpleMessage("إسترجاع"),
|
||||
"scrollTabBackwardLabel": MessageLookupByLibrary.simpleMessage(
|
||||
"تمرير قائمة علامة التبويب للخلف"),
|
||||
"scrollTabForwardLabel": MessageLookupByLibrary.simpleMessage(
|
||||
"تمرير قائمة علامة التبويب إلى الأمام"),
|
||||
"searchLabel": MessageLookupByLibrary.simpleMessage("بحث"),
|
||||
"selectAllActionLabel":
|
||||
MessageLookupByLibrary.simpleMessage("تحديد الكل"),
|
||||
"selectAllActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("تحديد المحتوى بالكامل")
|
||||
};
|
||||
}
|
||||
66
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_de.dart
vendored
Normal file
66
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_de.dart
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a de locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'de';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"backButtonTooltip": MessageLookupByLibrary.simpleMessage("Zurück"),
|
||||
"clickToSearch":
|
||||
MessageLookupByLibrary.simpleMessage("Zum Suchen klicken"),
|
||||
"closeButtonLabel": MessageLookupByLibrary.simpleMessage("Schließen"),
|
||||
"closeNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Navigation schließen"),
|
||||
"closeTabLabelSuffix":
|
||||
MessageLookupByLibrary.simpleMessage("Tab schließen"),
|
||||
"closeWindowTooltip": MessageLookupByLibrary.simpleMessage("Schließen"),
|
||||
"copyActionLabel": MessageLookupByLibrary.simpleMessage("Kopieren"),
|
||||
"copyActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Ausgewählten Inhalt in die Zwischenablage kopieren"),
|
||||
"cutActionLabel": MessageLookupByLibrary.simpleMessage("Ausschneiden"),
|
||||
"cutActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Ausgewählten Inhalt entfernen und in die Zwischenablage legen"),
|
||||
"dialogLabel": MessageLookupByLibrary.simpleMessage("Dialog"),
|
||||
"minimizeWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Minimieren"),
|
||||
"modalBarrierDismissLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Schließen"),
|
||||
"newTabLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Neuen Tab hinzufügen"),
|
||||
"noResultsFoundLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Keine Ergebnisse gefunden"),
|
||||
"openNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Navigation öffnen"),
|
||||
"pasteActionLabel": MessageLookupByLibrary.simpleMessage("Einfügen"),
|
||||
"pasteActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Fügt den Inhalt der Zwischenablage an der aktuellen Stelle ein"),
|
||||
"restoreWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Wiederherstellen"),
|
||||
"scrollTabBackwardLabel": MessageLookupByLibrary.simpleMessage(
|
||||
"Tab-Liste rückwärts scrollen"),
|
||||
"scrollTabForwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Tabliste vorwärts scrollen"),
|
||||
"searchLabel": MessageLookupByLibrary.simpleMessage("Suchen"),
|
||||
"selectAllActionLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Alles auswählen"),
|
||||
"selectAllActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Alle Inhalte auswählen")
|
||||
};
|
||||
}
|
||||
64
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_en.dart
vendored
Normal file
64
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_en.dart
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a en locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'en';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"backButtonTooltip": MessageLookupByLibrary.simpleMessage("Back"),
|
||||
"clickToSearch":
|
||||
MessageLookupByLibrary.simpleMessage("Click to search"),
|
||||
"closeButtonLabel": MessageLookupByLibrary.simpleMessage("Close"),
|
||||
"closeNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Close Navigation"),
|
||||
"closeTabLabelSuffix":
|
||||
MessageLookupByLibrary.simpleMessage("Close tab"),
|
||||
"closeWindowTooltip": MessageLookupByLibrary.simpleMessage("Close"),
|
||||
"copyActionLabel": MessageLookupByLibrary.simpleMessage("Copy"),
|
||||
"copyActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Copy the selected content to the clipboard"),
|
||||
"cutActionLabel": MessageLookupByLibrary.simpleMessage("Cut"),
|
||||
"cutActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Remove the selected content and put it in the clipboard"),
|
||||
"dialogLabel": MessageLookupByLibrary.simpleMessage("Dialog"),
|
||||
"minimizeWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Minimize"),
|
||||
"modalBarrierDismissLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Dismiss"),
|
||||
"newTabLabel": MessageLookupByLibrary.simpleMessage("Add new tab"),
|
||||
"noResultsFoundLabel":
|
||||
MessageLookupByLibrary.simpleMessage("No results found"),
|
||||
"openNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Open Navigation"),
|
||||
"pasteActionLabel": MessageLookupByLibrary.simpleMessage("Paste"),
|
||||
"pasteActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Inserts the contents of the clipboard at the current location"),
|
||||
"restoreWindowTooltip": MessageLookupByLibrary.simpleMessage("Restore"),
|
||||
"scrollTabBackwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Scroll tab list backward"),
|
||||
"scrollTabForwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Scroll tab list forward"),
|
||||
"searchLabel": MessageLookupByLibrary.simpleMessage("Search"),
|
||||
"selectAllActionLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Select all"),
|
||||
"selectAllActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Select all content")
|
||||
};
|
||||
}
|
||||
66
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_es.dart
vendored
Normal file
66
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_es.dart
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a es locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'es';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"backButtonTooltip": MessageLookupByLibrary.simpleMessage("Volver"),
|
||||
"clickToSearch":
|
||||
MessageLookupByLibrary.simpleMessage("Haz clic para buscar"),
|
||||
"closeButtonLabel": MessageLookupByLibrary.simpleMessage("Cerrar"),
|
||||
"closeNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Cerrar Navegador"),
|
||||
"closeTabLabelSuffix":
|
||||
MessageLookupByLibrary.simpleMessage("Cerrar pestaña"),
|
||||
"closeWindowTooltip": MessageLookupByLibrary.simpleMessage("Cerrar"),
|
||||
"copyActionLabel": MessageLookupByLibrary.simpleMessage("Copiar"),
|
||||
"copyActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Copiar el contenido seleccionado al portapapeles"),
|
||||
"cutActionLabel": MessageLookupByLibrary.simpleMessage("Cortar"),
|
||||
"cutActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Cortar el contenido seleccionado y ponerlo en el portapapeles"),
|
||||
"dialogLabel": MessageLookupByLibrary.simpleMessage("Diálogo"),
|
||||
"minimizeWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Minimizar"),
|
||||
"modalBarrierDismissLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Cancelar"),
|
||||
"newTabLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Añadir nueva pestaña"),
|
||||
"noResultsFoundLabel": MessageLookupByLibrary.simpleMessage(
|
||||
"No se encontraron resultados"),
|
||||
"openNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Abrir Navegador"),
|
||||
"pasteActionLabel": MessageLookupByLibrary.simpleMessage("Pegar"),
|
||||
"pasteActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Insertar el contenido del portapapeles en la posición actual"),
|
||||
"restoreWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Restaurar"),
|
||||
"scrollTabBackwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Hacer scroll hacia atrás"),
|
||||
"scrollTabForwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Hacer scroll hacia delante"),
|
||||
"searchLabel": MessageLookupByLibrary.simpleMessage("Buscar"),
|
||||
"selectAllActionLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Seleccionar todo"),
|
||||
"selectAllActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Seleccionar todo el contenido")
|
||||
};
|
||||
}
|
||||
66
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_fr.dart
vendored
Normal file
66
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_fr.dart
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a fr locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'fr';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"backButtonTooltip": MessageLookupByLibrary.simpleMessage("Retour"),
|
||||
"clickToSearch":
|
||||
MessageLookupByLibrary.simpleMessage("Cliquez pour rechercher"),
|
||||
"closeButtonLabel": MessageLookupByLibrary.simpleMessage("Fermer"),
|
||||
"closeNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Fermer le navigateur"),
|
||||
"closeTabLabelSuffix":
|
||||
MessageLookupByLibrary.simpleMessage("Fermer l\'onglet"),
|
||||
"closeWindowTooltip": MessageLookupByLibrary.simpleMessage("Fermer"),
|
||||
"copyActionLabel": MessageLookupByLibrary.simpleMessage("Copier"),
|
||||
"copyActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Copier le contenu sélectionné dans le presse-papier"),
|
||||
"cutActionLabel": MessageLookupByLibrary.simpleMessage("Couper"),
|
||||
"cutActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Couper le contenu sélectionné et le mettre dans le presse-papier"),
|
||||
"dialogLabel": MessageLookupByLibrary.simpleMessage("Dialogue"),
|
||||
"minimizeWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Réduire"),
|
||||
"modalBarrierDismissLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Annuler"),
|
||||
"newTabLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Ajouter un nouvel onglet"),
|
||||
"noResultsFoundLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Aucun résultat trouvé"),
|
||||
"openNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Ouvrir le navigateur"),
|
||||
"pasteActionLabel": MessageLookupByLibrary.simpleMessage("Coller"),
|
||||
"pasteActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Coller le contenu du presse-papier à la position actuelle"),
|
||||
"restoreWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Restaurer"),
|
||||
"scrollTabBackwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Défiler vers l\'arrière"),
|
||||
"scrollTabForwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Défiler vers l\'avant"),
|
||||
"searchLabel": MessageLookupByLibrary.simpleMessage("Rechercher"),
|
||||
"selectAllActionLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Tout sélectionner"),
|
||||
"selectAllActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Sélectionner tout le contenu")
|
||||
};
|
||||
}
|
||||
65
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_hi.dart
vendored
Normal file
65
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_hi.dart
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a hi locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'hi';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"backButtonTooltip": MessageLookupByLibrary.simpleMessage("वापस"),
|
||||
"clickToSearch":
|
||||
MessageLookupByLibrary.simpleMessage("खोजने के लिए क्लिक करें"),
|
||||
"closeButtonLabel": MessageLookupByLibrary.simpleMessage("बंद करें"),
|
||||
"closeNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("नेविगेशन बंद करें"),
|
||||
"closeTabLabelSuffix":
|
||||
MessageLookupByLibrary.simpleMessage("टैब बंद करें"),
|
||||
"closeWindowTooltip": MessageLookupByLibrary.simpleMessage("बंद करें"),
|
||||
"copyActionLabel": MessageLookupByLibrary.simpleMessage("कॉपी"),
|
||||
"copyActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"सेलेक्टेड कंटेंट क्लिपबोर्ड पर कॉपी करें"),
|
||||
"cutActionLabel": MessageLookupByLibrary.simpleMessage("कट"),
|
||||
"cutActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"सिलेक्टेड कंटेंट यहाँ से हटा कर क्लिपबोर्ड पर कॉपी करें"),
|
||||
"dialogLabel": MessageLookupByLibrary.simpleMessage("डायलॉग"),
|
||||
"minimizeWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("मिनीमाइज करें"),
|
||||
"modalBarrierDismissLabel":
|
||||
MessageLookupByLibrary.simpleMessage("हटाएँ"),
|
||||
"newTabLabel": MessageLookupByLibrary.simpleMessage("नया टैब ऐड करें"),
|
||||
"noResultsFoundLabel":
|
||||
MessageLookupByLibrary.simpleMessage("कोई रिजल्ट नहीं मिला"),
|
||||
"openNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("नेविगेशन खोलें"),
|
||||
"pasteActionLabel": MessageLookupByLibrary.simpleMessage("पेस्ट"),
|
||||
"pasteActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"क्लिपबोर्ड का कंटेंट इस लोकेशन पर पेस्ट करें"),
|
||||
"restoreWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("वापिस लाएं"),
|
||||
"scrollTabBackwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("टैब लिस्ट पीछे स्क्रॉल करें"),
|
||||
"scrollTabForwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("टैब लिस्ट आगे स्क्रॉल करें"),
|
||||
"searchLabel": MessageLookupByLibrary.simpleMessage("खोजें"),
|
||||
"selectAllActionLabel":
|
||||
MessageLookupByLibrary.simpleMessage("सब-कुछ सेलेक्ट करें"),
|
||||
"selectAllActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("सारा कंटेंट सेलेक्ट करें")
|
||||
};
|
||||
}
|
||||
66
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_pt.dart
vendored
Normal file
66
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_pt.dart
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a pt locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'pt';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"backButtonTooltip": MessageLookupByLibrary.simpleMessage("Voltar"),
|
||||
"clickToSearch":
|
||||
MessageLookupByLibrary.simpleMessage("Clique para pesquisar"),
|
||||
"closeButtonLabel": MessageLookupByLibrary.simpleMessage("Fechar"),
|
||||
"closeNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Fechar navegação"),
|
||||
"closeTabLabelSuffix":
|
||||
MessageLookupByLibrary.simpleMessage("Fechar guia"),
|
||||
"closeWindowTooltip": MessageLookupByLibrary.simpleMessage("Fechar"),
|
||||
"copyActionLabel": MessageLookupByLibrary.simpleMessage("Copiar"),
|
||||
"copyActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Copiar conteúdo selecionado para a área de transferência"),
|
||||
"cutActionLabel": MessageLookupByLibrary.simpleMessage("Cortar"),
|
||||
"cutActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Recortar o conteúdo selecionado e colocá-lo na área de transferência"),
|
||||
"dialogLabel": MessageLookupByLibrary.simpleMessage("Caixa de diálogo"),
|
||||
"minimizeWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Minimizar"),
|
||||
"modalBarrierDismissLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Cancelar"),
|
||||
"newTabLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Adicionar nova guia"),
|
||||
"noResultsFoundLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Nenhum resultado encontrado"),
|
||||
"openNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Abrir navegação"),
|
||||
"pasteActionLabel": MessageLookupByLibrary.simpleMessage("Colar"),
|
||||
"pasteActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Colar o conteúdo da área de transferência na posição atual"),
|
||||
"restoreWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Restaurar"),
|
||||
"scrollTabBackwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Rolar para trás"),
|
||||
"scrollTabForwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Rolar para frente"),
|
||||
"searchLabel": MessageLookupByLibrary.simpleMessage("Pesquisar"),
|
||||
"selectAllActionLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Selecionar tudo"),
|
||||
"selectAllActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Selecionar tudo")
|
||||
};
|
||||
}
|
||||
65
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_ru.dart
vendored
Normal file
65
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_ru.dart
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a ru locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'ru';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"backButtonTooltip": MessageLookupByLibrary.simpleMessage("Назад"),
|
||||
"clickToSearch":
|
||||
MessageLookupByLibrary.simpleMessage("Нажмите для поиска"),
|
||||
"closeButtonLabel": MessageLookupByLibrary.simpleMessage("Закрыть"),
|
||||
"closeNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Закрыть панель навигации"),
|
||||
"closeTabLabelSuffix":
|
||||
MessageLookupByLibrary.simpleMessage("Закрыть вкладку"),
|
||||
"closeWindowTooltip": MessageLookupByLibrary.simpleMessage("Закрыть"),
|
||||
"copyActionLabel": MessageLookupByLibrary.simpleMessage("Копировать"),
|
||||
"copyActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Скопировать в буфер обмена"),
|
||||
"cutActionLabel": MessageLookupByLibrary.simpleMessage("Вырезать"),
|
||||
"cutActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Вырезать и поместить в буфер обмена"),
|
||||
"dialogLabel": MessageLookupByLibrary.simpleMessage("Диалог"),
|
||||
"minimizeWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Свернуть"),
|
||||
"modalBarrierDismissLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Отмена"),
|
||||
"newTabLabel": MessageLookupByLibrary.simpleMessage("Новая вкладка"),
|
||||
"noResultsFoundLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Результаты не найдены"),
|
||||
"openNavigationTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Открыть панель навигации"),
|
||||
"pasteActionLabel": MessageLookupByLibrary.simpleMessage("Вставить"),
|
||||
"pasteActionTooltip": MessageLookupByLibrary.simpleMessage(
|
||||
"Вставить содержимое буфера обмена"),
|
||||
"restoreWindowTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Восстановить"),
|
||||
"scrollTabBackwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Прокрутить назад"),
|
||||
"scrollTabForwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Прокрутить вперед"),
|
||||
"searchLabel": MessageLookupByLibrary.simpleMessage("Поиск"),
|
||||
"selectAllActionLabel":
|
||||
MessageLookupByLibrary.simpleMessage("Выбрать все"),
|
||||
"selectAllActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("Выбрать все")
|
||||
};
|
||||
}
|
||||
55
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_zh.dart
vendored
Normal file
55
dependencies/fluent_ui-3.12.0/lib/generated/intl/messages_zh.dart
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
|
||||
// This is a library that provides messages for a zh locale. All the
|
||||
// messages from the main program should be duplicated here with the same
|
||||
// function name.
|
||||
|
||||
// Ignore issues from commonly used lints in this file.
|
||||
// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new
|
||||
// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering
|
||||
// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases
|
||||
// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes
|
||||
// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/message_lookup_by_library.dart';
|
||||
|
||||
final messages = new MessageLookup();
|
||||
|
||||
typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
||||
|
||||
class MessageLookup extends MessageLookupByLibrary {
|
||||
String get localeName => 'zh';
|
||||
|
||||
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"backButtonTooltip": MessageLookupByLibrary.simpleMessage("返回"),
|
||||
"clickToSearch": MessageLookupByLibrary.simpleMessage("点击搜索"),
|
||||
"closeButtonLabel": MessageLookupByLibrary.simpleMessage("关闭"),
|
||||
"closeNavigationTooltip": MessageLookupByLibrary.simpleMessage("关闭导航"),
|
||||
"closeTabLabelSuffix": MessageLookupByLibrary.simpleMessage("关闭标签"),
|
||||
"closeWindowTooltip": MessageLookupByLibrary.simpleMessage("关闭"),
|
||||
"copyActionLabel": MessageLookupByLibrary.simpleMessage("复制"),
|
||||
"copyActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("将选中的内容复制到剪贴板"),
|
||||
"cutActionLabel": MessageLookupByLibrary.simpleMessage("剪切"),
|
||||
"cutActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("将选中的内容剪切到剪贴板"),
|
||||
"dialogLabel": MessageLookupByLibrary.simpleMessage("对话"),
|
||||
"minimizeWindowTooltip": MessageLookupByLibrary.simpleMessage("最小化"),
|
||||
"modalBarrierDismissLabel": MessageLookupByLibrary.simpleMessage("取消"),
|
||||
"newTabLabel": MessageLookupByLibrary.simpleMessage("添加新标签"),
|
||||
"noResultsFoundLabel": MessageLookupByLibrary.simpleMessage("没有找到结果"),
|
||||
"openNavigationTooltip": MessageLookupByLibrary.simpleMessage("打开导航"),
|
||||
"pasteActionLabel": MessageLookupByLibrary.simpleMessage("粘贴"),
|
||||
"pasteActionTooltip":
|
||||
MessageLookupByLibrary.simpleMessage("在当前位置插入剪贴板的内容"),
|
||||
"restoreWindowTooltip": MessageLookupByLibrary.simpleMessage("恢复"),
|
||||
"scrollTabBackwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("向后滚动标签列表"),
|
||||
"scrollTabForwardLabel":
|
||||
MessageLookupByLibrary.simpleMessage("向前滚动标签列表"),
|
||||
"searchLabel": MessageLookupByLibrary.simpleMessage("搜索"),
|
||||
"selectAllActionLabel": MessageLookupByLibrary.simpleMessage("全选"),
|
||||
"selectAllActionTooltip": MessageLookupByLibrary.simpleMessage("选择所有内容")
|
||||
};
|
||||
}
|
||||
326
dependencies/fluent_ui-3.12.0/lib/generated/l10n.dart
vendored
Normal file
326
dependencies/fluent_ui-3.12.0/lib/generated/l10n.dart
vendored
Normal file
@@ -0,0 +1,326 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'intl/messages_all.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// Generator: Flutter Intl IDE plugin
|
||||
// Made by Localizely
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
|
||||
// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each
|
||||
// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes
|
||||
|
||||
class S {
|
||||
S();
|
||||
|
||||
static S? _current;
|
||||
|
||||
static S get current {
|
||||
assert(_current != null,
|
||||
'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.');
|
||||
return _current!;
|
||||
}
|
||||
|
||||
static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
|
||||
|
||||
static Future<S> load(Locale locale) {
|
||||
final name = (locale.countryCode?.isEmpty ?? false)
|
||||
? locale.languageCode
|
||||
: locale.toString();
|
||||
final localeName = Intl.canonicalizedLocale(name);
|
||||
return initializeMessages(localeName).then((_) {
|
||||
Intl.defaultLocale = localeName;
|
||||
final instance = S();
|
||||
S._current = instance;
|
||||
|
||||
return instance;
|
||||
});
|
||||
}
|
||||
|
||||
static S of(BuildContext context) {
|
||||
final instance = S.maybeOf(context);
|
||||
assert(instance != null,
|
||||
'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?');
|
||||
return instance!;
|
||||
}
|
||||
|
||||
static S? maybeOf(BuildContext context) {
|
||||
return Localizations.of<S>(context, S);
|
||||
}
|
||||
|
||||
/// `Back`
|
||||
String get backButtonTooltip {
|
||||
return Intl.message(
|
||||
'Back',
|
||||
name: 'backButtonTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Close`
|
||||
String get closeButtonLabel {
|
||||
return Intl.message(
|
||||
'Close',
|
||||
name: 'closeButtonLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Search`
|
||||
String get searchLabel {
|
||||
return Intl.message(
|
||||
'Search',
|
||||
name: 'searchLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Close Navigation`
|
||||
String get closeNavigationTooltip {
|
||||
return Intl.message(
|
||||
'Close Navigation',
|
||||
name: 'closeNavigationTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Open Navigation`
|
||||
String get openNavigationTooltip {
|
||||
return Intl.message(
|
||||
'Open Navigation',
|
||||
name: 'openNavigationTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Click to search`
|
||||
String get clickToSearch {
|
||||
return Intl.message(
|
||||
'Click to search',
|
||||
name: 'clickToSearch',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Dismiss`
|
||||
String get modalBarrierDismissLabel {
|
||||
return Intl.message(
|
||||
'Dismiss',
|
||||
name: 'modalBarrierDismissLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Minimize`
|
||||
String get minimizeWindowTooltip {
|
||||
return Intl.message(
|
||||
'Minimize',
|
||||
name: 'minimizeWindowTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Restore`
|
||||
String get restoreWindowTooltip {
|
||||
return Intl.message(
|
||||
'Restore',
|
||||
name: 'restoreWindowTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Close`
|
||||
String get closeWindowTooltip {
|
||||
return Intl.message(
|
||||
'Close',
|
||||
name: 'closeWindowTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Dialog`
|
||||
String get dialogLabel {
|
||||
return Intl.message(
|
||||
'Dialog',
|
||||
name: 'dialogLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Cut`
|
||||
String get cutActionLabel {
|
||||
return Intl.message(
|
||||
'Cut',
|
||||
name: 'cutActionLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Copy`
|
||||
String get copyActionLabel {
|
||||
return Intl.message(
|
||||
'Copy',
|
||||
name: 'copyActionLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Paste`
|
||||
String get pasteActionLabel {
|
||||
return Intl.message(
|
||||
'Paste',
|
||||
name: 'pasteActionLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Select all`
|
||||
String get selectAllActionLabel {
|
||||
return Intl.message(
|
||||
'Select all',
|
||||
name: 'selectAllActionLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Add new tab`
|
||||
String get newTabLabel {
|
||||
return Intl.message(
|
||||
'Add new tab',
|
||||
name: 'newTabLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Close tab`
|
||||
String get closeTabLabelSuffix {
|
||||
return Intl.message(
|
||||
'Close tab',
|
||||
name: 'closeTabLabelSuffix',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Scroll tab list backward`
|
||||
String get scrollTabBackwardLabel {
|
||||
return Intl.message(
|
||||
'Scroll tab list backward',
|
||||
name: 'scrollTabBackwardLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Scroll tab list forward`
|
||||
String get scrollTabForwardLabel {
|
||||
return Intl.message(
|
||||
'Scroll tab list forward',
|
||||
name: 'scrollTabForwardLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `No results found`
|
||||
String get noResultsFoundLabel {
|
||||
return Intl.message(
|
||||
'No results found',
|
||||
name: 'noResultsFoundLabel',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Copy the selected content to the clipboard`
|
||||
String get copyActionTooltip {
|
||||
return Intl.message(
|
||||
'Copy the selected content to the clipboard',
|
||||
name: 'copyActionTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Remove the selected content and put it in the clipboard`
|
||||
String get cutActionTooltip {
|
||||
return Intl.message(
|
||||
'Remove the selected content and put it in the clipboard',
|
||||
name: 'cutActionTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Inserts the contents of the clipboard at the current location`
|
||||
String get pasteActionTooltip {
|
||||
return Intl.message(
|
||||
'Inserts the contents of the clipboard at the current location',
|
||||
name: 'pasteActionTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// `Select all content`
|
||||
String get selectAllActionTooltip {
|
||||
return Intl.message(
|
||||
'Select all content',
|
||||
name: 'selectAllActionTooltip',
|
||||
desc: '',
|
||||
args: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||
const AppLocalizationDelegate();
|
||||
|
||||
List<Locale> get supportedLocales {
|
||||
return const <Locale>[
|
||||
Locale.fromSubtags(languageCode: 'en'),
|
||||
Locale.fromSubtags(languageCode: 'ar'),
|
||||
Locale.fromSubtags(languageCode: 'de'),
|
||||
Locale.fromSubtags(languageCode: 'es'),
|
||||
Locale.fromSubtags(languageCode: 'fr'),
|
||||
Locale.fromSubtags(languageCode: 'hi'),
|
||||
Locale.fromSubtags(languageCode: 'pt'),
|
||||
Locale.fromSubtags(languageCode: 'ru'),
|
||||
Locale.fromSubtags(languageCode: 'zh'),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) => _isSupported(locale);
|
||||
@override
|
||||
Future<S> load(Locale locale) => S.load(locale);
|
||||
@override
|
||||
bool shouldReload(AppLocalizationDelegate old) => false;
|
||||
|
||||
bool _isSupported(Locale locale) {
|
||||
for (var supportedLocale in supportedLocales) {
|
||||
if (supportedLocale.languageCode == locale.languageCode) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
1
dependencies/fluent_ui-3.12.0/lib/l10n/README.md
vendored
Normal file
1
dependencies/fluent_ui-3.12.0/lib/l10n/README.md
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Every `.arb` localization must have a corresponding entry in `localization.dart:defaultSupportedLocales` list.
|
||||
28
dependencies/fluent_ui-3.12.0/lib/l10n/intl_ar.arb
vendored
Normal file
28
dependencies/fluent_ui-3.12.0/lib/l10n/intl_ar.arb
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"@@locale": "ar",
|
||||
"backButtonTooltip": "رجوع",
|
||||
"closeButtonLabel": "إغلاق",
|
||||
"searchLabel": "بحث",
|
||||
"closeNavigationTooltip": "إغلاق الواجهة",
|
||||
"openNavigationTooltip": "فتح الواجهة",
|
||||
"clickToSearch": "انقر للبحث",
|
||||
"modalBarrierDismissLabel": "استبعاد",
|
||||
"minimizeWindowTooltip": "تصغير",
|
||||
"restoreWindowTooltip": "إسترجاع",
|
||||
"closeWindowTooltip": "إغلاق",
|
||||
"dialogLabel": "مربع حوار",
|
||||
"cutActionLabel": "قص",
|
||||
"copyActionLabel": "نسخ",
|
||||
"pasteActionLabel": "لصق",
|
||||
"selectAllActionLabel": "تحديد الكل",
|
||||
"newTabLabel": "إضافة علامة تبويب جديدة",
|
||||
"closeTabLabelSuffix": "إغلاق علامة التبويب",
|
||||
"scrollTabBackwardLabel": "تمرير قائمة علامة التبويب للخلف",
|
||||
"scrollTabForwardLabel": "تمرير قائمة علامة التبويب إلى الأمام",
|
||||
"noResultsFoundLabel": "لم يتم العثور على نتائج",
|
||||
"copyActionTooltip": "انسخ المحتوى المحدد إلى الحافظة",
|
||||
"cutActionTooltip": "قم بإزالة المحتوى المحدد وضعه في الحافظة",
|
||||
"pasteActionTooltip": "إدراج محتويات الحافظة إلى الموقع الحالي",
|
||||
"selectAllActionTooltip": "تحديد المحتوى بالكامل"
|
||||
}
|
||||
|
||||
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_de.arb
vendored
Normal file
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_de.arb
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@@locale": "de",
|
||||
"backButtonTooltip": "Zurück",
|
||||
"closeButtonLabel": "Schließen",
|
||||
"searchLabel": "Suchen",
|
||||
"closeNavigationTooltip": "Navigation schließen",
|
||||
"openNavigationTooltip": "Navigation öffnen",
|
||||
"clickToSearch": "Zum Suchen klicken",
|
||||
"modalBarrierDismissLabel": "Schließen",
|
||||
"minimizeWindowTooltip": "Minimieren",
|
||||
"restoreWindowTooltip": "Wiederherstellen",
|
||||
"closeWindowTooltip": "Schließen",
|
||||
"dialogLabel": "Dialog",
|
||||
"cutActionLabel": "Ausschneiden",
|
||||
"copyActionLabel": "Kopieren",
|
||||
"pasteActionLabel": "Einfügen",
|
||||
"selectAllActionLabel": "Alles auswählen",
|
||||
"newTabLabel": "Neuen Tab hinzufügen",
|
||||
"closeTabLabelSuffix": "Tab schließen",
|
||||
"scrollTabBackwardLabel": "Tab-Liste rückwärts scrollen",
|
||||
"scrollTabForwardLabel": "Tabliste vorwärts scrollen",
|
||||
"noResultsFoundLabel": "Keine Ergebnisse gefunden",
|
||||
"copyActionTooltip": "Ausgewählten Inhalt in die Zwischenablage kopieren",
|
||||
"cutActionTooltip": "Ausgewählten Inhalt entfernen und in die Zwischenablage legen",
|
||||
"pasteActionTooltip": "Fügt den Inhalt der Zwischenablage an der aktuellen Stelle ein",
|
||||
"selectAllActionTooltip": "Alle Inhalte auswählen"
|
||||
}
|
||||
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_en.arb
vendored
Normal file
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_en.arb
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"backButtonTooltip": "Back",
|
||||
"closeButtonLabel": "Close",
|
||||
"searchLabel": "Search",
|
||||
"closeNavigationTooltip": "Close Navigation",
|
||||
"openNavigationTooltip": "Open Navigation",
|
||||
"clickToSearch": "Click to search",
|
||||
"modalBarrierDismissLabel": "Dismiss",
|
||||
"minimizeWindowTooltip": "Minimize",
|
||||
"restoreWindowTooltip": "Restore",
|
||||
"closeWindowTooltip": "Close",
|
||||
"dialogLabel": "Dialog",
|
||||
"cutActionLabel": "Cut",
|
||||
"copyActionLabel": "Copy",
|
||||
"pasteActionLabel": "Paste",
|
||||
"selectAllActionLabel": "Select all",
|
||||
"newTabLabel": "Add new tab",
|
||||
"closeTabLabelSuffix": "Close tab",
|
||||
"scrollTabBackwardLabel": "Scroll tab list backward",
|
||||
"scrollTabForwardLabel": "Scroll tab list forward",
|
||||
"noResultsFoundLabel": "No results found",
|
||||
"copyActionTooltip": "Copy the selected content to the clipboard",
|
||||
"cutActionTooltip": "Remove the selected content and put it in the clipboard",
|
||||
"pasteActionTooltip": "Inserts the contents of the clipboard at the current location",
|
||||
"selectAllActionTooltip": "Select all content"
|
||||
}
|
||||
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_es.arb
vendored
Normal file
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_es.arb
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@@locale": "es",
|
||||
"backButtonTooltip": "Volver",
|
||||
"closeButtonLabel": "Cerrar",
|
||||
"searchLabel": "Buscar",
|
||||
"closeNavigationTooltip": "Cerrar Navegador",
|
||||
"openNavigationTooltip": "Abrir Navegador",
|
||||
"clickToSearch": "Haz clic para buscar",
|
||||
"modalBarrierDismissLabel": "Cancelar",
|
||||
"minimizeWindowTooltip": "Minimizar",
|
||||
"restoreWindowTooltip": "Restaurar",
|
||||
"closeWindowTooltip": "Cerrar",
|
||||
"dialogLabel": "Diálogo",
|
||||
"cutActionLabel": "Cortar",
|
||||
"copyActionLabel": "Copiar",
|
||||
"pasteActionLabel": "Pegar",
|
||||
"selectAllActionLabel": "Seleccionar todo",
|
||||
"newTabLabel": "Añadir nueva pestaña",
|
||||
"closeTabLabelSuffix": "Cerrar pestaña",
|
||||
"scrollTabBackwardLabel": "Hacer scroll hacia atrás",
|
||||
"scrollTabForwardLabel": "Hacer scroll hacia delante",
|
||||
"noResultsFoundLabel": "No se encontraron resultados",
|
||||
"copyActionTooltip": "Copiar el contenido seleccionado al portapapeles",
|
||||
"cutActionTooltip": "Cortar el contenido seleccionado y ponerlo en el portapapeles",
|
||||
"pasteActionTooltip": "Insertar el contenido del portapapeles en la posición actual",
|
||||
"selectAllActionTooltip": "Seleccionar todo el contenido"
|
||||
}
|
||||
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_fr.arb
vendored
Normal file
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_fr.arb
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@@locale": "fr",
|
||||
"backButtonTooltip": "Retour",
|
||||
"closeButtonLabel": "Fermer",
|
||||
"searchLabel": "Rechercher",
|
||||
"closeNavigationTooltip": "Fermer le navigateur",
|
||||
"openNavigationTooltip": "Ouvrir le navigateur",
|
||||
"clickToSearch": "Cliquez pour rechercher",
|
||||
"modalBarrierDismissLabel": "Annuler",
|
||||
"minimizeWindowTooltip": "Réduire",
|
||||
"restoreWindowTooltip": "Restaurer",
|
||||
"closeWindowTooltip": "Fermer",
|
||||
"dialogLabel": "Dialogue",
|
||||
"cutActionLabel": "Couper",
|
||||
"copyActionLabel": "Copier",
|
||||
"pasteActionLabel": "Coller",
|
||||
"selectAllActionLabel": "Tout sélectionner",
|
||||
"newTabLabel": "Ajouter un nouvel onglet",
|
||||
"closeTabLabelSuffix": "Fermer l'onglet",
|
||||
"scrollTabBackwardLabel": "Défiler vers l'arrière",
|
||||
"scrollTabForwardLabel": "Défiler vers l'avant",
|
||||
"noResultsFoundLabel": "Aucun résultat trouvé",
|
||||
"copyActionTooltip": "Copier le contenu sélectionné dans le presse-papier",
|
||||
"cutActionTooltip": "Couper le contenu sélectionné et le mettre dans le presse-papier",
|
||||
"pasteActionTooltip": "Coller le contenu du presse-papier à la position actuelle",
|
||||
"selectAllActionTooltip": "Sélectionner tout le contenu"
|
||||
}
|
||||
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_hi.arb
vendored
Normal file
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_hi.arb
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@@locale": "hi",
|
||||
"backButtonTooltip": "वापस",
|
||||
"closeButtonLabel": "बंद करें",
|
||||
"searchLabel": "खोजें",
|
||||
"closeNavigationTooltip": "नेविगेशन बंद करें",
|
||||
"openNavigationTooltip": "नेविगेशन खोलें",
|
||||
"clickToSearch": "खोजने के लिए क्लिक करें",
|
||||
"modalBarrierDismissLabel": "हटाएँ",
|
||||
"minimizeWindowTooltip": "मिनीमाइज करें",
|
||||
"restoreWindowTooltip": "वापिस लाएं",
|
||||
"closeWindowTooltip": "बंद करें",
|
||||
"dialogLabel": "डायलॉग",
|
||||
"cutActionLabel": "कट",
|
||||
"copyActionLabel": "कॉपी",
|
||||
"pasteActionLabel": "पेस्ट",
|
||||
"selectAllActionLabel": "सब-कुछ सेलेक्ट करें",
|
||||
"newTabLabel": "नया टैब ऐड करें",
|
||||
"closeTabLabelSuffix": "टैब बंद करें",
|
||||
"scrollTabBackwardLabel": "टैब लिस्ट पीछे स्क्रॉल करें",
|
||||
"scrollTabForwardLabel": "टैब लिस्ट आगे स्क्रॉल करें",
|
||||
"noResultsFoundLabel": "कोई रिजल्ट नहीं मिला",
|
||||
"copyActionTooltip": "सेलेक्टेड कंटेंट क्लिपबोर्ड पर कॉपी करें",
|
||||
"cutActionTooltip": "सिलेक्टेड कंटेंट यहाँ से हटा कर क्लिपबोर्ड पर कॉपी करें",
|
||||
"pasteActionTooltip": "क्लिपबोर्ड का कंटेंट इस लोकेशन पर पेस्ट करें",
|
||||
"selectAllActionTooltip": "सारा कंटेंट सेलेक्ट करें"
|
||||
}
|
||||
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_pt.arb
vendored
Normal file
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_pt.arb
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@@locale": "pt",
|
||||
"backButtonTooltip": "Voltar",
|
||||
"closeButtonLabel": "Fechar",
|
||||
"searchLabel": "Pesquisar",
|
||||
"closeNavigationTooltip": "Fechar navegação",
|
||||
"openNavigationTooltip": "Abrir navegação",
|
||||
"clickToSearch": "Clique para pesquisar",
|
||||
"modalBarrierDismissLabel": "Cancelar",
|
||||
"minimizeWindowTooltip": "Minimizar",
|
||||
"restoreWindowTooltip": "Restaurar",
|
||||
"closeWindowTooltip": "Fechar",
|
||||
"dialogLabel": "Caixa de diálogo",
|
||||
"cutActionLabel": "Cortar",
|
||||
"copyActionLabel": "Copiar",
|
||||
"pasteActionLabel": "Colar",
|
||||
"selectAllActionLabel": "Selecionar tudo",
|
||||
"newTabLabel": "Adicionar nova guia",
|
||||
"closeTabLabelSuffix": "Fechar guia",
|
||||
"scrollTabBackwardLabel": "Rolar para trás",
|
||||
"scrollTabForwardLabel": "Rolar para frente",
|
||||
"noResultsFoundLabel": "Nenhum resultado encontrado",
|
||||
"copyActionTooltip": "Copiar conteúdo selecionado para a área de transferência",
|
||||
"cutActionTooltip": "Recortar o conteúdo selecionado e colocá-lo na área de transferência",
|
||||
"pasteActionTooltip": "Colar o conteúdo da área de transferência na posição atual",
|
||||
"selectAllActionTooltip": "Selecionar tudo"
|
||||
}
|
||||
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_ru.arb
vendored
Normal file
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_ru.arb
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@@locale": "ru",
|
||||
"backButtonTooltip": "Назад",
|
||||
"closeButtonLabel": "Закрыть",
|
||||
"searchLabel": "Поиск",
|
||||
"closeNavigationTooltip": "Закрыть панель навигации",
|
||||
"openNavigationTooltip": "Открыть панель навигации",
|
||||
"clickToSearch": "Нажмите для поиска",
|
||||
"modalBarrierDismissLabel": "Отмена",
|
||||
"minimizeWindowTooltip": "Свернуть",
|
||||
"restoreWindowTooltip": "Восстановить",
|
||||
"closeWindowTooltip": "Закрыть",
|
||||
"dialogLabel": "Диалог",
|
||||
"cutActionLabel": "Вырезать",
|
||||
"copyActionLabel": "Копировать",
|
||||
"pasteActionLabel": "Вставить",
|
||||
"selectAllActionLabel": "Выбрать все",
|
||||
"newTabLabel": "Новая вкладка",
|
||||
"closeTabLabelSuffix": "Закрыть вкладку",
|
||||
"scrollTabBackwardLabel": "Прокрутить назад",
|
||||
"scrollTabForwardLabel": "Прокрутить вперед",
|
||||
"noResultsFoundLabel": "Результаты не найдены",
|
||||
"copyActionTooltip": "Скопировать в буфер обмена",
|
||||
"cutActionTooltip": "Вырезать и поместить в буфер обмена",
|
||||
"pasteActionTooltip": "Вставить содержимое буфера обмена",
|
||||
"selectAllActionTooltip": "Выбрать все"
|
||||
}
|
||||
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_zh.arb
vendored
Normal file
27
dependencies/fluent_ui-3.12.0/lib/l10n/intl_zh.arb
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@@locale": "zh",
|
||||
"backButtonTooltip": "返回",
|
||||
"closeButtonLabel": "关闭",
|
||||
"searchLabel": "搜索",
|
||||
"closeNavigationTooltip": "关闭导航",
|
||||
"openNavigationTooltip": "打开导航",
|
||||
"clickToSearch": "点击搜索",
|
||||
"modalBarrierDismissLabel": "取消",
|
||||
"minimizeWindowTooltip": "最小化",
|
||||
"restoreWindowTooltip": "恢复",
|
||||
"closeWindowTooltip": "关闭",
|
||||
"dialogLabel": "对话",
|
||||
"cutActionLabel": "剪切",
|
||||
"copyActionLabel": "复制",
|
||||
"pasteActionLabel": "粘贴",
|
||||
"selectAllActionLabel": "全选",
|
||||
"newTabLabel": "添加新标签",
|
||||
"closeTabLabelSuffix": "关闭标签",
|
||||
"scrollTabBackwardLabel": "向后滚动标签列表",
|
||||
"scrollTabForwardLabel": "向前滚动标签列表",
|
||||
"noResultsFoundLabel": "没有找到结果",
|
||||
"copyActionTooltip": "将选中的内容复制到剪贴板",
|
||||
"cutActionTooltip": "将选中的内容剪切到剪贴板",
|
||||
"pasteActionTooltip": "在当前位置插入剪贴板的内容",
|
||||
"selectAllActionTooltip": "选择所有内容"
|
||||
}
|
||||
555
dependencies/fluent_ui-3.12.0/lib/src/app.dart
vendored
Normal file
555
dependencies/fluent_ui-3.12.0/lib/src/app.dart
vendored
Normal file
@@ -0,0 +1,555 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/material.dart' as m;
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
|
||||
/// An application that uses fluent design.
|
||||
///
|
||||
/// A convenience widget that wraps a number of widgets that are commonly
|
||||
/// required for fluent design applications.
|
||||
///
|
||||
/// The [FluentApp] configures the top-level [Navigator] to search for routes
|
||||
/// in the following order:
|
||||
///
|
||||
/// 1. For the `/` route, the [home] property, if non-null, is used.
|
||||
///
|
||||
/// 2. Otherwise, the [routes] table is used, if it has an entry for the route.
|
||||
///
|
||||
/// 3. Otherwise, [onGenerateRoute] is called, if provided. It should return a
|
||||
/// non-null value for any _valid_ route not handled by [home] and [routes].
|
||||
///
|
||||
/// 4. Finally if all else fails [onUnknownRoute] is called.
|
||||
///
|
||||
/// If a [Navigator] is created, at least one of these options must handle the
|
||||
/// `/` route, since it is used when an invalid [initialRoute] is specified on
|
||||
/// startup (e.g. by another application launching this one with an intent on
|
||||
/// Android; see [dart:ui.PlatformDispatcher.defaultRouteName]).
|
||||
///
|
||||
/// This widget also configures the observer of the top-level [Navigator] (if
|
||||
/// any) to perform [Hero] animations.
|
||||
///
|
||||
/// If [home], [routes], [onGenerateRoute], and [onUnknownRoute] are all null,
|
||||
/// and [builder] is not null, then no [Navigator] is created.
|
||||
class FluentApp extends StatefulWidget {
|
||||
/// Creates a FluentApp.
|
||||
///
|
||||
/// At least one of [home], [routes], [onGenerateRoute], or [builder] must be
|
||||
/// non-null. If only [routes] is given, it must include an entry for the
|
||||
/// [Navigator.defaultRouteName] (`/`), since that is the route used when the
|
||||
/// application is launched with an intent that specifies an otherwise
|
||||
/// unsupported route.
|
||||
///
|
||||
/// This class creates an instance of [WidgetsApp].
|
||||
///
|
||||
/// The boolean arguments, [routes], and [navigatorObservers], must not be null.
|
||||
const FluentApp({
|
||||
Key? key,
|
||||
this.navigatorKey,
|
||||
this.onGenerateRoute,
|
||||
this.onGenerateInitialRoutes,
|
||||
this.onUnknownRoute,
|
||||
this.navigatorObservers = const <NavigatorObserver>[],
|
||||
this.initialRoute,
|
||||
this.home,
|
||||
this.routes = const <String, WidgetBuilder>{},
|
||||
this.builder,
|
||||
this.title = '',
|
||||
this.onGenerateTitle,
|
||||
this.color,
|
||||
this.locale,
|
||||
this.localizationsDelegates,
|
||||
this.localeListResolutionCallback,
|
||||
this.localeResolutionCallback,
|
||||
this.supportedLocales = defaultSupportedLocales,
|
||||
this.showPerformanceOverlay = false,
|
||||
this.checkerboardRasterCacheImages = false,
|
||||
this.checkerboardOffscreenLayers = false,
|
||||
this.showSemanticsDebugger = false,
|
||||
this.debugShowCheckedModeBanner = true,
|
||||
this.shortcuts,
|
||||
this.actions,
|
||||
this.theme,
|
||||
this.darkTheme,
|
||||
this.themeMode,
|
||||
this.restorationScopeId,
|
||||
this.scrollBehavior = const FluentScrollBehavior(),
|
||||
this.useInheritedMediaQuery = false,
|
||||
}) : routeInformationProvider = null,
|
||||
routeInformationParser = null,
|
||||
routerDelegate = null,
|
||||
backButtonDispatcher = null,
|
||||
super(key: key);
|
||||
|
||||
/// Creates a [FluentApp] that uses the [Router] instead of a [Navigator].
|
||||
FluentApp.router({
|
||||
Key? key,
|
||||
this.theme,
|
||||
this.darkTheme,
|
||||
this.themeMode,
|
||||
this.routeInformationProvider,
|
||||
required this.routeInformationParser,
|
||||
required this.routerDelegate,
|
||||
BackButtonDispatcher? backButtonDispatcher,
|
||||
this.builder,
|
||||
this.title = '',
|
||||
this.onGenerateTitle,
|
||||
required Color this.color,
|
||||
this.locale,
|
||||
this.localizationsDelegates,
|
||||
this.localeListResolutionCallback,
|
||||
this.localeResolutionCallback,
|
||||
this.supportedLocales = defaultSupportedLocales,
|
||||
this.showPerformanceOverlay = false,
|
||||
this.checkerboardRasterCacheImages = false,
|
||||
this.checkerboardOffscreenLayers = false,
|
||||
this.showSemanticsDebugger = false,
|
||||
this.debugShowCheckedModeBanner = true,
|
||||
this.shortcuts,
|
||||
this.actions,
|
||||
this.restorationScopeId,
|
||||
this.scrollBehavior = const FluentScrollBehavior(),
|
||||
this.useInheritedMediaQuery = false,
|
||||
}) : assert(routeInformationParser != null && routerDelegate != null,
|
||||
'The routeInformationParser and routerDelegate cannot be null.'),
|
||||
assert(supportedLocales.isNotEmpty),
|
||||
navigatorObservers = null,
|
||||
backButtonDispatcher =
|
||||
backButtonDispatcher ?? RootBackButtonDispatcher(),
|
||||
navigatorKey = null,
|
||||
onGenerateRoute = null,
|
||||
home = null,
|
||||
onGenerateInitialRoutes = null,
|
||||
onUnknownRoute = null,
|
||||
routes = null,
|
||||
initialRoute = null,
|
||||
super(key: key);
|
||||
|
||||
/// Default visual properties, like colors fonts and shapes, for this app's
|
||||
/// fluent widgets.
|
||||
///
|
||||
/// A second [darkTheme] [ThemeData] value, which is used to provide a dark
|
||||
/// version of the user interface can also be specified. [themeMode] will
|
||||
/// control which theme will be used if a [darkTheme] is provided.
|
||||
///
|
||||
/// The default value of this property is the value of `ThemeData(brightness: Brightness.light)`.
|
||||
final ThemeData? theme;
|
||||
|
||||
/// The [ThemeData] to use when a 'dark mode' is requested by the system.
|
||||
///
|
||||
/// Some host platforms allow the users to select a system-wide 'dark mode',
|
||||
/// or the application may want to offer the user the ability to choose a
|
||||
/// dark theme just for this application. This is theme that will be used for
|
||||
/// such cases. [themeMode] will control which theme will be used.
|
||||
///
|
||||
/// This theme should have a [ThemeData.brightness] set to [Brightness.dark].
|
||||
///
|
||||
/// Uses [theme] instead when null. Defaults to the value of
|
||||
/// [ThemeData(brightness: Brightness.light)] when both [darkTheme] and [theme] are null.
|
||||
final ThemeData? darkTheme;
|
||||
|
||||
/// Determines which theme will be used by the application if both [theme]
|
||||
/// and [darkTheme] are provided.
|
||||
///
|
||||
/// If set to [ThemeMode.system], the choice of which theme to use will
|
||||
/// be based on the user's system preferences. If the [MediaQuery.platformBrightnessOf]
|
||||
/// is [Brightness.light], [theme] will be used. If it is [Brightness.dark],
|
||||
/// [darkTheme] will be used (unless it is null, in which case [theme]
|
||||
/// will be used.
|
||||
///
|
||||
/// If set to [ThemeMode.light] the [theme] will always be used,
|
||||
/// regardless of the user's system preference.
|
||||
///
|
||||
/// If set to [ThemeMode.dark] the [darkTheme] will be used
|
||||
/// regardless of the user's system preference. If [darkTheme] is null
|
||||
/// then it will fallback to using [theme].
|
||||
///
|
||||
/// The default value is [ThemeMode.system].
|
||||
final ThemeMode? themeMode;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.navigatorKey}
|
||||
final GlobalKey<NavigatorState>? navigatorKey;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.home}
|
||||
final Widget? home;
|
||||
|
||||
/// The application's top-level routing table.
|
||||
///
|
||||
/// When a named route is pushed with [Navigator.pushNamed], the route name is
|
||||
/// looked up in this map. If the name is present, the associated
|
||||
/// [WidgetBuilder] is used to construct a [FluentPageRoute] that performs
|
||||
/// an appropriate transition, including [Hero] animations, to the new route.
|
||||
///
|
||||
/// {@macro flutter.widgets.widgetsApp.routes}
|
||||
final Map<String, WidgetBuilder>? routes;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.initialRoute}
|
||||
final String? initialRoute;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.onGenerateRoute}
|
||||
final RouteFactory? onGenerateRoute;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.onGenerateInitialRoutes}
|
||||
final InitialRouteListFactory? onGenerateInitialRoutes;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
|
||||
final RouteFactory? onUnknownRoute;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.navigatorObservers}
|
||||
final List<NavigatorObserver>? navigatorObservers;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.routeInformationProvider}
|
||||
final RouteInformationProvider? routeInformationProvider;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.routeInformationParser}
|
||||
final RouteInformationParser<Object>? routeInformationParser;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.routerDelegate}
|
||||
final RouterDelegate<Object>? routerDelegate;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.backButtonDispatcher}
|
||||
final BackButtonDispatcher? backButtonDispatcher;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.builder}
|
||||
///
|
||||
/// Fluent specific features such as [showDialog] and [showMenu], and widgets
|
||||
/// such as [Tooltip], [PopupMenuButton], also require a [Navigator] to properly
|
||||
/// function.
|
||||
final TransitionBuilder? builder;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.title}
|
||||
///
|
||||
/// This value is passed unmodified to [WidgetsApp.title].
|
||||
final String title;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.onGenerateTitle}
|
||||
///
|
||||
/// This value is passed unmodified to [WidgetsApp.onGenerateTitle].
|
||||
final GenerateAppTitle? onGenerateTitle;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.color}
|
||||
final Color? color;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.locale}
|
||||
final Locale? locale;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.localizationsDelegates}
|
||||
final Iterable<LocalizationsDelegate<dynamic>>? localizationsDelegates;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}
|
||||
///
|
||||
/// This callback is passed along to the [WidgetsApp] built by this widget.
|
||||
final LocaleListResolutionCallback? localeListResolutionCallback;
|
||||
|
||||
/// {@macro flutter.widgets.LocaleResolutionCallback}
|
||||
///
|
||||
/// This callback is passed along to the [WidgetsApp] built by this widget.
|
||||
final LocaleResolutionCallback? localeResolutionCallback;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.supportedLocales}
|
||||
///
|
||||
/// It is passed along unmodified to the [WidgetsApp] built by this widget.
|
||||
final Iterable<Locale> supportedLocales;
|
||||
|
||||
/// Turns on a performance overlay.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://flutter.dev/debugging/#performanceoverlay>
|
||||
final bool showPerformanceOverlay;
|
||||
|
||||
/// Turns on checkerboarding of raster cache images.
|
||||
final bool checkerboardRasterCacheImages;
|
||||
|
||||
/// Turns on checkerboarding of layers rendered to offscreen bitmaps.
|
||||
final bool checkerboardOffscreenLayers;
|
||||
|
||||
/// Turns on an overlay that shows the accessibility information
|
||||
/// reported by the framework.
|
||||
final bool showSemanticsDebugger;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.debugShowCheckedModeBanner}
|
||||
final bool debugShowCheckedModeBanner;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.shortcuts}
|
||||
/// {@tool snippet}
|
||||
/// This example shows how to add a single shortcut for
|
||||
/// [LogicalKeyboardKey.select] to the default shortcuts without needing to
|
||||
/// add your own [Shortcuts] widget.
|
||||
///
|
||||
/// Alternatively, you could insert a [Shortcuts] widget with just the mapping
|
||||
/// you want to add between the [FluentApp] and its child and get the same
|
||||
/// effect.
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return FluentApp(
|
||||
/// shortcuts: <LogicalKeySet, Intent>{
|
||||
/// ...WidgetsApp.defaultShortcuts,
|
||||
/// LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
|
||||
/// },
|
||||
/// color: const Color(0xFFFF0000),
|
||||
/// builder: (BuildContext context, Widget? child) {
|
||||
/// return const Placeholder();
|
||||
/// },
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
/// {@macro flutter.widgets.widgetsApp.shortcuts.seeAlso}
|
||||
final Map<LogicalKeySet, Intent>? shortcuts;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.actions}
|
||||
/// {@tool snippet}
|
||||
/// This example shows how to add a single action handling an
|
||||
/// [ActivateAction] to the default actions without needing to
|
||||
/// add your own [Actions] widget.
|
||||
///
|
||||
/// Alternatively, you could insert a [Actions] widget with just the mapping
|
||||
/// you want to add between the [FluentApp] and its child and get the same
|
||||
/// effect.
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return FluentApp(
|
||||
/// actions: <Type, Action<Intent>>{
|
||||
/// ... WidgetsApp.defaultActions,
|
||||
/// ActivateAction: CallbackAction(
|
||||
/// onInvoke: (Intent intent) {
|
||||
/// // Do something here...
|
||||
/// return null;
|
||||
/// },
|
||||
/// ),
|
||||
/// },
|
||||
/// color: const Color(0xFFFF0000),
|
||||
/// builder: (BuildContext context, Widget? child) {
|
||||
/// return const Placeholder();
|
||||
/// },
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
|
||||
final Map<Type, Action<Intent>>? actions;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.restorationScopeId}
|
||||
final String? restorationScopeId;
|
||||
|
||||
/// {@macro flutter.material.materialApp.scrollBehavior}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ScrollConfiguration], which controls how [Scrollable] widgets behave
|
||||
/// in a subtree.
|
||||
final ScrollBehavior scrollBehavior;
|
||||
|
||||
static bool showPerformanceOverlayOverride = false;
|
||||
|
||||
static bool debugShowWidgetInspectorOverride = false;
|
||||
|
||||
static bool debugAllowBannerOverride = true;
|
||||
|
||||
/// {@macro flutter.widgets.widgetsApp.useInheritedMediaQuery}
|
||||
final bool useInheritedMediaQuery;
|
||||
|
||||
@override
|
||||
_FluentAppState createState() => _FluentAppState();
|
||||
}
|
||||
|
||||
class _FluentAppState extends State<FluentApp> {
|
||||
late HeroController _heroController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_heroController = HeroController();
|
||||
}
|
||||
|
||||
// Combine the Localizations for Material with the ones contributed
|
||||
// by the localizationsDelegates parameter, if any. Only the first delegate
|
||||
// of a particular LocalizationsDelegate.type is loaded so the
|
||||
// localizationsDelegate parameter can be used to override
|
||||
// _FluentLocalizationsDelegate.
|
||||
Iterable<LocalizationsDelegate<dynamic>> get _localizationsDelegates sync* {
|
||||
if (widget.localizationsDelegates != null) {
|
||||
yield* widget.localizationsDelegates!;
|
||||
}
|
||||
yield DefaultFluentLocalizations.delegate;
|
||||
yield GlobalMaterialLocalizations.delegate;
|
||||
yield GlobalWidgetsLocalizations.delegate;
|
||||
}
|
||||
|
||||
bool get _usesRouter => widget.routerDelegate != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final result = _buildApp(context);
|
||||
return ScrollConfiguration(
|
||||
behavior: widget.scrollBehavior,
|
||||
child: HeroControllerScope(
|
||||
controller: _heroController,
|
||||
child: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData theme(BuildContext context) {
|
||||
final mode = widget.themeMode ?? ThemeMode.system;
|
||||
final platformBrightness = MediaQuery.platformBrightnessOf(context);
|
||||
final usedarkStyle = mode == ThemeMode.dark ||
|
||||
(mode == ThemeMode.system && platformBrightness == Brightness.dark);
|
||||
|
||||
ThemeData data = () {
|
||||
late ThemeData result;
|
||||
if (usedarkStyle) {
|
||||
result = widget.darkTheme ?? widget.theme ?? ThemeData();
|
||||
} else {
|
||||
result = widget.theme ?? ThemeData();
|
||||
}
|
||||
return result;
|
||||
}();
|
||||
return data;
|
||||
}
|
||||
|
||||
Widget _builder(BuildContext context, Widget? child) {
|
||||
final themeData = theme(context);
|
||||
final mTheme = context.findAncestorWidgetOfExactType<m.Theme>();
|
||||
return m.AnimatedTheme(
|
||||
data: mTheme?.data ??
|
||||
m.ThemeData(
|
||||
brightness: themeData.brightness,
|
||||
canvasColor: themeData.cardColor,
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
selectionColor: themeData.accentColor
|
||||
.resolveFromBrightness(themeData.brightness)
|
||||
.withOpacity(0.8),
|
||||
cursorColor: themeData.inactiveColor,
|
||||
),
|
||||
),
|
||||
child: AnimatedFluentTheme(
|
||||
curve: themeData.animationCurve,
|
||||
data: themeData,
|
||||
child: widget.builder != null
|
||||
? Builder(
|
||||
builder: (BuildContext context) {
|
||||
// Why are we surrounding a builder with a builder?
|
||||
//
|
||||
// The widget.builder may contain code that invokes
|
||||
// Theme.of(), which should return the theme we selected
|
||||
// above in AnimatedTheme. However, if we invoke
|
||||
// widget.builder() directly as the child of AnimatedTheme
|
||||
// then there is no Context separating them, and the
|
||||
// widget.builder() will not find the theme. Therefore, we
|
||||
// surround widget.builder with yet another builder so that
|
||||
// a context separates them and Theme.of() correctly
|
||||
// resolves to the theme we passed to AnimatedTheme.
|
||||
return widget.builder!(context, child);
|
||||
},
|
||||
)
|
||||
: child ?? const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildApp(BuildContext context) {
|
||||
final fluentColor = widget.color ?? Colors.blue;
|
||||
if (_usesRouter) {
|
||||
return WidgetsApp.router(
|
||||
key: GlobalObjectKey(this),
|
||||
routeInformationProvider: widget.routeInformationProvider,
|
||||
routeInformationParser: widget.routeInformationParser!,
|
||||
routerDelegate: widget.routerDelegate!,
|
||||
backButtonDispatcher: widget.backButtonDispatcher,
|
||||
builder: _builder,
|
||||
title: widget.title,
|
||||
onGenerateTitle: widget.onGenerateTitle,
|
||||
color: fluentColor,
|
||||
locale: widget.locale,
|
||||
localeResolutionCallback: widget.localeResolutionCallback,
|
||||
localeListResolutionCallback: widget.localeListResolutionCallback,
|
||||
supportedLocales: widget.supportedLocales,
|
||||
showPerformanceOverlay: widget.showPerformanceOverlay,
|
||||
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
|
||||
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
|
||||
showSemanticsDebugger: widget.showSemanticsDebugger,
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
localizationsDelegates: _localizationsDelegates,
|
||||
useInheritedMediaQuery: widget.useInheritedMediaQuery,
|
||||
);
|
||||
}
|
||||
|
||||
return WidgetsApp(
|
||||
key: GlobalObjectKey(this),
|
||||
navigatorKey: widget.navigatorKey,
|
||||
navigatorObservers: widget.navigatorObservers!,
|
||||
home: widget.home,
|
||||
routes: widget.routes!,
|
||||
initialRoute: widget.initialRoute,
|
||||
onGenerateRoute: widget.onGenerateRoute,
|
||||
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
|
||||
onUnknownRoute: widget.onUnknownRoute,
|
||||
builder: _builder,
|
||||
title: widget.title,
|
||||
onGenerateTitle: widget.onGenerateTitle,
|
||||
color: fluentColor,
|
||||
locale: widget.locale,
|
||||
localeResolutionCallback: widget.localeResolutionCallback,
|
||||
localeListResolutionCallback: widget.localeListResolutionCallback,
|
||||
supportedLocales: widget.supportedLocales,
|
||||
showPerformanceOverlay: widget.showPerformanceOverlay,
|
||||
checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
|
||||
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
|
||||
showSemanticsDebugger: widget.showSemanticsDebugger,
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
localizationsDelegates: _localizationsDelegates,
|
||||
useInheritedMediaQuery: widget.useInheritedMediaQuery,
|
||||
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
|
||||
return FluentPageRoute<T>(settings: settings, builder: builder);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes how [Scrollable] widgets behave for [FluentApp]s.
|
||||
///
|
||||
/// {@macro flutter.widgets.scrollBehavior}
|
||||
///
|
||||
/// When using the desktop platform, if the [Scrollable] widget scrolls in the
|
||||
/// [Axis.vertical], a [Scrollbar] is applied.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ScrollBehavior], the default scrolling behavior extended by this class.
|
||||
class FluentScrollBehavior extends ScrollBehavior {
|
||||
/// Creates a FluentScrollBehavior that decorates [Scrollable]s with
|
||||
/// [Scrollbar]s based on the current platform and provided [ScrollableDetails].
|
||||
const FluentScrollBehavior();
|
||||
|
||||
@override
|
||||
Widget buildScrollbar(context, child, details) {
|
||||
// When modifying this function, consider modifying the implementation in
|
||||
// the base class as well.
|
||||
switch (axisDirectionToAxis(details.direction)) {
|
||||
case Axis.horizontal:
|
||||
return child;
|
||||
case Axis.vertical:
|
||||
switch (getPlatform(context)) {
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
return Scrollbar(
|
||||
controller: details.controller,
|
||||
child: child,
|
||||
);
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.iOS:
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
515
dependencies/fluent_ui-3.12.0/lib/src/controls/form/auto_suggest_box.dart
vendored
Normal file
515
dependencies/fluent_ui-3.12.0/lib/src/controls/form/auto_suggest_box.dart
vendored
Normal file
@@ -0,0 +1,515 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
enum TextChangedReason {
|
||||
/// Whether the text in an [AutoSuggestBox] was changed by user input
|
||||
userInput,
|
||||
|
||||
/// Whether the text in an [AutoSuggestBox] was changed because the user
|
||||
/// chose the suggestion
|
||||
suggestionChosen,
|
||||
}
|
||||
|
||||
// TODO: Navigate through items using keyboard (https://github.com/bdlukaa/fluent_ui/issues/19)
|
||||
|
||||
/// An AutoSuggestBox provides a list of suggestions for a user to select from
|
||||
/// as they type.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/auto-suggest-box>
|
||||
/// * [TextBox], which is used by this widget to enter user text input
|
||||
/// * [Overlay], which is used to show the suggestion popup
|
||||
class AutoSuggestBox extends StatefulWidget {
|
||||
/// Creates a fluent-styled auto suggest box.
|
||||
const AutoSuggestBox({
|
||||
Key? key,
|
||||
required this.items,
|
||||
this.controller,
|
||||
this.onChanged,
|
||||
this.onSelected,
|
||||
this.leadingIcon,
|
||||
this.trailingIcon,
|
||||
this.clearButtonEnabled = true,
|
||||
this.placeholder,
|
||||
this.placeholderStyle,
|
||||
this.style,
|
||||
this.decoration,
|
||||
this.foregroundDecoration,
|
||||
this.highlightColor,
|
||||
this.cursorColor,
|
||||
this.cursorHeight,
|
||||
this.cursorRadius,
|
||||
this.cursorWidth = 1.5,
|
||||
this.showCursor,
|
||||
this.keyboardAppearance,
|
||||
this.scrollPadding = const EdgeInsets.all(20.0),
|
||||
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The list of items to display to the user to pick
|
||||
final List<String> items;
|
||||
|
||||
/// The controller used to have control over what to show on
|
||||
/// the [TextBox].
|
||||
final TextEditingController? controller;
|
||||
|
||||
/// Called when the text is updated
|
||||
final void Function(String text, TextChangedReason reason)? onChanged;
|
||||
|
||||
/// Called when the user selected a value.
|
||||
final ValueChanged<String>? onSelected;
|
||||
|
||||
/// A widget displayed at the start of the text box
|
||||
///
|
||||
/// Usually an [IconButton] or [Icon]
|
||||
final Widget? leadingIcon;
|
||||
|
||||
/// A widget displayed at the end of the text box
|
||||
///
|
||||
/// Usually an [IconButton] or [Icon]
|
||||
final Widget? trailingIcon;
|
||||
|
||||
/// Whether the close button is enabled
|
||||
///
|
||||
/// Defauls to true
|
||||
final bool clearButtonEnabled;
|
||||
|
||||
/// The text shown when the text box is empty
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextBox.placeholder]
|
||||
final String? placeholder;
|
||||
|
||||
/// The style of [placeholder]
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextBox.placeholderStyle]
|
||||
final TextStyle? placeholderStyle;
|
||||
|
||||
/// The style to use for the text being edited.
|
||||
final TextStyle? style;
|
||||
|
||||
/// Controls the [BoxDecoration] of the box behind the text input.
|
||||
final BoxDecoration? decoration;
|
||||
|
||||
/// Controls the [BoxDecoration] of the box in front of the text input.
|
||||
///
|
||||
/// If [highlightColor] is provided, this must not be provided
|
||||
final BoxDecoration? foregroundDecoration;
|
||||
|
||||
/// The highlight color of the text box.
|
||||
///
|
||||
/// If [foregroundDecoration] is provided, this must not be provided.
|
||||
final Color? highlightColor;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorWidth}
|
||||
final double cursorWidth;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorHeight}
|
||||
final double? cursorHeight;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.cursorRadius}
|
||||
final Radius? cursorRadius;
|
||||
|
||||
/// The color of the cursor.
|
||||
///
|
||||
/// The cursor indicates the current location of text insertion point in
|
||||
/// the field.
|
||||
final Color? cursorColor;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.showCursor}
|
||||
final bool? showCursor;
|
||||
|
||||
/// Controls how tall the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxHeightStyle] for details on available styles.
|
||||
final ui.BoxHeightStyle selectionHeightStyle;
|
||||
|
||||
/// Controls how wide the selection highlight boxes are computed to be.
|
||||
///
|
||||
/// See [ui.BoxWidthStyle] for details on available styles.
|
||||
final ui.BoxWidthStyle selectionWidthStyle;
|
||||
|
||||
/// The appearance of the keyboard.
|
||||
///
|
||||
/// This setting is only honored on iOS devices.
|
||||
///
|
||||
/// If unset, defaults to the brightness of [ThemeData.primaryColorBrightness].
|
||||
final Brightness? keyboardAppearance;
|
||||
|
||||
/// {@macro flutter.widgets.editableText.scrollPadding}
|
||||
final EdgeInsets scrollPadding;
|
||||
|
||||
@override
|
||||
_AutoSuggestBoxState createState() => _AutoSuggestBoxState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IterableProperty<String>('items', items));
|
||||
properties.add(ObjectFlagProperty<ValueChanged<String>?>(
|
||||
'onSelected',
|
||||
onSelected,
|
||||
ifNull: 'disabled',
|
||||
));
|
||||
properties.add(FlagProperty(
|
||||
'clearButtonEnabled',
|
||||
value: clearButtonEnabled,
|
||||
defaultValue: true,
|
||||
ifFalse: 'clear button disabled',
|
||||
));
|
||||
}
|
||||
|
||||
static List defaultItemSorter<T>(String text, List items) {
|
||||
return items.where((element) {
|
||||
return element.toString().toLowerCase().contains(text.toLowerCase());
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class _AutoSuggestBoxState<T> extends State<AutoSuggestBox> {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
OverlayEntry? _entry;
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
final GlobalKey _textBoxKey = GlobalKey();
|
||||
|
||||
late TextEditingController controller;
|
||||
final FocusScopeNode overlayNode = FocusScopeNode();
|
||||
|
||||
final clearGlobalKey = GlobalKey();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = widget.controller ?? TextEditingController();
|
||||
controller.addListener(() {
|
||||
if (!mounted) return;
|
||||
if (controller.text.length < 2) setState(() {});
|
||||
});
|
||||
focusNode.addListener(_handleFocusChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.removeListener(_handleFocusChanged);
|
||||
if (widget.controller == null) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleFocusChanged() {
|
||||
final hasFocus = focusNode.hasFocus;
|
||||
if (!hasFocus) {
|
||||
_dismissOverlay();
|
||||
} else {
|
||||
_showOverlay();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _insertOverlay() {
|
||||
_entry = OverlayEntry(builder: (context) {
|
||||
final context = _textBoxKey.currentContext;
|
||||
if (context == null) return const SizedBox.shrink();
|
||||
final box = _textBoxKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final child = Positioned(
|
||||
width: box.size.width,
|
||||
child: CompositedTransformFollower(
|
||||
link: _layerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: Offset(0, box.size.height + 0.8),
|
||||
child: SizedBox(
|
||||
width: box.size.width,
|
||||
child: FluentTheme(
|
||||
data: FluentTheme.of(context),
|
||||
child: _AutoSuggestBoxOverlay(
|
||||
node: overlayNode,
|
||||
controller: controller,
|
||||
items: widget.items,
|
||||
onSelected: (String item) {
|
||||
widget.onSelected?.call(item);
|
||||
controller.text = item;
|
||||
controller.selection = TextSelection.collapsed(
|
||||
offset: item.length,
|
||||
);
|
||||
widget.onChanged?.call(item, TextChangedReason.userInput);
|
||||
|
||||
// After selected, the overlay is dismissed and the text box is
|
||||
// unfocused
|
||||
_dismissOverlay();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return child;
|
||||
});
|
||||
|
||||
if (_textBoxKey.currentContext != null) {
|
||||
Overlay.of(context)?.insert(_entry!);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
void _dismissOverlay() {
|
||||
_entry?.remove();
|
||||
_entry = null;
|
||||
}
|
||||
|
||||
void _showOverlay() {
|
||||
if (_entry == null && !(_entry?.mounted ?? false)) {
|
||||
_insertOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
|
||||
return CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: Actions(
|
||||
actions: {
|
||||
DirectionalFocusIntent: _DirectionalFocusAction(),
|
||||
},
|
||||
child: TextBox(
|
||||
key: _textBoxKey,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
placeholder: widget.placeholder,
|
||||
placeholderStyle: widget.placeholderStyle,
|
||||
clipBehavior:
|
||||
_entry != null ? Clip.none : Clip.antiAliasWithSaveLayer,
|
||||
prefix: widget.leadingIcon,
|
||||
clearGlobalKey: clearGlobalKey,
|
||||
suffix: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (widget.trailingIcon != null) widget.trailingIcon!,
|
||||
if (widget.clearButtonEnabled && controller.text.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 2.0),
|
||||
child: IconButton(
|
||||
key: clearGlobalKey,
|
||||
icon: const Icon(FluentIcons.chrome_close),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
]),
|
||||
suffixMode: OverlayVisibilityMode.always,
|
||||
onChanged: (text) {
|
||||
widget.onChanged?.call(text, TextChangedReason.userInput);
|
||||
_showOverlay();
|
||||
},
|
||||
style: widget.style,
|
||||
decoration: widget.decoration,
|
||||
foregroundDecoration: widget.foregroundDecoration,
|
||||
highlightColor: widget.highlightColor,
|
||||
cursorColor: widget.cursorColor,
|
||||
cursorHeight: widget.cursorHeight,
|
||||
cursorRadius: widget.cursorRadius,
|
||||
cursorWidth: widget.cursorWidth,
|
||||
showCursor: widget.showCursor,
|
||||
scrollPadding: widget.scrollPadding,
|
||||
selectionHeightStyle: widget.selectionHeightStyle,
|
||||
selectionWidthStyle: widget.selectionWidthStyle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DirectionalFocusAction extends DirectionalFocusAction {
|
||||
@override
|
||||
void invoke(covariant DirectionalFocusIntent intent) {
|
||||
// if (!intent.ignoreTextFields || !_isForTextField) {
|
||||
// primaryFocus!.focusInDirection(intent.direction);
|
||||
// }
|
||||
debugPrint(intent.direction.toString());
|
||||
}
|
||||
}
|
||||
|
||||
class _AutoSuggestBoxOverlay extends StatelessWidget {
|
||||
const _AutoSuggestBoxOverlay({
|
||||
Key? key,
|
||||
required this.items,
|
||||
required this.controller,
|
||||
required this.onSelected,
|
||||
required this.node,
|
||||
}) : super(key: key);
|
||||
|
||||
final List items;
|
||||
final TextEditingController controller;
|
||||
final ValueChanged<String> onSelected;
|
||||
final FocusScopeNode node;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = FluentTheme.of(context);
|
||||
final localizations = FluentLocalizations.of(context);
|
||||
return FocusScope(
|
||||
node: node,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 380),
|
||||
decoration: ShapeDecoration(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(4.0),
|
||||
),
|
||||
),
|
||||
color: theme.menuColor,
|
||||
shadows: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
offset: const Offset(-1, 1),
|
||||
blurRadius: 2.0,
|
||||
spreadRadius: 3.0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
offset: const Offset(1, 1),
|
||||
blurRadius: 2.0,
|
||||
spreadRadius: 3.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: controller,
|
||||
builder: (context, value, _) {
|
||||
final items = AutoSuggestBox.defaultItemSorter(
|
||||
value.text,
|
||||
this.items,
|
||||
);
|
||||
late Widget result;
|
||||
if (items.isEmpty) {
|
||||
result = Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: _AutoSuggestBoxOverlayTile(
|
||||
text: localizations.noResultsFoundLabel),
|
||||
);
|
||||
} else {
|
||||
result = ListView(
|
||||
key: ValueKey<int>(items.length),
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
children: List.generate(items.length, (index) {
|
||||
final item = items[index];
|
||||
return _AutoSuggestBoxOverlayTile(
|
||||
text: '$item',
|
||||
onSelected: () => onSelected(item),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AutoSuggestBoxOverlayTile extends StatefulWidget {
|
||||
const _AutoSuggestBoxOverlayTile({
|
||||
Key? key,
|
||||
required this.text,
|
||||
this.onSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
final String text;
|
||||
final VoidCallback? onSelected;
|
||||
|
||||
@override
|
||||
__AutoSuggestBoxOverlayTileState createState() =>
|
||||
__AutoSuggestBoxOverlayTileState();
|
||||
}
|
||||
|
||||
class __AutoSuggestBoxOverlayTileState extends State<_AutoSuggestBoxOverlayTile>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController controller;
|
||||
final node = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 125),
|
||||
);
|
||||
controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
node.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = FluentTheme.of(context);
|
||||
return HoverButton(
|
||||
focusNode: node,
|
||||
onPressed: widget.onSelected,
|
||||
margin: const EdgeInsets.only(top: 4.0, left: 4.0, right: 4.0),
|
||||
builder: (context, states) => Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 36.0,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
theme,
|
||||
states.isDisabled ? {ButtonStates.none} : states,
|
||||
),
|
||||
),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: EntrancePageTransition(
|
||||
animation: Tween<double>(
|
||||
begin: 0.75,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: controller,
|
||||
curve: Curves.easeOut,
|
||||
)),
|
||||
vertical: true,
|
||||
child: Text(
|
||||
widget.text,
|
||||
style: theme.typography.body,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (states.isFocused)
|
||||
Positioned(
|
||||
top: 11.0,
|
||||
bottom: 11.0,
|
||||
left: 0.0,
|
||||
child: Container(
|
||||
width: 3.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.accentColor,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1369
dependencies/fluent_ui-3.12.0/lib/src/controls/form/combo_box.dart
vendored
Normal file
1369
dependencies/fluent_ui-3.12.0/lib/src/controls/form/combo_box.dart
vendored
Normal file
File diff suppressed because it is too large
Load Diff
59
dependencies/fluent_ui-3.12.0/lib/src/controls/form/form_row.dart
vendored
Normal file
59
dependencies/fluent_ui-3.12.0/lib/src/controls/form/form_row.dart
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
const EdgeInsetsGeometry _kDefaultPadding = EdgeInsetsDirectional.fromSTEB(
|
||||
20.0,
|
||||
6.0,
|
||||
6.0,
|
||||
6.0,
|
||||
);
|
||||
|
||||
class FormRow extends StatelessWidget {
|
||||
const FormRow({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.padding = _kDefaultPadding,
|
||||
this.helper,
|
||||
this.error,
|
||||
this.textStyle,
|
||||
}) : super(key: key);
|
||||
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
final TextStyle? textStyle;
|
||||
|
||||
final Widget? helper;
|
||||
|
||||
final Widget? error;
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: <Widget>[
|
||||
Flexible(child: child),
|
||||
if (helper != null)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: textStyle!,
|
||||
child: helper!,
|
||||
),
|
||||
),
|
||||
if (error != null)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 2.0),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(
|
||||
color: Colors.warningPrimaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: error!,
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
579
dependencies/fluent_ui-3.12.0/lib/src/controls/form/pickers/date_picker.dart
vendored
Normal file
579
dependencies/fluent_ui-3.12.0/lib/src/controls/form/pickers/date_picker.dart
vendored
Normal file
@@ -0,0 +1,579 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:fluent_ui/src/utils/popup.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'pickers.dart';
|
||||
|
||||
// There is a known issue with clicking in the popup and select the date.
|
||||
// The current workaround is very hacky and doesn't work very well with the
|
||||
// current implementation. TODO: Fix clicking on ListWheelScrollView
|
||||
// https://github.com/flutter/flutter/issues/38803
|
||||
|
||||
/// The date picker gives you a standardized way to let users pick a localized
|
||||
/// date value using touch, mouse, or keyboard input. Use a date picker to let
|
||||
/// a user pick a known date, such as a date of birth, where the context of the
|
||||
/// calendar is not important.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// - [DatePicker Documentation](https://pub.dev/packages/fluent_ui#date-picker)
|
||||
/// - [TimePicker](https://pub.dev/packages/fluent_ui#time-picker)
|
||||
class DatePicker extends StatefulWidget {
|
||||
const DatePicker({
|
||||
Key? key,
|
||||
required this.selected,
|
||||
this.onChanged,
|
||||
this.onCancel,
|
||||
this.header,
|
||||
this.headerStyle,
|
||||
this.showDay = true,
|
||||
this.showMonth = true,
|
||||
this.showYear = true,
|
||||
this.startYear,
|
||||
this.endYear,
|
||||
this.contentPadding = kPickerContentPadding,
|
||||
this.popupHeight = kPopupHeight,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The current date.
|
||||
final DateTime selected;
|
||||
|
||||
/// Whenever the current date is changed. If this is null, the picker is considered disabled
|
||||
final ValueChanged<DateTime>? onChanged;
|
||||
|
||||
/// Whenever the user cancels when changing the date.
|
||||
final VoidCallback? onCancel;
|
||||
|
||||
/// The header of the picker
|
||||
final String? header;
|
||||
|
||||
/// The style of the [header]
|
||||
final TextStyle? headerStyle;
|
||||
|
||||
/// Whenever to show the month property
|
||||
final bool showMonth;
|
||||
|
||||
/// Whenever to show the day property
|
||||
final bool showDay;
|
||||
|
||||
/// Whenever to show the year property
|
||||
final bool showYear;
|
||||
|
||||
/// The year to start counting from. If `null`, defaults to [date]'s year `- 100`
|
||||
final int? startYear;
|
||||
|
||||
/// The year to end the counting. If `null`, defaults to [date]'s year `+ 25`
|
||||
final int? endYear;
|
||||
|
||||
/// The padding of the picker. Defaults to [kPickerContentPadding]
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
|
||||
/// The focus node of the picker.
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// Whenever `autofocus` is enabled or not
|
||||
final bool autofocus;
|
||||
|
||||
/// The height of the popup. Defaults to [kPopupHeight]
|
||||
final double popupHeight;
|
||||
|
||||
@override
|
||||
_DatePickerState createState() => _DatePickerState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('selected', selected,
|
||||
ifNull: '${DateTime.now()}'))
|
||||
..add(FlagProperty('showMonth',
|
||||
value: showMonth, ifFalse: 'not displaying month'))
|
||||
..add(FlagProperty('showDay',
|
||||
value: showDay, ifFalse: 'not displaying day'))
|
||||
..add(FlagProperty('showYear',
|
||||
value: showYear, ifFalse: 'not displaying year'))
|
||||
..add(IntProperty('startYear', startYear ?? selected.year - 100))
|
||||
..add(IntProperty('endYear', endYear ?? selected.year + 25))
|
||||
..add(DiagnosticsProperty('contentPadding', contentPadding))
|
||||
..add(ObjectFlagProperty.has('focusNode', focusNode))
|
||||
..add(
|
||||
FlagProperty('autofocus', value: autofocus, ifFalse: 'manual focus'))
|
||||
..add(DoubleProperty('popupHeight', popupHeight));
|
||||
}
|
||||
}
|
||||
|
||||
class _DatePickerState extends State<DatePicker> {
|
||||
late DateTime date;
|
||||
final popupKey = GlobalKey<PopUpState>();
|
||||
|
||||
FixedExtentScrollController? _monthController;
|
||||
FixedExtentScrollController? _dayController;
|
||||
FixedExtentScrollController? _yearController;
|
||||
|
||||
int get startYear => (widget.startYear ?? DateTime.now().year - 100).toInt();
|
||||
int get endYear => (widget.endYear ?? DateTime.now().year + 25).toInt();
|
||||
|
||||
int get currentYear {
|
||||
return List.generate(endYear - startYear, (index) {
|
||||
return startYear + index;
|
||||
}).firstWhere((v) => v == date.year, orElse: () => 0);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
date = widget.selected;
|
||||
initControllers();
|
||||
}
|
||||
|
||||
void initControllers() {
|
||||
_monthController = FixedExtentScrollController(
|
||||
initialItem: date.month - 1,
|
||||
);
|
||||
_dayController = FixedExtentScrollController(
|
||||
initialItem: date.day - 1,
|
||||
);
|
||||
|
||||
_yearController = FixedExtentScrollController(
|
||||
initialItem: currentYear - startYear - 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_monthController?.dispose();
|
||||
_dayController?.dispose();
|
||||
_yearController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(DatePicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selected != date) {
|
||||
date = widget.selected;
|
||||
_monthController?.jumpToItem(date.month - 1);
|
||||
_dayController?.jumpToItem(date.day - 1);
|
||||
_yearController?.jumpToItem(currentYear - startYear - 1);
|
||||
}
|
||||
}
|
||||
|
||||
void handleDateChanged(DateTime newDate) {
|
||||
setState(() => date = newDate);
|
||||
}
|
||||
|
||||
Size? size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
Widget picker = HoverButton(
|
||||
autofocus: widget.autofocus,
|
||||
focusNode: widget.focusNode,
|
||||
onPressed: () async {
|
||||
await popupKey.currentState?.openPopup();
|
||||
_monthController?.dispose();
|
||||
_monthController = null;
|
||||
_dayController?.dispose();
|
||||
_dayController = null;
|
||||
_yearController?.dispose();
|
||||
_yearController = null;
|
||||
initControllers();
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.isDisabled) state = <ButtonStates>{};
|
||||
const divider = Divider(
|
||||
direction: Axis.vertical,
|
||||
style: DividerThemeData(
|
||||
verticalMargin: EdgeInsets.zero,
|
||||
horizontalMargin: EdgeInsets.zero,
|
||||
thickness: 0.6,
|
||||
),
|
||||
);
|
||||
return AnimatedContainer(
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
height: kPickerHeight,
|
||||
decoration: kPickerDecorationBuilder(context, state),
|
||||
child: Row(children: [
|
||||
if (widget.showMonth)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: () {
|
||||
// MONTH
|
||||
return Padding(
|
||||
padding: widget.contentPadding,
|
||||
child: Text(DateFormat.MMMM().format(widget.selected)),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
if (widget.showDay) ...[
|
||||
divider,
|
||||
Expanded(
|
||||
child: () {
|
||||
// DAY
|
||||
return Padding(
|
||||
padding: widget.contentPadding,
|
||||
child: Text(
|
||||
'${widget.selected.day}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
],
|
||||
if (widget.showYear) ...[
|
||||
divider,
|
||||
Expanded(
|
||||
child: () {
|
||||
// YEAR
|
||||
return Padding(
|
||||
padding: widget.contentPadding,
|
||||
child: Text(
|
||||
'${widget.selected.year}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
],
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
picker = PopUp(
|
||||
key: popupKey,
|
||||
child: picker,
|
||||
content: (context) => _DatePickerContentPopUp(
|
||||
height: widget.popupHeight,
|
||||
date: date,
|
||||
dayController: _dayController!,
|
||||
endYear: endYear,
|
||||
handleDateChanged: handleDateChanged,
|
||||
monthController: _monthController!,
|
||||
onCancel: () => widget.onCancel?.call(),
|
||||
onChanged: () => widget.onChanged?.call(date),
|
||||
showDay: widget.showDay,
|
||||
showMonth: widget.showMonth,
|
||||
showYear: widget.showYear,
|
||||
startYear: startYear,
|
||||
yearController: _yearController!,
|
||||
),
|
||||
);
|
||||
if (widget.header != null) {
|
||||
return InfoLabel(
|
||||
label: widget.header!,
|
||||
labelStyle: widget.headerStyle,
|
||||
child: picker,
|
||||
);
|
||||
}
|
||||
return picker;
|
||||
}
|
||||
}
|
||||
|
||||
class _DatePickerContentPopUp extends StatefulWidget {
|
||||
const _DatePickerContentPopUp({
|
||||
Key? key,
|
||||
required this.showMonth,
|
||||
required this.showDay,
|
||||
required this.showYear,
|
||||
required this.date,
|
||||
required this.handleDateChanged,
|
||||
required this.onChanged,
|
||||
required this.onCancel,
|
||||
required this.monthController,
|
||||
required this.dayController,
|
||||
required this.yearController,
|
||||
required this.startYear,
|
||||
required this.endYear,
|
||||
required this.height,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool showMonth;
|
||||
final bool showDay;
|
||||
final bool showYear;
|
||||
final DateTime date;
|
||||
final ValueChanged<DateTime> handleDateChanged;
|
||||
final VoidCallback onChanged;
|
||||
final VoidCallback onCancel;
|
||||
final FixedExtentScrollController monthController;
|
||||
final FixedExtentScrollController dayController;
|
||||
final FixedExtentScrollController yearController;
|
||||
final int startYear;
|
||||
final int endYear;
|
||||
final double height;
|
||||
|
||||
@override
|
||||
__DatePickerContentPopUpState createState() =>
|
||||
__DatePickerContentPopUpState();
|
||||
}
|
||||
|
||||
class __DatePickerContentPopUpState extends State<_DatePickerContentPopUp> {
|
||||
int _getDaysInMonth([int? month, int? year]) {
|
||||
year ??= DateTime.now().year;
|
||||
month ??= DateTime.now().month;
|
||||
return DateTimeRange(
|
||||
start: DateTime(year, month),
|
||||
end: DateTime(year, month + 1),
|
||||
).duration.inDays;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
const divider = Divider(
|
||||
direction: Axis.vertical,
|
||||
style: DividerThemeData(
|
||||
verticalMargin: EdgeInsets.zero,
|
||||
horizontalMargin: EdgeInsets.zero,
|
||||
),
|
||||
);
|
||||
|
||||
final highlightTileColor =
|
||||
theme.accentColor.resolveFromBrightness(theme.brightness);
|
||||
|
||||
return SizedBox(
|
||||
height: widget.height,
|
||||
child: Acrylic(
|
||||
tint: kPickerBackgroundColor(context),
|
||||
shape: kPickerShape(context),
|
||||
child: Column(children: [
|
||||
Expanded(
|
||||
child: Stack(children: [
|
||||
kHighlightTile(),
|
||||
Row(children: [
|
||||
if (widget.showMonth)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: () {
|
||||
final items = List.generate(
|
||||
12,
|
||||
(month) {
|
||||
month++;
|
||||
final text = DateFormat.MMMM().format(
|
||||
DateTime(1, month),
|
||||
);
|
||||
return ListTile(
|
||||
title: Text(
|
||||
text,
|
||||
style: kPickerPopupTextStyle(context)?.copyWith(
|
||||
color: month == widget.date.month
|
||||
? highlightTileColor.basedOnLuminance()
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
// MONTH
|
||||
return PickerNavigatorIndicator(
|
||||
onBackward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.monthController,
|
||||
false,
|
||||
12,
|
||||
);
|
||||
},
|
||||
onForward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.monthController,
|
||||
true,
|
||||
12,
|
||||
);
|
||||
},
|
||||
child: ListWheelScrollView.useDelegate(
|
||||
controller: widget.monthController,
|
||||
itemExtent: kOneLineTileHeight,
|
||||
diameterRatio: kPickerDiameterRatio,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
childDelegate: ListWheelChildLoopingListDelegate(
|
||||
children: items,
|
||||
),
|
||||
onSelectedItemChanged: (index) {
|
||||
final month = index + 1;
|
||||
final daysInMonth =
|
||||
_getDaysInMonth(month, widget.date.year);
|
||||
int day = widget.date.day;
|
||||
if (day > daysInMonth) day = daysInMonth;
|
||||
widget.handleDateChanged(DateTime(
|
||||
widget.date.year,
|
||||
month,
|
||||
day,
|
||||
widget.date.hour,
|
||||
widget.date.minute,
|
||||
widget.date.second,
|
||||
widget.date.millisecond,
|
||||
widget.date.microsecond,
|
||||
));
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
if (widget.showDay) ...[
|
||||
divider,
|
||||
Expanded(
|
||||
child: () {
|
||||
// DAY
|
||||
final daysInMonth =
|
||||
_getDaysInMonth(widget.date.month, widget.date.year);
|
||||
return PickerNavigatorIndicator(
|
||||
onBackward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.dayController,
|
||||
false,
|
||||
daysInMonth,
|
||||
);
|
||||
},
|
||||
onForward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.dayController,
|
||||
true,
|
||||
daysInMonth,
|
||||
);
|
||||
},
|
||||
child: ListWheelScrollView.useDelegate(
|
||||
controller: widget.dayController,
|
||||
itemExtent: kOneLineTileHeight,
|
||||
diameterRatio: kPickerDiameterRatio,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
childDelegate: ListWheelChildLoopingListDelegate(
|
||||
children: List<Widget>.generate(
|
||||
daysInMonth,
|
||||
(day) {
|
||||
day++;
|
||||
return ListTile(
|
||||
title: Center(
|
||||
child: Text(
|
||||
'$day',
|
||||
style: kPickerPopupTextStyle(context)
|
||||
?.copyWith(
|
||||
color: day == widget.date.day
|
||||
? highlightTileColor
|
||||
.basedOnLuminance()
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
onSelectedItemChanged: (index) {
|
||||
widget.handleDateChanged(DateTime(
|
||||
widget.date.year,
|
||||
widget.date.month,
|
||||
index + 1,
|
||||
widget.date.hour,
|
||||
widget.date.minute,
|
||||
widget.date.second,
|
||||
widget.date.millisecond,
|
||||
widget.date.microsecond,
|
||||
));
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
],
|
||||
if (widget.showYear) ...[
|
||||
divider,
|
||||
Expanded(
|
||||
child: () {
|
||||
final years = widget.endYear - widget.startYear;
|
||||
// YEAR
|
||||
return PickerNavigatorIndicator(
|
||||
onBackward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.yearController,
|
||||
false,
|
||||
years,
|
||||
);
|
||||
},
|
||||
onForward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.yearController,
|
||||
true,
|
||||
years,
|
||||
);
|
||||
},
|
||||
child: ListWheelScrollView(
|
||||
controller: widget.yearController,
|
||||
itemExtent: kOneLineTileHeight,
|
||||
diameterRatio: kPickerDiameterRatio,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
onSelectedItemChanged: (index) {
|
||||
widget.handleDateChanged(DateTime(
|
||||
widget.startYear + index + 1,
|
||||
widget.date.month,
|
||||
widget.date.day,
|
||||
widget.date.hour,
|
||||
widget.date.minute,
|
||||
widget.date.second,
|
||||
widget.date.millisecond,
|
||||
widget.date.microsecond,
|
||||
));
|
||||
setState(() {});
|
||||
},
|
||||
children: List.generate(years, (index) {
|
||||
// index++;
|
||||
final realYear = widget.startYear + index + 1;
|
||||
return ListTile(
|
||||
title: Center(
|
||||
child: Text(
|
||||
'$realYear',
|
||||
style: kPickerPopupTextStyle(context)
|
||||
?.copyWith(
|
||||
color: realYear == widget.date.year
|
||||
? highlightTileColor
|
||||
.basedOnLuminance()
|
||||
: null),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
],
|
||||
]),
|
||||
]),
|
||||
),
|
||||
const Divider(
|
||||
style: DividerThemeData(
|
||||
verticalMargin: EdgeInsets.zero,
|
||||
horizontalMargin: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
YesNoPickerControl(
|
||||
onChanged: () {
|
||||
Navigator.pop(context);
|
||||
widget.onChanged();
|
||||
},
|
||||
onCancel: () {
|
||||
Navigator.pop(context);
|
||||
widget.onCancel();
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
211
dependencies/fluent_ui-3.12.0/lib/src/controls/form/pickers/pickers.dart
vendored
Normal file
211
dependencies/fluent_ui-3.12.0/lib/src/controls/form/pickers/pickers.dart
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
const kPickerContentPadding = EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
);
|
||||
|
||||
const kPickerHeight = 32.0;
|
||||
const kPickerDiameterRatio = 100.0;
|
||||
|
||||
const kPopupHeight = kOneLineTileHeight * 10;
|
||||
|
||||
Color kPickerBackgroundColor(BuildContext context) =>
|
||||
FluentTheme.of(context).menuColor;
|
||||
|
||||
ShapeBorder kPickerShape(BuildContext context) => RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
side: BorderSide(
|
||||
color: FluentTheme.of(context).scaffoldBackgroundColor,
|
||||
width: 0.6,
|
||||
),
|
||||
);
|
||||
|
||||
TextStyle? kPickerPopupTextStyle(BuildContext context) {
|
||||
return FluentTheme.of(context).typography.body?.copyWith(fontSize: 16);
|
||||
}
|
||||
|
||||
Decoration kPickerDecorationBuilder(
|
||||
BuildContext context, Set<ButtonStates> states) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
return BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: ButtonThemeData.buttonColor(theme.brightness, states),
|
||||
);
|
||||
}
|
||||
|
||||
Widget kHighlightTile() {
|
||||
return Builder(builder: (context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
final highlightTileColor =
|
||||
theme.accentColor.resolveFromBrightness(theme.brightness);
|
||||
return Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
height: kOneLineTileHeight,
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: ListTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
tileColor: highlightTileColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class YesNoPickerControl extends StatelessWidget {
|
||||
const YesNoPickerControl({
|
||||
Key? key,
|
||||
required this.onChanged,
|
||||
required this.onCancel,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onChanged;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
|
||||
ButtonStyle style() {
|
||||
return ButtonStyle(
|
||||
backgroundColor: ButtonState.resolveWith(
|
||||
(states) => ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
states,
|
||||
),
|
||||
),
|
||||
border: ButtonState.all(BorderSide.none),
|
||||
);
|
||||
}
|
||||
|
||||
return FocusTheme(
|
||||
data: const FocusThemeData(renderOutside: false),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
height: kOneLineTileHeight / 1.2,
|
||||
child: Button(
|
||||
onPressed: onChanged,
|
||||
style: style(),
|
||||
child: const Icon(FluentIcons.check_mark),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
height: kOneLineTileHeight / 1.2,
|
||||
child: Button(
|
||||
onPressed: onCancel,
|
||||
style: style(),
|
||||
child: const Icon(FluentIcons.chrome_close),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PickerNavigatorIndicator extends StatelessWidget {
|
||||
const PickerNavigatorIndicator({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.onBackward,
|
||||
required this.onForward,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final VoidCallback onForward;
|
||||
final VoidCallback onBackward;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return HoverButton(
|
||||
onPressed: () {},
|
||||
builder: (context, state) {
|
||||
final show = state.isHovering || state.isPressing || state.isFocused;
|
||||
return ButtonTheme.merge(
|
||||
data: ButtonThemeData.all(ButtonStyle(
|
||||
padding: ButtonState.all(const EdgeInsets.all(2.0)),
|
||||
backgroundColor: ButtonState.all(kPickerBackgroundColor(context)),
|
||||
border: ButtonState.all(BorderSide.none),
|
||||
)),
|
||||
child: FocusTheme(
|
||||
data: const FocusThemeData(renderOutside: false),
|
||||
child: Stack(children: [
|
||||
child,
|
||||
if (show)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: kOneLineTileHeight,
|
||||
child: Button(
|
||||
onPressed: onBackward,
|
||||
child: const Center(
|
||||
child: Icon(FluentIcons.chevron_up, size: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (show)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: kOneLineTileHeight,
|
||||
child: Button(
|
||||
onPressed: onForward,
|
||||
child: const Center(
|
||||
child: Icon(FluentIcons.chevron_down, size: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void navigateSides(
|
||||
BuildContext context,
|
||||
FixedExtentScrollController controller,
|
||||
bool forward,
|
||||
int amount,
|
||||
) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final duration = FluentTheme.of(context).fasterAnimationDuration;
|
||||
final curve = FluentTheme.of(context).animationCurve;
|
||||
if (forward) {
|
||||
final currentItem = controller.selectedItem;
|
||||
int to = currentItem + 1;
|
||||
if (currentItem == amount - 1) to = 0;
|
||||
controller.animateToItem(
|
||||
to,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
} else {
|
||||
final currentItem = controller.selectedItem;
|
||||
int to = currentItem - 1;
|
||||
if (currentItem == 0) to = amount - 1;
|
||||
controller.animateToItem(
|
||||
to,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
}
|
||||
}
|
||||
504
dependencies/fluent_ui-3.12.0/lib/src/controls/form/pickers/time_picker.dart
vendored
Normal file
504
dependencies/fluent_ui-3.12.0/lib/src/controls/form/pickers/time_picker.dart
vendored
Normal file
@@ -0,0 +1,504 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluent_ui/src/utils/popup.dart';
|
||||
|
||||
import 'pickers.dart';
|
||||
|
||||
/// The time picker gives you a standardized way to let users pick a time
|
||||
/// value using touch, mouse, or keyboard input. Use a time picker to let
|
||||
/// a user pick a single time value.
|
||||
///
|
||||
/// 7
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// - [TimePicker Documentation](https://pub.dev/packages/fluent_ui#time-picker)
|
||||
/// - [DatePicker](https://pub.dev/packages/fluent_ui#date-picker)
|
||||
class TimePicker extends StatefulWidget {
|
||||
const TimePicker({
|
||||
Key? key,
|
||||
required this.selected,
|
||||
this.onChanged,
|
||||
this.onCancel,
|
||||
this.header,
|
||||
this.headerStyle,
|
||||
this.contentPadding = kPickerContentPadding,
|
||||
this.popupHeight = kPopupHeight,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.hourFormat = HourFormat.h,
|
||||
this.hourPlaceholder = 'hour',
|
||||
this.minutePlaceholder = 'minute',
|
||||
this.amText = 'AM',
|
||||
this.pmText = 'PM',
|
||||
this.minuteIncrement = 1,
|
||||
}) : super(key: key);
|
||||
|
||||
final DateTime? selected;
|
||||
final ValueChanged<DateTime>? onChanged;
|
||||
final VoidCallback? onCancel;
|
||||
final HourFormat hourFormat;
|
||||
|
||||
final String? header;
|
||||
final TextStyle? headerStyle;
|
||||
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
final FocusNode? focusNode;
|
||||
final bool autofocus;
|
||||
|
||||
final String hourPlaceholder;
|
||||
final String minutePlaceholder;
|
||||
final String amText;
|
||||
final String pmText;
|
||||
|
||||
final double popupHeight;
|
||||
final double minuteIncrement;
|
||||
|
||||
bool get use24Format => [HourFormat.HH, HourFormat.H].contains(hourFormat);
|
||||
|
||||
@override
|
||||
_TimePickerState createState() => _TimePickerState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<DateTime>('selected', selected));
|
||||
properties.add(EnumProperty<HourFormat>('hourFormat', hourFormat));
|
||||
properties.add(DiagnosticsProperty('contentPadding', contentPadding));
|
||||
properties.add(ObjectFlagProperty.has('focusNode', focusNode));
|
||||
properties.add(
|
||||
FlagProperty('autofocus', value: autofocus, ifFalse: 'manual focus'));
|
||||
properties.add(StringProperty('hourPlaceholder', hourPlaceholder));
|
||||
properties.add(StringProperty('minutePlaceholder', minutePlaceholder));
|
||||
properties.add(StringProperty('amText', amText));
|
||||
properties.add(StringProperty('pmText', pmText));
|
||||
properties.add(DoubleProperty('popupHeight', popupHeight));
|
||||
}
|
||||
}
|
||||
|
||||
class _TimePickerState extends State<TimePicker> {
|
||||
late DateTime time;
|
||||
|
||||
final popupKey = GlobalKey<PopUpState>();
|
||||
|
||||
FixedExtentScrollController? _hourController;
|
||||
FixedExtentScrollController? _minuteController;
|
||||
FixedExtentScrollController? _amPmController;
|
||||
|
||||
bool am = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
time = widget.selected ?? DateTime.now();
|
||||
initControllers();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hourController?.dispose();
|
||||
_minuteController?.dispose();
|
||||
_amPmController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TimePicker oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.selected != time) {
|
||||
time = widget.selected ?? DateTime.now();
|
||||
_hourController?.jumpToItem(() {
|
||||
int hour = time.hour - 1;
|
||||
if (!widget.use24Format) {
|
||||
hour -= 12;
|
||||
}
|
||||
return hour;
|
||||
}());
|
||||
_minuteController?.jumpToItem(time.minute);
|
||||
_amPmController?.jumpToItem(_isPm ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
void handleDateChanged(DateTime date) {
|
||||
setState(() => time = date);
|
||||
}
|
||||
|
||||
void initControllers() {
|
||||
_hourController = FixedExtentScrollController(
|
||||
initialItem: () {
|
||||
int hour = time.hour - 1;
|
||||
if (!widget.use24Format) {
|
||||
hour -= 12;
|
||||
}
|
||||
return hour;
|
||||
}(),
|
||||
);
|
||||
_minuteController = FixedExtentScrollController(initialItem: time.minute);
|
||||
|
||||
_amPmController = FixedExtentScrollController(initialItem: _isPm ? 1 : 0);
|
||||
}
|
||||
|
||||
bool get _isPm => time.hour > 12;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
Widget picker = HoverButton(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
onPressed: () async {
|
||||
await popupKey.currentState?.openPopup();
|
||||
_hourController?.dispose();
|
||||
_hourController = null;
|
||||
_minuteController?.dispose();
|
||||
_minuteController = null;
|
||||
_amPmController?.dispose();
|
||||
_amPmController = null;
|
||||
initControllers();
|
||||
},
|
||||
builder: (context, state) {
|
||||
const divider = Divider(
|
||||
direction: Axis.vertical,
|
||||
style: DividerThemeData(
|
||||
verticalMargin: EdgeInsets.zero,
|
||||
horizontalMargin: EdgeInsets.zero,
|
||||
thickness: 0.6,
|
||||
),
|
||||
);
|
||||
return AnimatedContainer(
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
height: kPickerHeight,
|
||||
decoration: kPickerDecorationBuilder(context, state),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: widget.contentPadding,
|
||||
child: Text(
|
||||
() {
|
||||
int hour = time.hour;
|
||||
if (!widget.use24Format && hour > 12) return '${hour - 12}';
|
||||
return '$hour';
|
||||
}(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
divider,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: widget.contentPadding,
|
||||
child: Text('${time.minute}', textAlign: TextAlign.center),
|
||||
),
|
||||
),
|
||||
divider,
|
||||
if (!widget.use24Format)
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: widget.contentPadding,
|
||||
child: Text(
|
||||
() {
|
||||
if (_isPm) return widget.pmText;
|
||||
return widget.amText;
|
||||
}(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
picker = PopUp(
|
||||
key: popupKey,
|
||||
child: picker,
|
||||
content: (context) => _TimePickerContentPopup(
|
||||
height: widget.popupHeight,
|
||||
onCancel: widget.onCancel ?? () {},
|
||||
onChanged: () => widget.onChanged?.call(time),
|
||||
amText: widget.amText,
|
||||
pmText: widget.pmText,
|
||||
handleDateChanged: handleDateChanged,
|
||||
date: widget.selected ?? DateTime.now(),
|
||||
amPmController: _amPmController!,
|
||||
hourController: _hourController!,
|
||||
minuteController: _minuteController!,
|
||||
use24Format: widget.use24Format,
|
||||
minuteIncrement: widget.minuteIncrement,
|
||||
),
|
||||
);
|
||||
if (widget.header != null) {
|
||||
return InfoLabel(
|
||||
label: widget.header!,
|
||||
labelStyle: widget.headerStyle,
|
||||
child: picker,
|
||||
);
|
||||
}
|
||||
return picker;
|
||||
}
|
||||
}
|
||||
|
||||
class _TimePickerContentPopup extends StatefulWidget {
|
||||
const _TimePickerContentPopup({
|
||||
Key? key,
|
||||
required this.date,
|
||||
required this.onChanged,
|
||||
required this.onCancel,
|
||||
required this.amText,
|
||||
required this.pmText,
|
||||
required this.handleDateChanged,
|
||||
required this.hourController,
|
||||
required this.minuteController,
|
||||
required this.amPmController,
|
||||
required this.use24Format,
|
||||
required this.height,
|
||||
required this.minuteIncrement,
|
||||
}) : super(key: key);
|
||||
|
||||
final FixedExtentScrollController hourController;
|
||||
final FixedExtentScrollController minuteController;
|
||||
final FixedExtentScrollController amPmController;
|
||||
|
||||
final VoidCallback onChanged;
|
||||
final VoidCallback onCancel;
|
||||
final DateTime date;
|
||||
final String amText;
|
||||
final String pmText;
|
||||
final ValueChanged<DateTime> handleDateChanged;
|
||||
|
||||
final bool use24Format;
|
||||
final double height;
|
||||
final double minuteIncrement;
|
||||
|
||||
@override
|
||||
__TimePickerContentPopupState createState() =>
|
||||
__TimePickerContentPopupState();
|
||||
}
|
||||
|
||||
class __TimePickerContentPopupState extends State<_TimePickerContentPopup> {
|
||||
bool get isAm => widget.amPmController.selectedItem == 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
const divider = Divider(
|
||||
direction: Axis.vertical,
|
||||
style: DividerThemeData(
|
||||
verticalMargin: EdgeInsets.zero,
|
||||
horizontalMargin: EdgeInsets.zero,
|
||||
),
|
||||
);
|
||||
final duration = FluentTheme.of(context).fasterAnimationDuration;
|
||||
final curve = FluentTheme.of(context).animationCurve;
|
||||
final hoursAmount = widget.use24Format ? 24 : 12;
|
||||
return SizedBox(
|
||||
height: widget.height,
|
||||
child: Acrylic(
|
||||
tint: kPickerBackgroundColor(context),
|
||||
shape: kPickerShape(context),
|
||||
child: Column(children: [
|
||||
Expanded(
|
||||
child: Stack(children: [
|
||||
kHighlightTile(),
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: PickerNavigatorIndicator(
|
||||
onBackward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.hourController,
|
||||
false,
|
||||
hoursAmount,
|
||||
);
|
||||
},
|
||||
onForward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.hourController,
|
||||
true,
|
||||
hoursAmount,
|
||||
);
|
||||
},
|
||||
child: ListWheelScrollView.useDelegate(
|
||||
controller: widget.hourController,
|
||||
childDelegate: ListWheelChildLoopingListDelegate(
|
||||
children: List.generate(
|
||||
hoursAmount,
|
||||
(index) => ListTile(
|
||||
title: Center(
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: kPickerPopupTextStyle(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
itemExtent: kOneLineTileHeight,
|
||||
diameterRatio: kPickerDiameterRatio,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
onSelectedItemChanged: (index) {
|
||||
int hour = index + 1;
|
||||
if (!widget.use24Format && !isAm) {
|
||||
hour += 12;
|
||||
}
|
||||
widget.handleDateChanged(DateTime(
|
||||
widget.date.year,
|
||||
widget.date.month,
|
||||
widget.date.day,
|
||||
hour,
|
||||
widget.date.minute,
|
||||
widget.date.second,
|
||||
widget.date.millisecond,
|
||||
widget.date.microsecond,
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
divider,
|
||||
Expanded(
|
||||
child: PickerNavigatorIndicator(
|
||||
onBackward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.minuteController,
|
||||
false,
|
||||
60,
|
||||
);
|
||||
},
|
||||
onForward: () {
|
||||
navigateSides(
|
||||
context,
|
||||
widget.minuteController,
|
||||
true,
|
||||
60,
|
||||
);
|
||||
},
|
||||
child: ListWheelScrollView.useDelegate(
|
||||
controller: widget.minuteController,
|
||||
childDelegate: ListWheelChildLoopingListDelegate(
|
||||
children: List.generate(60 ~/ widget.minuteIncrement,
|
||||
(index) {
|
||||
return ListTile(
|
||||
title: Center(
|
||||
child: Text(
|
||||
'${(index * widget.minuteIncrement).toInt()}',
|
||||
style: kPickerPopupTextStyle(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
itemExtent: kOneLineTileHeight,
|
||||
diameterRatio: kPickerDiameterRatio,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
onSelectedItemChanged: (index) {
|
||||
widget.handleDateChanged(DateTime(
|
||||
widget.date.year,
|
||||
widget.date.month,
|
||||
widget.date.day,
|
||||
widget.date.hour,
|
||||
index,
|
||||
widget.date.second,
|
||||
widget.date.millisecond,
|
||||
widget.date.microsecond,
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!widget.use24Format) ...[
|
||||
divider,
|
||||
Expanded(
|
||||
child: PickerNavigatorIndicator(
|
||||
onBackward: () {
|
||||
widget.amPmController.animateToItem(
|
||||
0,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
},
|
||||
onForward: () {
|
||||
widget.amPmController.animateToItem(
|
||||
1,
|
||||
duration: duration,
|
||||
curve: curve,
|
||||
);
|
||||
},
|
||||
child: ListWheelScrollView(
|
||||
controller: widget.amPmController,
|
||||
itemExtent: kOneLineTileHeight,
|
||||
physics: const FixedExtentScrollPhysics(),
|
||||
children: [
|
||||
ListTile(
|
||||
title: Center(
|
||||
child: Text(
|
||||
widget.amText,
|
||||
style: kPickerPopupTextStyle(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Center(
|
||||
child: Text(
|
||||
widget.pmText,
|
||||
style: kPickerPopupTextStyle(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
onSelectedItemChanged: (index) {
|
||||
setState(() {});
|
||||
int hour = widget.date.hour;
|
||||
final isAm = index == 0;
|
||||
if (!widget.use24Format) {
|
||||
// If it was previously am and now it's pm
|
||||
if (!isAm) {
|
||||
hour += 12;
|
||||
// If it was previously pm and now it's am
|
||||
} else if (isAm) {
|
||||
hour -= 12;
|
||||
}
|
||||
}
|
||||
widget.handleDateChanged(DateTime(
|
||||
widget.date.year,
|
||||
widget.date.month,
|
||||
widget.date.day,
|
||||
hour,
|
||||
widget.date.minute,
|
||||
widget.date.second,
|
||||
widget.date.millisecond,
|
||||
widget.date.microsecond,
|
||||
));
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
]),
|
||||
]),
|
||||
),
|
||||
const Divider(
|
||||
style: DividerThemeData(
|
||||
verticalMargin: EdgeInsets.zero,
|
||||
horizontalMargin: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
YesNoPickerControl(
|
||||
onChanged: () {
|
||||
Navigator.pop(context);
|
||||
widget.onChanged();
|
||||
},
|
||||
onCancel: () {
|
||||
Navigator.pop(context);
|
||||
widget.onCancel();
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
412
dependencies/fluent_ui-3.12.0/lib/src/controls/form/selection_controls.dart
vendored
Normal file
412
dependencies/fluent_ui-3.12.0/lib/src/controls/form/selection_controls.dart
vendored
Normal file
@@ -0,0 +1,412 @@
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
const double _kToolbarScreenPadding = 8.0;
|
||||
const double _kToolbarWidth = 180.0;
|
||||
|
||||
class _FluentTextSelectionControls extends TextSelectionControls {
|
||||
/// Fluent has no text selection handles.
|
||||
@override
|
||||
Size getHandleSize(double textLineHeight) {
|
||||
return Size.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildToolbar(
|
||||
BuildContext context,
|
||||
Rect globalEditableRegion,
|
||||
double textLineHeight,
|
||||
Offset selectionMidpoint,
|
||||
List<TextSelectionPoint> endpoints,
|
||||
TextSelectionDelegate delegate,
|
||||
ClipboardStatusNotifier? clipboardStatus,
|
||||
Offset? lastSecondaryTapDownPosition,
|
||||
) {
|
||||
return _FluentTextSelectionControlsToolbar(
|
||||
clipboardStatus: clipboardStatus,
|
||||
endpoints: endpoints,
|
||||
globalEditableRegion: globalEditableRegion,
|
||||
handleCut:
|
||||
canCut(delegate) ? () => handleCut(delegate, clipboardStatus) : null,
|
||||
handleCopy: canCopy(delegate)
|
||||
? () => handleCopy(delegate, clipboardStatus)
|
||||
: null,
|
||||
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
|
||||
handleSelectAll:
|
||||
canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
|
||||
selectionMidpoint: selectionMidpoint,
|
||||
lastSecondaryTapDownPosition: lastSecondaryTapDownPosition,
|
||||
textLineHeight: textLineHeight,
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the text selection handles, but desktop has none.
|
||||
@override
|
||||
Widget buildHandle(
|
||||
BuildContext context,
|
||||
TextSelectionHandleType type,
|
||||
double textLineHeight, [
|
||||
VoidCallback? onTap,
|
||||
double? startGlyphHeight,
|
||||
double? endGlyphHeight,
|
||||
]) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
/// Gets the position for the text selection handles, but desktop has none.
|
||||
@override
|
||||
Offset getHandleAnchor(
|
||||
TextSelectionHandleType type,
|
||||
double textLineHeight, [
|
||||
double? startGlyphHeight,
|
||||
double? endGlyphHeight,
|
||||
]) {
|
||||
return Offset.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
bool canSelectAll(TextSelectionDelegate delegate) {
|
||||
// Allow SelectAll when selection is not collapsed, unless everything has
|
||||
// already been selected. Same behavior as Android.
|
||||
final TextEditingValue value = delegate.textEditingValue;
|
||||
return delegate.selectAllEnabled &&
|
||||
value.text.isNotEmpty &&
|
||||
!(value.selection.start == 0 &&
|
||||
value.selection.end == value.text.length);
|
||||
}
|
||||
}
|
||||
|
||||
/// Text selection controls that loosely follows Fluent design conventions.
|
||||
final TextSelectionControls fluentTextSelectionControls =
|
||||
_FluentTextSelectionControls();
|
||||
|
||||
// Generates the child that's passed into FluentTextSelectionToolbar.
|
||||
class _FluentTextSelectionControlsToolbar extends StatefulWidget {
|
||||
const _FluentTextSelectionControlsToolbar({
|
||||
Key? key,
|
||||
required this.clipboardStatus,
|
||||
required this.endpoints,
|
||||
required this.globalEditableRegion,
|
||||
required this.handleCopy,
|
||||
required this.handleCut,
|
||||
required this.handlePaste,
|
||||
required this.handleSelectAll,
|
||||
required this.selectionMidpoint,
|
||||
required this.textLineHeight,
|
||||
required this.lastSecondaryTapDownPosition,
|
||||
}) : super(key: key);
|
||||
|
||||
final ClipboardStatusNotifier? clipboardStatus;
|
||||
final List<TextSelectionPoint> endpoints;
|
||||
final Rect globalEditableRegion;
|
||||
final VoidCallback? handleCopy;
|
||||
final VoidCallback? handleCut;
|
||||
final VoidCallback? handlePaste;
|
||||
final VoidCallback? handleSelectAll;
|
||||
final Offset? lastSecondaryTapDownPosition;
|
||||
final Offset selectionMidpoint;
|
||||
final double textLineHeight;
|
||||
|
||||
@override
|
||||
_FluentTextSelectionControlsToolbarState createState() =>
|
||||
_FluentTextSelectionControlsToolbarState();
|
||||
}
|
||||
|
||||
class _FluentTextSelectionControlsToolbarState
|
||||
extends State<_FluentTextSelectionControlsToolbar> {
|
||||
ClipboardStatusNotifier? _clipboardStatus;
|
||||
|
||||
void _onChangedClipboardStatus() {
|
||||
setState(() {
|
||||
// Inform the widget that the value of clipboardStatus has changed.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.handlePaste != null) {
|
||||
_clipboardStatus = widget.clipboardStatus;
|
||||
_clipboardStatus!.addListener(_onChangedClipboardStatus);
|
||||
_clipboardStatus!.update();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_FluentTextSelectionControlsToolbar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.clipboardStatus != widget.clipboardStatus) {
|
||||
if (_clipboardStatus != null) {
|
||||
_clipboardStatus!.removeListener(_onChangedClipboardStatus);
|
||||
_clipboardStatus!.dispose();
|
||||
}
|
||||
_clipboardStatus = widget.clipboardStatus;
|
||||
_clipboardStatus!.addListener(_onChangedClipboardStatus);
|
||||
if (widget.handlePaste != null) {
|
||||
_clipboardStatus!.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
// When used in an Overlay, this can be disposed after its creator has
|
||||
// already disposed _clipboardStatus.
|
||||
if (_clipboardStatus != null && !_clipboardStatus!.disposed) {
|
||||
_clipboardStatus!.removeListener(_onChangedClipboardStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If there are no buttons to be shown, don't render anything.
|
||||
if (widget.handleCut == null &&
|
||||
widget.handleCopy == null &&
|
||||
widget.handlePaste == null &&
|
||||
widget.handleSelectAll == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||
|
||||
final Offset midpointAnchor = Offset(
|
||||
(widget.selectionMidpoint.dx - widget.globalEditableRegion.left).clamp(
|
||||
mediaQuery.padding.left,
|
||||
mediaQuery.size.width - mediaQuery.padding.right,
|
||||
),
|
||||
widget.selectionMidpoint.dy - widget.globalEditableRegion.top,
|
||||
);
|
||||
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
final FluentLocalizations localizations = FluentLocalizations.of(context);
|
||||
final List<Widget> items = <Widget>[];
|
||||
|
||||
void addToolbarButton(
|
||||
String text,
|
||||
IconData? icon,
|
||||
String shortcut,
|
||||
String tooltip,
|
||||
VoidCallback onPressed,
|
||||
) {
|
||||
items.add(_FluentTextSelectionToolbarButton(
|
||||
onPressed: onPressed,
|
||||
icon: icon,
|
||||
shortcut: shortcut,
|
||||
tooltip: tooltip,
|
||||
text: text,
|
||||
));
|
||||
}
|
||||
|
||||
if (widget.handleCut != null) {
|
||||
addToolbarButton(
|
||||
localizations.cutActionLabel,
|
||||
FluentIcons.cut,
|
||||
localizations.cutShortcut,
|
||||
localizations.cutActionTooltip,
|
||||
widget.handleCut!,
|
||||
);
|
||||
}
|
||||
if (widget.handleCopy != null) {
|
||||
addToolbarButton(
|
||||
localizations.copyActionLabel,
|
||||
FluentIcons.copy,
|
||||
localizations.copyShortcut,
|
||||
localizations.copyActionTooltip,
|
||||
widget.handleCopy!,
|
||||
);
|
||||
}
|
||||
if (widget.handlePaste != null &&
|
||||
_clipboardStatus!.value == ClipboardStatus.pasteable) {
|
||||
addToolbarButton(
|
||||
localizations.pasteActionLabel,
|
||||
FluentIcons.paste,
|
||||
localizations.pasteShortcut,
|
||||
localizations.pasteActionTooltip,
|
||||
widget.handlePaste!,
|
||||
);
|
||||
}
|
||||
if (widget.handleSelectAll != null) {
|
||||
addToolbarButton(
|
||||
localizations.selectAllActionLabel,
|
||||
null,
|
||||
localizations.selectAllShortcut,
|
||||
localizations.selectAllActionTooltip,
|
||||
widget.handleSelectAll!,
|
||||
);
|
||||
}
|
||||
|
||||
// If there is no option available, build an empty widget.
|
||||
if (items.isEmpty) {
|
||||
return const SizedBox(width: 0.0, height: 0.0);
|
||||
}
|
||||
|
||||
return _FluentTextSelectionToolbar(
|
||||
anchor: widget.lastSecondaryTapDownPosition ?? midpointAnchor,
|
||||
children: items,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A Fluent-style desktop text selection toolbar.
|
||||
///
|
||||
/// Typically displays buttons for text manipulation, e.g. copying and pasting
|
||||
/// text.
|
||||
///
|
||||
/// Tries to position itself as closesly as possible to [anchor] while remaining
|
||||
/// fully on-screen.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [_FluentTextSelectionControls.buildToolbar], where this is used by
|
||||
/// default to build a Fluent-style desktop toolbar.
|
||||
/// * [TextSelectionToolbar], which is similar, but builds an Android-style
|
||||
/// toolbar.
|
||||
class _FluentTextSelectionToolbar extends StatelessWidget {
|
||||
/// Creates an instance of _FluentTextSelectionToolbar.
|
||||
const _FluentTextSelectionToolbar({
|
||||
Key? key,
|
||||
required this.anchor,
|
||||
required this.children,
|
||||
}) : assert(children.length > 0),
|
||||
super(key: key);
|
||||
|
||||
/// The point at which the toolbar will attempt to position itself as closely
|
||||
/// as possible.
|
||||
final Offset anchor;
|
||||
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||
|
||||
final double paddingAbove = mediaQuery.padding.top + _kToolbarScreenPadding;
|
||||
final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
_kToolbarScreenPadding,
|
||||
paddingAbove,
|
||||
_kToolbarScreenPadding,
|
||||
_kToolbarScreenPadding,
|
||||
),
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: DesktopTextSelectionToolbarLayoutDelegate(
|
||||
anchor: anchor - localAdjustment,
|
||||
),
|
||||
child: PhysicalModel(
|
||||
elevation: 4.0,
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).micaBackgroundColor,
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
border: Border.all(
|
||||
width: 0.25,
|
||||
color: FluentTheme.of(context).inactiveBackgroundColor,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.only(top: 5.0, left: 5.0, right: 5.0),
|
||||
child: SizedBox(
|
||||
width: _kToolbarWidth,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A [TextButton] for the Fluent desktop text selection toolbar.
|
||||
class _FluentTextSelectionToolbarButton extends StatelessWidget {
|
||||
const _FluentTextSelectionToolbarButton({
|
||||
Key? key,
|
||||
required this.onPressed,
|
||||
required this.text,
|
||||
required this.icon,
|
||||
required this.shortcut,
|
||||
required this.tooltip,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final String text;
|
||||
final IconData? icon;
|
||||
final String shortcut;
|
||||
final String tooltip;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return HoverButton(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
builder: (context, states) {
|
||||
final theme = FluentTheme.of(context);
|
||||
final radius = BorderRadius.circular(4.0);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 5.0),
|
||||
child: FocusBorder(
|
||||
focused: states.isFocused,
|
||||
renderOutside: true,
|
||||
style: FocusThemeData(borderRadius: radius),
|
||||
child: Tooltip(
|
||||
message: tooltip,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(theme, states),
|
||||
borderRadius: radius,
|
||||
),
|
||||
padding: const EdgeInsets.only(
|
||||
top: 4.0,
|
||||
bottom: 4.0,
|
||||
left: 10.0,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 10.0),
|
||||
child: Icon(icon, size: 16.0),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 10.0),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
inherit: false,
|
||||
fontSize: 14.0,
|
||||
letterSpacing: -0.15,
|
||||
color: theme.inactiveColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
shortcut,
|
||||
style: TextStyle(
|
||||
inherit: false,
|
||||
fontSize: 12.0,
|
||||
color: theme.borderInputColor,
|
||||
height: 0.7,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
1090
dependencies/fluent_ui-3.12.0/lib/src/controls/form/text_box.dart
vendored
Normal file
1090
dependencies/fluent_ui-3.12.0/lib/src/controls/form/text_box.dart
vendored
Normal file
File diff suppressed because it is too large
Load Diff
277
dependencies/fluent_ui-3.12.0/lib/src/controls/form/text_form_box.dart
vendored
Normal file
277
dependencies/fluent_ui-3.12.0/lib/src/controls/form/text_form_box.dart
vendored
Normal file
@@ -0,0 +1,277 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// A [FormField] that contains a [TextBox].
|
||||
///
|
||||
/// This is a convenience widget that wraps a [TextBox] widget in a
|
||||
/// [FormField].
|
||||
///
|
||||
/// A [Form] ancestor is not required. The [Form] simply makes it easier to
|
||||
/// save, reset, or validate multiple fields at once. To use without a [Form],
|
||||
/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to
|
||||
/// save or reset the form field.
|
||||
///
|
||||
/// When a [controller] is specified, its [TextEditingController.text]
|
||||
/// defines the [initialValue]. If this [FormField] is part of a scrolling
|
||||
/// container that lazily constructs its children, like a [ListView] or a
|
||||
/// [CustomScrollView], then a [controller] should be specified.
|
||||
/// The controller's lifetime should be managed by a stateful widget ancestor
|
||||
/// of the scrolling container.
|
||||
///
|
||||
/// If a [controller] is not specified, [initialValue] can be used to give
|
||||
/// the automatically generated controller an initial value.
|
||||
///
|
||||
/// Remember to call [TextEditingController.dispose] of the [TextEditingController]
|
||||
/// when it is no longer needed. This will ensure we discard any resources used
|
||||
/// by the object.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/text-box>
|
||||
/// * [TextBox], which is the underlying text field without the [Form]
|
||||
/// integration.
|
||||
class TextFormBox extends FormField<String> {
|
||||
/// Creates a text form box
|
||||
TextFormBox({
|
||||
Key? key,
|
||||
this.controller,
|
||||
String? initialValue,
|
||||
FocusNode? focusNode,
|
||||
TextInputType? keyboardType,
|
||||
TextCapitalization textCapitalization = TextCapitalization.none,
|
||||
TextInputAction? textInputAction,
|
||||
TextStyle? style,
|
||||
StrutStyle? strutStyle,
|
||||
TextDirection? textDirection,
|
||||
TextAlign textAlign = TextAlign.start,
|
||||
TextAlignVertical? textAlignVertical,
|
||||
bool autofocus = false,
|
||||
bool readOnly = false,
|
||||
ToolbarOptions? toolbarOptions,
|
||||
bool? showCursor,
|
||||
String obscuringCharacter = '•',
|
||||
bool obscureText = false,
|
||||
bool autocorrect = true,
|
||||
SmartDashesType? smartDashesType,
|
||||
SmartQuotesType? smartQuotesType,
|
||||
bool enableSuggestions = true,
|
||||
int? maxLines = 1,
|
||||
int? minLines,
|
||||
bool expands = false,
|
||||
int? maxLength,
|
||||
double? minHeight,
|
||||
EdgeInsetsGeometry padding = kTextBoxPadding,
|
||||
ValueChanged<String>? onChanged,
|
||||
GestureTapCallback? onTap,
|
||||
VoidCallback? onEditingComplete,
|
||||
ValueChanged<String>? onFieldSubmitted,
|
||||
FormFieldSetter<String>? onSaved,
|
||||
FormFieldValidator<String>? validator,
|
||||
List<TextInputFormatter>? inputFormatters,
|
||||
bool? enabled,
|
||||
double cursorWidth = 2.0,
|
||||
double? cursorHeight,
|
||||
Radius cursorRadius = const Radius.circular(2.0),
|
||||
Color? cursorColor,
|
||||
Brightness? keyboardAppearance,
|
||||
EdgeInsets scrollPadding = const EdgeInsets.all(20.0),
|
||||
bool enableInteractiveSelection = true,
|
||||
TextSelectionControls? selectionControls,
|
||||
ScrollPhysics? scrollPhysics,
|
||||
Iterable<String>? autofillHints,
|
||||
AutovalidateMode autovalidateMode = AutovalidateMode.disabled,
|
||||
String? placeholder,
|
||||
TextStyle? placeholderStyle,
|
||||
String? header,
|
||||
TextStyle? headerStyle,
|
||||
ScrollController? scrollController,
|
||||
Clip clipBehavior = Clip.antiAlias,
|
||||
Widget? prefix,
|
||||
OverlayVisibilityMode prefixMode = OverlayVisibilityMode.always,
|
||||
Widget? suffix,
|
||||
OverlayVisibilityMode suffixMode = OverlayVisibilityMode.always,
|
||||
DragStartBehavior dragStartBehavior = DragStartBehavior.start,
|
||||
String? restorationId,
|
||||
MaxLengthEnforcement? maxLengthEnforcement,
|
||||
ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||
ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||
BoxDecoration? decoration,
|
||||
bool hidePadding = false
|
||||
}) : assert(initialValue == null || controller == null),
|
||||
assert(obscuringCharacter.length == 1),
|
||||
assert(maxLines == null || maxLines > 0),
|
||||
assert(minLines == null || minLines > 0),
|
||||
assert(
|
||||
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||
"minLines can't be greater than maxLines",
|
||||
),
|
||||
assert(
|
||||
!expands || (maxLines == null && minLines == null),
|
||||
'minLines and maxLines must be null when expands is true.',
|
||||
),
|
||||
assert(!obscureText || maxLines == 1,
|
||||
'Obscured fields cannot be multiline.'),
|
||||
assert(maxLength == null || maxLength > 0),
|
||||
super(
|
||||
key: key,
|
||||
initialValue: controller?.text ?? initialValue ?? '',
|
||||
onSaved: onSaved,
|
||||
validator: validator,
|
||||
autovalidateMode: autovalidateMode,
|
||||
builder: (FormFieldState<String> field) {
|
||||
final _TextFormBoxState state = field as _TextFormBoxState;
|
||||
|
||||
void onChangedHandler(String value) {
|
||||
field.didChange(value);
|
||||
if (onChanged != null) {
|
||||
onChanged(value);
|
||||
}
|
||||
}
|
||||
|
||||
return FormRow(
|
||||
padding: EdgeInsets.only(bottom: field.errorText == null ? 22.0 : 0.0),
|
||||
error: (field.errorText == null) ? null : Text(field.errorText!),
|
||||
child: TextBox(
|
||||
controller: state._effectiveController,
|
||||
focusNode: focusNode,
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
style: style,
|
||||
strutStyle: strutStyle,
|
||||
textAlign: textAlign,
|
||||
textAlignVertical: textAlignVertical,
|
||||
textCapitalization: textCapitalization,
|
||||
autofocus: autofocus,
|
||||
toolbarOptions: toolbarOptions,
|
||||
readOnly: readOnly,
|
||||
showCursor: showCursor,
|
||||
obscuringCharacter: obscuringCharacter,
|
||||
obscureText: obscureText,
|
||||
autocorrect: autocorrect,
|
||||
smartDashesType: smartDashesType,
|
||||
smartQuotesType: smartQuotesType,
|
||||
enableSuggestions: enableSuggestions,
|
||||
maxLines: maxLines,
|
||||
minLines: minLines,
|
||||
expands: expands,
|
||||
maxLength: maxLength,
|
||||
onChanged: onChangedHandler,
|
||||
onTap: onTap,
|
||||
onEditingComplete: onEditingComplete,
|
||||
onSubmitted: onFieldSubmitted,
|
||||
inputFormatters: inputFormatters,
|
||||
enabled: enabled,
|
||||
cursorWidth: cursorWidth,
|
||||
cursorHeight: cursorHeight,
|
||||
cursorColor: cursorColor,
|
||||
cursorRadius: cursorRadius,
|
||||
scrollPadding: scrollPadding,
|
||||
scrollPhysics: scrollPhysics,
|
||||
keyboardAppearance: keyboardAppearance,
|
||||
enableInteractiveSelection: enableInteractiveSelection,
|
||||
autofillHints: autofillHints,
|
||||
placeholder: placeholder,
|
||||
placeholderStyle: placeholderStyle,
|
||||
header: header,
|
||||
headerStyle: headerStyle,
|
||||
scrollController: scrollController,
|
||||
clipBehavior: clipBehavior,
|
||||
prefix: prefix,
|
||||
prefixMode: prefixMode,
|
||||
suffix: suffix,
|
||||
suffixMode: suffixMode,
|
||||
highlightColor: (field.errorText == null) ? null : Colors.red,
|
||||
dragStartBehavior: dragStartBehavior,
|
||||
minHeight: minHeight,
|
||||
padding: padding,
|
||||
maxLengthEnforcement: maxLengthEnforcement,
|
||||
restorationId: restorationId,
|
||||
selectionHeightStyle: selectionHeightStyle,
|
||||
selectionWidthStyle: selectionWidthStyle,
|
||||
decoration: decoration,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final TextEditingController? controller;
|
||||
|
||||
@override
|
||||
FormFieldState<String> createState() => _TextFormBoxState();
|
||||
}
|
||||
|
||||
class _TextFormBoxState extends FormFieldState<String> {
|
||||
TextEditingController? _controller;
|
||||
|
||||
TextEditingController? get _effectiveController =>
|
||||
widget.controller ?? _controller;
|
||||
|
||||
@override
|
||||
TextFormBox get widget => super.widget as TextFormBox;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller == null) {
|
||||
_controller = TextEditingController(text: widget.initialValue);
|
||||
} else {
|
||||
widget.controller!.addListener(_handleControllerChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TextFormBox oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.controller != oldWidget.controller) {
|
||||
oldWidget.controller?.removeListener(_handleControllerChanged);
|
||||
widget.controller?.addListener(_handleControllerChanged);
|
||||
|
||||
if (oldWidget.controller != null && widget.controller == null) {
|
||||
_controller =
|
||||
TextEditingController.fromValue(oldWidget.controller!.value);
|
||||
}
|
||||
|
||||
if (widget.controller != null) {
|
||||
setValue(widget.controller!.text);
|
||||
if (oldWidget.controller == null) {
|
||||
_controller = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller?.removeListener(_handleControllerChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChange(String? value) {
|
||||
super.didChange(value);
|
||||
|
||||
if (value != null && _effectiveController!.text != value) {
|
||||
_effectiveController!.text = value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void reset() {
|
||||
super.reset();
|
||||
|
||||
if (widget.initialValue != null) {
|
||||
setState(() {
|
||||
_effectiveController!.text = widget.initialValue!;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleControllerChanged() {
|
||||
if (_effectiveController!.text != value) {
|
||||
didChange(_effectiveController!.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
188
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/base.dart
vendored
Normal file
188
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/base.dart
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
// import 'package:flutter/material.dart' as m;
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// {@template fluent_ui.buttons.base}
|
||||
/// Buttons give people a way to trigger an action. They’re typically found in
|
||||
/// forms, dialog panels, and dialogs.
|
||||
/// {@end-template}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://developer.microsoft.com/en-us/fluentui#/controls/android/button>
|
||||
/// * <https://developer.microsoft.com/en-us/fluentui#/controls/web/button>
|
||||
/// * [TextButton], a borderless button with mainly text-based content
|
||||
/// * [OutlinedButton], an outlined button
|
||||
/// * [FilledButton], a colored button
|
||||
abstract class BaseButton extends StatefulWidget {
|
||||
const BaseButton({
|
||||
Key? key,
|
||||
required this.onPressed,
|
||||
required this.onLongPress,
|
||||
required this.style,
|
||||
required this.focusNode,
|
||||
required this.autofocus,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Called when the button is tapped or otherwise activated.
|
||||
///
|
||||
/// If this callback and [onLongPress] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// Called when the button is long-pressed.
|
||||
///
|
||||
/// If this callback and [onPressed] are null, then the button will be disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [enabled], which is true if the button is enabled.
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// Customizes this button's appearance.
|
||||
final ButtonStyle? style;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// Typically the button's label.
|
||||
///
|
||||
/// Usually a [Text] widget
|
||||
final Widget child;
|
||||
|
||||
@protected
|
||||
ButtonStyle defaultStyleOf(BuildContext context);
|
||||
|
||||
@protected
|
||||
ButtonStyle? themeStyleOf(BuildContext context);
|
||||
|
||||
/// Whether the button is enabled or disabled.
|
||||
///
|
||||
/// Buttons are disabled by default. To enable a button, set its [onPressed]
|
||||
/// or [onLongPress] properties to a non-null value.
|
||||
bool get enabled => onPressed != null || onLongPress != null;
|
||||
|
||||
@override
|
||||
_BaseButtonState createState() => _BaseButtonState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
.add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
|
||||
properties.add(
|
||||
DiagnosticsProperty<ButtonStyle>('style', style, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
|
||||
defaultValue: null));
|
||||
}
|
||||
}
|
||||
|
||||
class _BaseButtonState extends State<BaseButton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final ThemeData theme = FluentTheme.of(context);
|
||||
|
||||
final ButtonStyle? widgetStyle = widget.style;
|
||||
final ButtonStyle? themeStyle = widget.themeStyleOf(context);
|
||||
final ButtonStyle defaultStyle = widget.defaultStyleOf(context);
|
||||
|
||||
T? effectiveValue<T>(T? Function(ButtonStyle? style) getProperty) {
|
||||
final T? widgetValue = getProperty(widgetStyle);
|
||||
final T? themeValue = getProperty(themeStyle);
|
||||
final T? defaultValue = getProperty(defaultStyle);
|
||||
return widgetValue ?? themeValue ?? defaultValue;
|
||||
}
|
||||
|
||||
final Widget result = HoverButton(
|
||||
autofocus: widget.autofocus,
|
||||
focusNode: widget.focusNode,
|
||||
onPressed: widget.onPressed,
|
||||
onLongPress: widget.onLongPress,
|
||||
builder: (context, states) {
|
||||
T? resolve<T>(
|
||||
ButtonState<T>? Function(ButtonStyle? style) getProperty) {
|
||||
return effectiveValue(
|
||||
(ButtonStyle? style) => getProperty(style)?.resolve(states),
|
||||
);
|
||||
}
|
||||
|
||||
final double? resolvedElevation =
|
||||
resolve<double?>((ButtonStyle? style) => style?.elevation);
|
||||
final TextStyle? resolvedTextStyle = theme.typography.body?.merge(
|
||||
resolve<TextStyle?>((ButtonStyle? style) => style?.textStyle));
|
||||
final Color? resolvedBackgroundColor =
|
||||
resolve<Color?>((ButtonStyle? style) => style?.backgroundColor);
|
||||
final Color? resolvedForegroundColor =
|
||||
resolve<Color?>((ButtonStyle? style) => style?.foregroundColor);
|
||||
final Color? resolvedShadowColor =
|
||||
resolve<Color?>((ButtonStyle? style) => style?.shadowColor);
|
||||
final EdgeInsetsGeometry resolvedPadding = resolve<EdgeInsetsGeometry?>(
|
||||
(ButtonStyle? style) => style?.padding) ??
|
||||
EdgeInsets.zero;
|
||||
final BorderSide? resolvedBorder =
|
||||
resolve<BorderSide?>((ButtonStyle? style) => style?.border);
|
||||
final OutlinedBorder resolvedShape =
|
||||
resolve<OutlinedBorder?>((ButtonStyle? style) => style?.shape) ??
|
||||
const RoundedRectangleBorder();
|
||||
|
||||
final EdgeInsetsGeometry padding = resolvedPadding
|
||||
.add(EdgeInsets.symmetric(
|
||||
horizontal: theme.visualDensity.horizontal,
|
||||
vertical: theme.visualDensity.vertical,
|
||||
))
|
||||
.clamp(EdgeInsets.zero, EdgeInsetsGeometry.infinity);
|
||||
final double? iconSize = resolve<double?>((style) => style?.iconSize);
|
||||
Widget result = PhysicalModel(
|
||||
color: Colors.transparent,
|
||||
shadowColor: resolvedShadowColor ?? Colors.black,
|
||||
elevation: resolvedElevation ?? 0.0,
|
||||
borderRadius: resolvedShape is RoundedRectangleBorder
|
||||
? resolvedShape.borderRadius is BorderRadius
|
||||
? resolvedShape.borderRadius as BorderRadius
|
||||
: BorderRadius.zero
|
||||
: BorderRadius.zero,
|
||||
child: AnimatedContainer(
|
||||
duration: FluentTheme.of(context).fasterAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
decoration: ShapeDecoration(
|
||||
shape: resolvedShape.copyWith(side: resolvedBorder),
|
||||
color: resolvedBackgroundColor,
|
||||
),
|
||||
padding: padding,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(
|
||||
color: resolvedForegroundColor,
|
||||
size: iconSize ?? 14.0,
|
||||
),
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
style: (resolvedTextStyle ?? const TextStyle())
|
||||
.copyWith(color: resolvedForegroundColor),
|
||||
textAlign: TextAlign.center,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
return FocusBorder(focused: states.isFocused, child: result);
|
||||
},
|
||||
);
|
||||
|
||||
return Semantics(
|
||||
container: true,
|
||||
button: true,
|
||||
enabled: widget.enabled,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
}
|
||||
78
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/button.dart
vendored
Normal file
78
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/button.dart
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// A button gives the user a way to trigger an immediate action.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ToggleButton], a button that can be on and off.
|
||||
/// * [SplitButtonBar], A button with two sides. One side initiates
|
||||
/// an action, and the other side opens a menu.
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/buttons>
|
||||
class Button extends BaseButton {
|
||||
/// Creates a button.
|
||||
const Button({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required VoidCallback? onPressed,
|
||||
VoidCallback? onLongPress,
|
||||
FocusNode? focusNode,
|
||||
bool autofocus = false,
|
||||
ButtonStyle? style,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
onLongPress: onLongPress,
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
);
|
||||
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final ThemeData theme = FluentTheme.of(context);
|
||||
return ButtonStyle(
|
||||
elevation: ButtonState.resolveWith((states) {
|
||||
if (states.isPressing) return 0.0;
|
||||
return 0.3;
|
||||
}),
|
||||
shadowColor: ButtonState.all(theme.shadowColor),
|
||||
padding: ButtonState.all(const EdgeInsets.only(
|
||||
left: 11.0,
|
||||
top: 5.0,
|
||||
right: 11.0,
|
||||
bottom: 6.0,
|
||||
)),
|
||||
shape: ButtonState.all(RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: theme.brightness.isLight
|
||||
? const Color.fromRGBO(0, 0, 0, 0.09)
|
||||
: const Color.fromRGBO(255, 255, 255, 0.05),
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
)),
|
||||
backgroundColor: ButtonState.resolveWith((states) {
|
||||
return ButtonThemeData.buttonColor(theme.brightness, states);
|
||||
}),
|
||||
foregroundColor: ButtonState.resolveWith((states) {
|
||||
if (states.isDisabled) return theme.disabledColor;
|
||||
return ButtonThemeData.buttonColor(theme.brightness, states).basedOnLuminance().toAccentColor()[
|
||||
states.isPressing
|
||||
? theme.brightness.isLight
|
||||
? 'lighter'
|
||||
: 'dark'
|
||||
: 'normal'];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ButtonStyle? themeStyleOf(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return ButtonTheme.of(context).defaultButtonStyle;
|
||||
}
|
||||
}
|
||||
77
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/filled_button.dart
vendored
Normal file
77
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/filled_button.dart
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// A colored button.
|
||||
///
|
||||
/// {@macro fluent_ui.buttons.base}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Button], the default button
|
||||
/// * [OutlinedButton], an outlined button
|
||||
/// * [TextButton], a borderless button with mainly text-based content
|
||||
class FilledButton extends Button {
|
||||
/// Creates a filled button
|
||||
const FilledButton({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required VoidCallback? onPressed,
|
||||
VoidCallback? onLongPress,
|
||||
FocusNode? focusNode,
|
||||
bool autofocus = false,
|
||||
ButtonStyle? style,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
onLongPress: onLongPress,
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
);
|
||||
|
||||
@override
|
||||
ButtonStyle? themeStyleOf(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final buttonTheme = ButtonTheme.of(context);
|
||||
return buttonTheme.filledButtonStyle;
|
||||
}
|
||||
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
final theme = FluentTheme.of(context);
|
||||
|
||||
final def = ButtonStyle(backgroundColor: ButtonState.resolveWith((states) {
|
||||
return backgroundColor(theme, states);
|
||||
}), foregroundColor: ButtonState.resolveWith((states) {
|
||||
if (states.isDisabled) {
|
||||
return theme.brightness.isDark ? theme.disabledColor : Colors.white;
|
||||
}
|
||||
return backgroundColor(theme, states).basedOnLuminance();
|
||||
}));
|
||||
|
||||
return super.defaultStyleOf(context).merge(def) ?? def;
|
||||
}
|
||||
|
||||
static Color backgroundColor(ThemeData theme, Set<ButtonStates> states) {
|
||||
if (states.isDisabled) {
|
||||
if (theme.brightness.isDark) {
|
||||
return const Color(0xFF434343);
|
||||
} else {
|
||||
return const Color(0xFFBFBFBF);
|
||||
}
|
||||
} else if (states.isPressing) {
|
||||
if (theme.brightness.isDark) {
|
||||
return theme.accentColor.darker;
|
||||
} else {
|
||||
return theme.accentColor.lighter;
|
||||
}
|
||||
} else if (states.isHovering) {
|
||||
if (theme.brightness.isDark) {
|
||||
return theme.accentColor.dark;
|
||||
} else {
|
||||
return theme.accentColor.light;
|
||||
}
|
||||
}
|
||||
return theme.accentColor;
|
||||
}
|
||||
}
|
||||
77
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/icon_button.dart
vendored
Normal file
77
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/icon_button.dart
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
enum IconButtonMode { tiny, small, large }
|
||||
|
||||
class IconButton extends BaseButton {
|
||||
const IconButton({
|
||||
Key? key,
|
||||
required Widget icon,
|
||||
required VoidCallback? onPressed,
|
||||
VoidCallback? onLongPress,
|
||||
FocusNode? focusNode,
|
||||
bool autofocus = false,
|
||||
ButtonStyle? style,
|
||||
this.iconButtonMode,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: icon,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
onLongPress: onLongPress,
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
);
|
||||
|
||||
final IconButtonMode? iconButtonMode;
|
||||
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
final isIconSmall = SmallIconButton.of(context) != null ||
|
||||
iconButtonMode == IconButtonMode.tiny;
|
||||
final isSmall = iconButtonMode != null
|
||||
? iconButtonMode != IconButtonMode.large
|
||||
: SmallIconButton.of(context) != null;
|
||||
return ButtonStyle(
|
||||
iconSize: ButtonState.all(isIconSmall ? 11.0 : null),
|
||||
padding: ButtonState.all(isSmall
|
||||
? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0)
|
||||
: const EdgeInsets.all(8.0)),
|
||||
backgroundColor: ButtonState.resolveWith((states) {
|
||||
return states.isDisabled
|
||||
? ButtonThemeData.buttonColor(theme.brightness, states)
|
||||
: ButtonThemeData.uncheckedInputColor(theme, states);
|
||||
}),
|
||||
foregroundColor: ButtonState.resolveWith((states) {
|
||||
if (states.isDisabled) return theme.disabledColor;
|
||||
return null;
|
||||
}),
|
||||
shape: ButtonState.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ButtonStyle? themeStyleOf(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return ButtonTheme.of(context).iconButtonStyle;
|
||||
}
|
||||
}
|
||||
|
||||
class SmallIconButton extends InheritedWidget {
|
||||
const SmallIconButton({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
static SmallIconButton? of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<SmallIconButton>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(SmallIconButton oldWidget) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
69
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/outlined_button.dart
vendored
Normal file
69
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/outlined_button.dart
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// An outlined button
|
||||
///
|
||||
/// {@macro fluent_ui.buttons.base}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [FilledButton], a colored button
|
||||
/// * [TextButton], a borderless button with mainly text-based content
|
||||
class OutlinedButton extends BaseButton {
|
||||
const OutlinedButton({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required VoidCallback? onPressed,
|
||||
VoidCallback? onLongPress,
|
||||
FocusNode? focusNode,
|
||||
bool autofocus = false,
|
||||
ButtonStyle? style,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
onLongPress: onLongPress,
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
);
|
||||
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
|
||||
return ButtonStyle(
|
||||
padding: ButtonState.all(const EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 6.0,
|
||||
)),
|
||||
shape: ButtonState.all(RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(2.0),
|
||||
)),
|
||||
border: ButtonState.all(BorderSide(color: theme.inactiveColor)),
|
||||
foregroundColor: ButtonState.all(theme.inactiveColor),
|
||||
backgroundColor: ButtonState.resolveWith((states) {
|
||||
if (states.isDisabled) {
|
||||
return theme.disabledColor.withOpacity(0.30);
|
||||
} else if (states.isPressing) {
|
||||
return theme.inactiveColor.withOpacity(0.25);
|
||||
} else if (states.isHovering) {
|
||||
return theme.inactiveColor.withOpacity(0.10);
|
||||
} else {
|
||||
return Colors.transparent;
|
||||
}
|
||||
}),
|
||||
textStyle: ButtonState.all(const TextStyle(
|
||||
fontSize: 13.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ButtonStyle? themeStyleOf(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return ButtonTheme.of(context).outlinedButtonStyle;
|
||||
}
|
||||
}
|
||||
69
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/text_button.dart
vendored
Normal file
69
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/text_button.dart
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// A borderless button with mainly text-based content
|
||||
///
|
||||
/// {@macro fluent_ui.buttons.base}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [OutlinedButton], an outlined button
|
||||
/// * [FilledButton], a colored button
|
||||
class TextButton extends BaseButton {
|
||||
/// Creates a text-button.
|
||||
const TextButton({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required VoidCallback? onPressed,
|
||||
VoidCallback? onLongPress,
|
||||
FocusNode? focusNode,
|
||||
bool autofocus = false,
|
||||
ButtonStyle? style,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
onLongPress: onLongPress,
|
||||
onPressed: onPressed,
|
||||
style: style,
|
||||
);
|
||||
|
||||
@override
|
||||
ButtonStyle defaultStyleOf(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
return ButtonStyle(
|
||||
backgroundColor: ButtonState.all(Colors.transparent),
|
||||
padding: ButtonState.all(const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8.0,
|
||||
)),
|
||||
foregroundColor: ButtonState.resolveWith((states) {
|
||||
late Color color;
|
||||
if (states.isDisabled) {
|
||||
color = theme.disabledColor;
|
||||
} else if (states.isPressing) {
|
||||
color = theme.accentColor.resolveFromBrightness(
|
||||
theme.brightness,
|
||||
level: 1,
|
||||
);
|
||||
} else if (states.isHovering) {
|
||||
color = theme.accentColor.resolveFromBrightness(theme.brightness);
|
||||
} else {
|
||||
color = theme.accentColor;
|
||||
}
|
||||
return color;
|
||||
}),
|
||||
textStyle: ButtonState.all(const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.5,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ButtonStyle? themeStyleOf(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return ButtonTheme.of(context).textButtonStyle;
|
||||
}
|
||||
}
|
||||
295
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/theme.dart
vendored
Normal file
295
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/buttons/theme.dart
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class ButtonStyle with Diagnosticable {
|
||||
const ButtonStyle({
|
||||
this.textStyle,
|
||||
this.backgroundColor,
|
||||
this.foregroundColor,
|
||||
this.shadowColor,
|
||||
this.elevation,
|
||||
this.padding,
|
||||
this.border,
|
||||
this.shape,
|
||||
this.iconSize,
|
||||
});
|
||||
|
||||
final ButtonState<TextStyle?>? textStyle;
|
||||
|
||||
final ButtonState<Color?>? backgroundColor;
|
||||
|
||||
final ButtonState<Color?>? foregroundColor;
|
||||
|
||||
final ButtonState<Color?>? shadowColor;
|
||||
|
||||
final ButtonState<double?>? elevation;
|
||||
|
||||
final ButtonState<EdgeInsetsGeometry?>? padding;
|
||||
|
||||
final ButtonState<BorderSide?>? border;
|
||||
|
||||
final ButtonState<OutlinedBorder?>? shape;
|
||||
|
||||
final ButtonState<double?>? iconSize;
|
||||
|
||||
ButtonStyle? merge(ButtonStyle? other) {
|
||||
if (other == null) return this;
|
||||
return ButtonStyle(
|
||||
textStyle: other.textStyle ?? textStyle,
|
||||
backgroundColor: other.backgroundColor ?? backgroundColor,
|
||||
foregroundColor: other.foregroundColor ?? foregroundColor,
|
||||
shadowColor: other.shadowColor ?? shadowColor,
|
||||
elevation: other.elevation ?? elevation,
|
||||
padding: other.padding ?? padding,
|
||||
border: other.border ?? border,
|
||||
shape: other.shape ?? shape,
|
||||
iconSize: other.iconSize ?? iconSize,
|
||||
);
|
||||
}
|
||||
|
||||
static ButtonStyle lerp(ButtonStyle? a, ButtonStyle? b, double t) {
|
||||
return ButtonStyle(
|
||||
textStyle:
|
||||
ButtonState.lerp(a?.textStyle, b?.textStyle, t, TextStyle.lerp),
|
||||
backgroundColor: ButtonState.lerp(
|
||||
a?.backgroundColor, b?.backgroundColor, t, Color.lerp),
|
||||
foregroundColor: ButtonState.lerp(
|
||||
a?.foregroundColor, b?.foregroundColor, t, Color.lerp),
|
||||
shadowColor:
|
||||
ButtonState.lerp(a?.shadowColor, b?.shadowColor, t, Color.lerp),
|
||||
elevation: ButtonState.lerp(a?.elevation, b?.elevation, t, lerpDouble),
|
||||
padding:
|
||||
ButtonState.lerp(a?.padding, b?.padding, t, EdgeInsetsGeometry.lerp),
|
||||
border: ButtonState.lerp(a?.border, b?.border, t, (a, b, t) {
|
||||
if (a == null && b == null) return null;
|
||||
if (a == null) return b;
|
||||
if (b == null) return a;
|
||||
return BorderSide.lerp(a, b, t);
|
||||
}),
|
||||
shape: ButtonState.lerp(a?.shape, b?.shape, t, (a, b, t) {
|
||||
return ShapeBorder.lerp(a, b, t) as OutlinedBorder;
|
||||
}),
|
||||
iconSize: ButtonState.lerp(
|
||||
a?.iconSize,
|
||||
b?.iconSize,
|
||||
t,
|
||||
lerpDouble,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ButtonStyle copyWith({
|
||||
ButtonState<TextStyle?>? textStyle,
|
||||
ButtonState<Color?>? backgroundColor,
|
||||
ButtonState<Color?>? foregroundColor,
|
||||
ButtonState<Color?>? shadowColor,
|
||||
ButtonState<double?>? elevation,
|
||||
ButtonState<EdgeInsetsGeometry?>? padding,
|
||||
ButtonState<BorderSide?>? border,
|
||||
ButtonState<OutlinedBorder?>? shape,
|
||||
ButtonState<double?>? iconSize,
|
||||
}) {
|
||||
return ButtonStyle(
|
||||
textStyle: textStyle ?? this.textStyle,
|
||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||
foregroundColor: foregroundColor ?? this.foregroundColor,
|
||||
shadowColor: shadowColor ?? this.shadowColor,
|
||||
elevation: elevation ?? this.elevation,
|
||||
padding: padding ?? this.padding,
|
||||
border: border ?? this.border,
|
||||
shape: shape ?? this.shape,
|
||||
iconSize: iconSize ?? this.iconSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [Button]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [Button] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class ButtonTheme extends InheritedTheme {
|
||||
/// Creates a button theme that controls the configurations for
|
||||
/// [Button].
|
||||
const ButtonTheme({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.data,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [Button] widgets.
|
||||
final ButtonThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [Button]s should
|
||||
/// look like, and merges in the current button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required ButtonThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return ButtonTheme(
|
||||
key: key,
|
||||
data: _getInheritedButtonThemeData(context)?.merge(data) ?? data,
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The data from the closest instance of this class that encloses the given
|
||||
/// context.
|
||||
///
|
||||
/// Defaults to [ThemeData.buttonTheme]
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// ButtonThemeData theme = ButtonTheme.of(context);
|
||||
/// ```
|
||||
static ButtonThemeData of(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return FluentTheme.of(context).buttonTheme.merge(
|
||||
_getInheritedButtonThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
static ButtonThemeData? _getInheritedButtonThemeData(BuildContext context) {
|
||||
final ButtonTheme? buttonTheme =
|
||||
context.dependOnInheritedWidgetOfExactType<ButtonTheme>();
|
||||
return buttonTheme?.data;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return ButtonTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ButtonTheme oldWidget) {
|
||||
return oldWidget.data != data;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ButtonThemeData with Diagnosticable {
|
||||
final ButtonStyle? defaultButtonStyle;
|
||||
final ButtonStyle? filledButtonStyle;
|
||||
final ButtonStyle? textButtonStyle;
|
||||
final ButtonStyle? outlinedButtonStyle;
|
||||
final ButtonStyle? iconButtonStyle;
|
||||
|
||||
const ButtonThemeData({
|
||||
this.defaultButtonStyle,
|
||||
this.filledButtonStyle,
|
||||
this.textButtonStyle,
|
||||
this.outlinedButtonStyle,
|
||||
this.iconButtonStyle,
|
||||
});
|
||||
|
||||
const ButtonThemeData.all(ButtonStyle? style)
|
||||
: defaultButtonStyle = style,
|
||||
filledButtonStyle = style,
|
||||
textButtonStyle = style,
|
||||
outlinedButtonStyle = style,
|
||||
iconButtonStyle = style;
|
||||
|
||||
static ButtonThemeData lerp(
|
||||
ButtonThemeData? a,
|
||||
ButtonThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return const ButtonThemeData();
|
||||
}
|
||||
|
||||
ButtonThemeData merge(ButtonThemeData? style) {
|
||||
if (style == null) return this;
|
||||
return ButtonThemeData(
|
||||
outlinedButtonStyle: style.outlinedButtonStyle ?? outlinedButtonStyle,
|
||||
filledButtonStyle: style.filledButtonStyle ?? filledButtonStyle,
|
||||
textButtonStyle: style.textButtonStyle ?? textButtonStyle,
|
||||
defaultButtonStyle: style.defaultButtonStyle ?? defaultButtonStyle,
|
||||
iconButtonStyle: style.iconButtonStyle ?? iconButtonStyle,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<ButtonStyle>(
|
||||
'outlinedButtonStyle', outlinedButtonStyle))
|
||||
..add(DiagnosticsProperty<ButtonStyle>(
|
||||
'filledButtonStyle', filledButtonStyle))
|
||||
..add(
|
||||
DiagnosticsProperty<ButtonStyle>('textButtonStyle', textButtonStyle))
|
||||
..add(DiagnosticsProperty<ButtonStyle>(
|
||||
'defaultButtonStyle', defaultButtonStyle))
|
||||
..add(
|
||||
DiagnosticsProperty<ButtonStyle>('iconButtonStyle', iconButtonStyle));
|
||||
}
|
||||
|
||||
/// Defines the default color used by [Button]s using the current brightness
|
||||
/// and state.
|
||||
///
|
||||
/// The color used for none and disabled are the same. Only the button
|
||||
/// content color should be changed. This can be done using the function
|
||||
/// [Color.basedOnLuminance] to define the contrast color.
|
||||
// Values eyeballed from Windows 10
|
||||
// Used when the state is not recieving any user
|
||||
// interaction or is disabled
|
||||
static Color buttonColor(Brightness brightness, Set<ButtonStates> states) {
|
||||
late Color color;
|
||||
if (brightness == Brightness.light) {
|
||||
if (states.isPressing) {
|
||||
color = const Color(0xFFf2f2f2);
|
||||
} else if (states.isHovering) {
|
||||
color = const Color(0xFFF6F6F6);
|
||||
} else {
|
||||
color = Colors.white;
|
||||
}
|
||||
return color;
|
||||
} else {
|
||||
if (states.isPressing) {
|
||||
color = const Color(0xFF272727);
|
||||
} else if (states.isHovering) {
|
||||
color = const Color(0xFF323232);
|
||||
} else {
|
||||
color = const Color(0xFF2b2b2b);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the default color used for inputs when checked, such as checkbox,
|
||||
/// radio button and toggle switch. It's based on the current style and the
|
||||
/// current state.
|
||||
static Color checkedInputColor(ThemeData theme, Set<ButtonStates> states) {
|
||||
final bool isDark = theme.brightness == Brightness.dark;
|
||||
return states.isPressing
|
||||
? isDark
|
||||
? theme.accentColor.darker
|
||||
: theme.accentColor.lighter
|
||||
: states.isHovering
|
||||
? isDark
|
||||
? theme.accentColor.dark
|
||||
: theme.accentColor.light
|
||||
: theme.accentColor;
|
||||
}
|
||||
|
||||
static Color uncheckedInputColor(ThemeData style, Set<ButtonStates> states) {
|
||||
if (style.brightness == Brightness.light) {
|
||||
if (states.isDisabled) return style.disabledColor;
|
||||
if (states.isPressing) return const Color(0xFF221D08).withOpacity(0.155);
|
||||
if (states.isHovering) return const Color(0xFF221D08).withOpacity(0.055);
|
||||
return Colors.transparent;
|
||||
} else {
|
||||
if (states.isDisabled) return style.disabledColor;
|
||||
if (states.isPressing) return const Color(0xFFFFF3E8).withOpacity(0.080);
|
||||
if (states.isHovering) return const Color(0xFFFFF3E8).withOpacity(0.12);
|
||||
return Colors.transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
385
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/checkbox.dart
vendored
Normal file
385
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/checkbox.dart
vendored
Normal file
@@ -0,0 +1,385 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// A check box is used to select or deselect action items. It can
|
||||
/// be used for a single item or for a list of multiple items that
|
||||
/// a user can choose from. The control has three selection states:
|
||||
/// unselected, selected, and indeterminate. Use the indeterminate
|
||||
/// state when a collection of sub-choices have both unselected and
|
||||
/// selected states.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
/// - [ToggleSwitch](https://pub.dev/packages/fluent_ui#toggle-switches)
|
||||
/// - [RadioButton](https://pub.dev/packages/fluent_ui#radio-buttons)
|
||||
/// - [ToggleButton]
|
||||
class Checkbox extends StatelessWidget {
|
||||
/// Creates a checkbox.
|
||||
const Checkbox({
|
||||
Key? key,
|
||||
required this.checked,
|
||||
required this.onChanged,
|
||||
this.style,
|
||||
this.content,
|
||||
this.semanticLabel,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Whether the checkbox is checked or not.
|
||||
///
|
||||
/// If `null`, the checkbox is in its third state.
|
||||
final bool? checked;
|
||||
|
||||
/// Called when the value of the [Checkbox] should change.
|
||||
///
|
||||
/// This callback passes a new value, but doesn't update its
|
||||
/// state internally.
|
||||
///
|
||||
/// If null, the checkbox is considered disabled.
|
||||
final ValueChanged<bool?>? onChanged;
|
||||
|
||||
/// The style applied to the checkbox. If non-null, it's mescled
|
||||
/// with [ThemeData.checkboxThemeData]
|
||||
final CheckboxThemeData? style;
|
||||
|
||||
/// The content of the radio button.
|
||||
///
|
||||
/// This, if non-null, is displayed at the right of the checkbox,
|
||||
/// and is affected by user touch.
|
||||
///
|
||||
/// Usually a [Text] or [Icon] widget
|
||||
final Widget? content;
|
||||
|
||||
/// {@macro fluent_ui.controls.inputs.HoverButton.semanticLabel}
|
||||
final String? semanticLabel;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(FlagProperty('checked', value: checked, ifFalse: 'unchecked'))
|
||||
..add(ObjectFlagProperty('onChanged', onChanged, ifNull: 'disabled'))
|
||||
..add(DiagnosticsProperty<CheckboxThemeData>('style', style))
|
||||
..add(StringProperty('semanticLabel', semanticLabel))
|
||||
..add(DiagnosticsProperty<FocusNode>('focusNode', focusNode))
|
||||
..add(FlagProperty(
|
||||
'autofocus',
|
||||
value: autofocus,
|
||||
defaultValue: false,
|
||||
ifFalse: 'manual focus',
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final CheckboxThemeData style = CheckboxTheme.of(context).merge(this.style);
|
||||
const double size = 20;
|
||||
return HoverButton(
|
||||
autofocus: autofocus,
|
||||
semanticLabel: semanticLabel,
|
||||
margin: style.margin,
|
||||
focusNode: focusNode,
|
||||
onPressed: onChanged == null
|
||||
? null
|
||||
: () => onChanged!(checked == null ? null : !(checked!)),
|
||||
builder: (context, state) {
|
||||
Widget child = AnimatedContainer(
|
||||
alignment: Alignment.center,
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
padding: style.padding,
|
||||
height: size,
|
||||
width: size,
|
||||
decoration: () {
|
||||
if (checked == null) {
|
||||
return style.thirdstateDecoration?.resolve(state);
|
||||
} else if (checked!) {
|
||||
return style.checkedDecoration?.resolve(state);
|
||||
} else {
|
||||
return style.uncheckedDecoration?.resolve(state);
|
||||
}
|
||||
}(),
|
||||
child: checked == null
|
||||
? _ThirdStateDash(
|
||||
color: style.thirdstateIconColor?.resolve(state) ??
|
||||
style.checkedIconColor?.resolve(state) ??
|
||||
FluentTheme.of(context).inactiveColor,
|
||||
)
|
||||
: Icon(
|
||||
style.icon,
|
||||
size: 12,
|
||||
color: () {
|
||||
if (checked == null) {
|
||||
return style.thirdstateIconColor?.resolve(state) ??
|
||||
style.checkedIconColor?.resolve(state);
|
||||
} else if (checked!) {
|
||||
return style.checkedIconColor?.resolve(state);
|
||||
} else {
|
||||
return style.uncheckedIconColor?.resolve(state);
|
||||
}
|
||||
}(),
|
||||
),
|
||||
);
|
||||
if (content != null) {
|
||||
child = Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
child,
|
||||
const SizedBox(width: 6.0),
|
||||
content!,
|
||||
]);
|
||||
}
|
||||
return Semantics(
|
||||
checked: checked,
|
||||
child: FocusBorder(
|
||||
focused: state.isFocused,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThirdStateDash extends StatelessWidget {
|
||||
const _ThirdStateDash({Key? key, required this.color}) : super(key: key);
|
||||
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 1.4,
|
||||
width: 8,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxTheme extends InheritedTheme {
|
||||
/// Creates a button theme that controls how descendant [Checkbox]es should
|
||||
/// look like.
|
||||
const CheckboxTheme({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.data,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final CheckboxThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [Checkbox]es should
|
||||
/// look like, and merges in the current button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required CheckboxThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return CheckboxTheme(
|
||||
key: key,
|
||||
data: _getInheritedCheckboxThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The data from the closest instance of this class that encloses the given
|
||||
/// context.
|
||||
///
|
||||
/// Defaults to [ThemeData.checkboxTheme]
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// CheckboxThemeData theme = CheckboxTheme.of(context);
|
||||
/// ```
|
||||
static CheckboxThemeData of(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return CheckboxThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedCheckboxThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
static CheckboxThemeData _getInheritedCheckboxThemeData(
|
||||
BuildContext context) {
|
||||
final CheckboxTheme? checkboxTheme =
|
||||
context.dependOnInheritedWidgetOfExactType<CheckboxTheme>();
|
||||
return checkboxTheme?.data ?? FluentTheme.of(context).checkboxTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return CheckboxTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(CheckboxTheme oldWidget) {
|
||||
return oldWidget.data != data;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class CheckboxThemeData with Diagnosticable {
|
||||
final ButtonState<Decoration?>? checkedDecoration;
|
||||
final ButtonState<Decoration?>? uncheckedDecoration;
|
||||
final ButtonState<Decoration?>? thirdstateDecoration;
|
||||
|
||||
final IconData? icon;
|
||||
final ButtonState<Color?>? checkedIconColor;
|
||||
final ButtonState<Color?>? uncheckedIconColor;
|
||||
final ButtonState<Color?>? thirdstateIconColor;
|
||||
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
const CheckboxThemeData({
|
||||
this.checkedDecoration,
|
||||
this.uncheckedDecoration,
|
||||
this.thirdstateDecoration,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.icon,
|
||||
this.checkedIconColor,
|
||||
this.uncheckedIconColor,
|
||||
this.thirdstateIconColor,
|
||||
});
|
||||
|
||||
factory CheckboxThemeData.standard(ThemeData style) {
|
||||
final BorderRadiusGeometry radius = BorderRadius.circular(4.0);
|
||||
return CheckboxThemeData(
|
||||
checkedDecoration: ButtonState.resolveWith(
|
||||
(states) => BoxDecoration(
|
||||
borderRadius: radius,
|
||||
color: !states.isDisabled
|
||||
? ButtonThemeData.checkedInputColor(style, states)
|
||||
: style.brightness.isLight
|
||||
? const Color.fromRGBO(0, 0, 0, 0.2169)
|
||||
: const Color.fromRGBO(255, 255, 255, 0.1581),
|
||||
),
|
||||
),
|
||||
uncheckedDecoration: ButtonState.resolveWith(
|
||||
(states) => BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: !states.isDisabled
|
||||
? style.borderInputColor
|
||||
: style.brightness.isLight
|
||||
? const Color.fromRGBO(0, 0, 0, 0.2169)
|
||||
: const Color.fromRGBO(255, 255, 255, 0.1581),
|
||||
),
|
||||
color:
|
||||
states.isHovering ? style.inactiveColor.withOpacity(0.1) : null,
|
||||
borderRadius: radius,
|
||||
),
|
||||
),
|
||||
thirdstateDecoration: ButtonState.resolveWith(
|
||||
(states) => BoxDecoration(
|
||||
borderRadius: radius,
|
||||
color: ButtonThemeData.checkedInputColor(style, states),
|
||||
),
|
||||
),
|
||||
checkedIconColor: ButtonState.resolveWith((states) {
|
||||
return !states.isDisabled
|
||||
? ButtonThemeData.checkedInputColor(
|
||||
style,
|
||||
states,
|
||||
).basedOnLuminance()
|
||||
: style.brightness.isLight
|
||||
? Colors.white
|
||||
: const Color.fromRGBO(255, 255, 255, 0.5302);
|
||||
}),
|
||||
uncheckedIconColor: ButtonState.all(Colors.transparent),
|
||||
icon: FluentIcons.check_mark,
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
);
|
||||
}
|
||||
|
||||
static CheckboxThemeData lerp(
|
||||
CheckboxThemeData? a,
|
||||
CheckboxThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return CheckboxThemeData(
|
||||
margin: EdgeInsetsGeometry.lerp(a?.margin, b?.margin, t),
|
||||
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
|
||||
icon: t < 0.5 ? a?.icon : b?.icon,
|
||||
checkedIconColor: ButtonState.lerp(
|
||||
a?.checkedIconColor, b?.checkedIconColor, t, Color.lerp),
|
||||
uncheckedIconColor: ButtonState.lerp(
|
||||
a?.uncheckedIconColor, b?.uncheckedIconColor, t, Color.lerp),
|
||||
thirdstateIconColor: ButtonState.lerp(
|
||||
a?.thirdstateIconColor, b?.thirdstateIconColor, t, Color.lerp),
|
||||
checkedDecoration: ButtonState.lerp(
|
||||
a?.checkedDecoration, b?.checkedDecoration, t, Decoration.lerp),
|
||||
uncheckedDecoration: ButtonState.lerp(
|
||||
a?.uncheckedDecoration, b?.uncheckedDecoration, t, Decoration.lerp),
|
||||
thirdstateDecoration: ButtonState.lerp(
|
||||
a?.thirdstateDecoration, b?.thirdstateDecoration, t, Decoration.lerp),
|
||||
);
|
||||
}
|
||||
|
||||
CheckboxThemeData merge(CheckboxThemeData? style) {
|
||||
return CheckboxThemeData(
|
||||
margin: style?.margin ?? margin,
|
||||
padding: style?.padding ?? padding,
|
||||
icon: style?.icon ?? icon,
|
||||
checkedIconColor: style?.checkedIconColor ?? checkedIconColor,
|
||||
uncheckedIconColor: style?.uncheckedIconColor ?? uncheckedIconColor,
|
||||
thirdstateIconColor: style?.thirdstateIconColor ?? thirdstateIconColor,
|
||||
checkedDecoration: style?.checkedDecoration ?? checkedDecoration,
|
||||
uncheckedDecoration: style?.uncheckedDecoration ?? uncheckedDecoration,
|
||||
thirdstateDecoration: style?.thirdstateDecoration ?? thirdstateDecoration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ObjectFlagProperty<ButtonState<Decoration?>?>.has(
|
||||
'thirdstateDecoration',
|
||||
thirdstateDecoration,
|
||||
));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Decoration?>?>.has(
|
||||
'uncheckedDecoration',
|
||||
uncheckedDecoration,
|
||||
));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Decoration?>?>.has(
|
||||
'checkedDecoration',
|
||||
checkedDecoration,
|
||||
));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Color?>?>.has(
|
||||
'thirdstateIconColor',
|
||||
thirdstateIconColor,
|
||||
));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Color?>?>.has(
|
||||
'uncheckedIconColor',
|
||||
uncheckedIconColor,
|
||||
));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Color?>?>.has(
|
||||
'checkedIconColor',
|
||||
checkedIconColor,
|
||||
));
|
||||
properties.add(IconDataProperty('icon', icon));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Decoration?>?>.has(
|
||||
'checkedDecoration',
|
||||
checkedDecoration,
|
||||
));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Decoration?>?>.has(
|
||||
'uncheckedDecoration',
|
||||
uncheckedDecoration,
|
||||
));
|
||||
properties.add(
|
||||
DiagnosticsProperty<EdgeInsetsGeometry?>('padding', padding),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry?>('margin', margin));
|
||||
}
|
||||
}
|
||||
312
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/chip.dart
vendored
Normal file
312
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/chip.dart
vendored
Normal file
@@ -0,0 +1,312 @@
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
const double _kChipSpacing = 6.0;
|
||||
|
||||
enum _ChipType {
|
||||
normal,
|
||||
selected,
|
||||
}
|
||||
|
||||
/// Chips are compact representations of entities (most commonly, people)
|
||||
/// that can be clicked, deleted, or dragged easily.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Button], a widget similar to [Chip], but adapted to larger screens
|
||||
class Chip extends StatelessWidget {
|
||||
/// Creates a normal chip.
|
||||
const Chip({
|
||||
Key? key,
|
||||
this.image,
|
||||
this.text,
|
||||
this.onPressed,
|
||||
this.semanticLabel,
|
||||
}) : _type = _ChipType.normal,
|
||||
super(key: key);
|
||||
|
||||
/// Creates a selected chip
|
||||
const Chip.selected({
|
||||
Key? key,
|
||||
this.image,
|
||||
this.text,
|
||||
this.onPressed,
|
||||
this.semanticLabel,
|
||||
}) : _type = _ChipType.selected,
|
||||
super(key: key);
|
||||
|
||||
/// The chip image. It's rendered before [text]
|
||||
///
|
||||
/// If disabled, a opacity of 0.6 is applied
|
||||
///
|
||||
/// It's usually a:
|
||||
///
|
||||
/// * [Icon]
|
||||
/// * [CircleAvatar]
|
||||
/// * [Image]
|
||||
final Widget? image;
|
||||
|
||||
/// The text of the chip. It's rendered after [image]
|
||||
///
|
||||
/// Typically a [Text]
|
||||
final Widget? text;
|
||||
|
||||
/// Called when the chip is pressed. If null, the chip will be considered disabled
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// {@macro fluent_ui.controls.inputs.HoverButton.semanticLabel}
|
||||
final String? semanticLabel;
|
||||
|
||||
final _ChipType _type;
|
||||
|
||||
bool get isEnabled => onPressed != null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = ChipTheme.of(context);
|
||||
final VisualDensity visualDensity = FluentTheme.of(context).visualDensity;
|
||||
final double spacing = theme.spacing ?? _kChipSpacing;
|
||||
return HoverButton(
|
||||
semanticLabel: semanticLabel,
|
||||
onPressed: onPressed,
|
||||
builder: (context, states) {
|
||||
final textStyle = _type == _ChipType.normal
|
||||
? theme.textStyle
|
||||
: theme.selectedTextStyle;
|
||||
final decoration = _type == _ChipType.normal
|
||||
? theme.decoration
|
||||
: theme.selectedDecoration;
|
||||
return AnimatedContainer(
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 24.0,
|
||||
maxHeight: 32.0,
|
||||
minWidth: 24,
|
||||
),
|
||||
decoration: decoration?.resolve(states),
|
||||
padding: EdgeInsets.only(
|
||||
left: spacing + visualDensity.horizontal,
|
||||
top: spacing,
|
||||
bottom: spacing,
|
||||
),
|
||||
child: AnimatedDefaultTextStyle(
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
style: textStyle?.resolve(states) ?? const TextStyle(),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (image != null)
|
||||
AnimatedOpacity(
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
opacity: isEnabled || _type == _ChipType.selected ? 1.0 : 0.6,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: spacing + visualDensity.horizontal),
|
||||
child: image,
|
||||
),
|
||||
),
|
||||
if (text != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: spacing + visualDensity.horizontal),
|
||||
child: text,
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [Chip]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [Chip] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class ChipTheme extends InheritedTheme {
|
||||
/// Creates a button theme that controls the configurations for
|
||||
/// [Chip].
|
||||
const ChipTheme({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.data,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [Chip] widgets.
|
||||
final ChipThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [Chip]s should
|
||||
/// look like, and merges in the current button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required ChipThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return ChipTheme(
|
||||
key: key,
|
||||
data: _getInheritedChipThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The data from the closest instance of this class that encloses the given
|
||||
/// context.
|
||||
///
|
||||
/// Defaults to [ThemeData.chipTheme]
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// ChipThemeData theme = ChipTheme.of(context);
|
||||
/// ```
|
||||
static ChipThemeData of(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return ChipThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedChipThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
static ChipThemeData _getInheritedChipThemeData(BuildContext context) {
|
||||
final ChipTheme? theme =
|
||||
context.dependOnInheritedWidgetOfExactType<ChipTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).chipTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return ChipTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ChipTheme oldWidget) {
|
||||
return oldWidget.data != data;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ChipThemeData with Diagnosticable {
|
||||
final ButtonState<Decoration?>? decoration;
|
||||
final ButtonState<TextStyle?>? textStyle;
|
||||
|
||||
final ButtonState<Decoration?>? selectedDecoration;
|
||||
final ButtonState<TextStyle?>? selectedTextStyle;
|
||||
|
||||
final double? spacing;
|
||||
|
||||
final ButtonState<MouseCursor>? cursor;
|
||||
|
||||
const ChipThemeData({
|
||||
this.decoration,
|
||||
this.spacing,
|
||||
this.selectedDecoration,
|
||||
this.selectedTextStyle,
|
||||
this.cursor,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
factory ChipThemeData.standard(ThemeData style) {
|
||||
Color normalColor(Set<ButtonStates> states) => style.brightness.isLight
|
||||
? states.isPressing
|
||||
? const Color(0xFFc1c1c1)
|
||||
: states.isFocused || states.isHovering
|
||||
? const Color(0xFFe1e1e1)
|
||||
: const Color(0xFFf1f1f1)
|
||||
: states.isPressing
|
||||
? const Color(0xFF292929)
|
||||
: states.isFocused || states.isHovering
|
||||
? const Color(0xFF383838)
|
||||
: const Color(0xFF212121);
|
||||
Color selectedColor(Set<ButtonStates> states) =>
|
||||
states.isFocused || states.isPressing || states.isHovering
|
||||
? style.accentColor.resolveFromBrightness(
|
||||
style.brightness,
|
||||
level: states.isPressing
|
||||
? 2
|
||||
: states.isFocused
|
||||
? 0
|
||||
: 1,
|
||||
)
|
||||
: style.accentColor;
|
||||
return ChipThemeData(
|
||||
spacing: _kChipSpacing,
|
||||
decoration: ButtonState.resolveWith((states) {
|
||||
return BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: normalColor(states),
|
||||
);
|
||||
}),
|
||||
textStyle: ButtonState.resolveWith((states) {
|
||||
return TextStyle(
|
||||
color: states.isDisabled
|
||||
? style.disabledColor
|
||||
: normalColor(states).basedOnLuminance(),
|
||||
);
|
||||
}),
|
||||
selectedDecoration: ButtonState.resolveWith((states) {
|
||||
return BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: selectedColor(states),
|
||||
);
|
||||
}),
|
||||
selectedTextStyle: ButtonState.resolveWith((states) {
|
||||
return TextStyle(color: selectedColor(states).basedOnLuminance());
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static ChipThemeData lerp(
|
||||
ChipThemeData? a,
|
||||
ChipThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return ChipThemeData(
|
||||
decoration:
|
||||
ButtonState.lerp(a?.decoration, b?.decoration, t, Decoration.lerp),
|
||||
selectedDecoration: ButtonState.lerp(
|
||||
a?.selectedDecoration, b?.selectedDecoration, t, Decoration.lerp),
|
||||
cursor: t < 0.5 ? a?.cursor : b?.cursor,
|
||||
textStyle:
|
||||
ButtonState.lerp(a?.textStyle, b?.textStyle, t, TextStyle.lerp),
|
||||
selectedTextStyle: ButtonState.lerp(
|
||||
a?.selectedTextStyle, b?.selectedTextStyle, t, TextStyle.lerp),
|
||||
spacing: lerpDouble(a?.spacing, b?.spacing, t),
|
||||
);
|
||||
}
|
||||
|
||||
ChipThemeData merge(ChipThemeData? style) {
|
||||
if (style == null) return this;
|
||||
return ChipThemeData(
|
||||
decoration: style.decoration ?? decoration,
|
||||
selectedDecoration: style.selectedDecoration ?? selectedDecoration,
|
||||
selectedTextStyle: style.selectedTextStyle ?? selectedTextStyle,
|
||||
cursor: style.cursor ?? cursor,
|
||||
textStyle: style.textStyle ?? textStyle,
|
||||
spacing: style.spacing ?? spacing,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<ButtonState<Decoration?>?>(
|
||||
'decoration', decoration));
|
||||
properties.add(DiagnosticsProperty<ButtonState<Decoration?>?>(
|
||||
'selectedDecoration', selectedDecoration));
|
||||
properties
|
||||
.add(DoubleProperty('spacing', spacing, defaultValue: _kChipSpacing));
|
||||
properties
|
||||
.add(DiagnosticsProperty<ButtonState<MouseCursor>?>('cursor', cursor));
|
||||
properties.add(
|
||||
DiagnosticsProperty<ButtonState<TextStyle?>?>('textStyle', textStyle));
|
||||
properties.add(DiagnosticsProperty<ButtonState<TextStyle?>?>(
|
||||
'selectedTextStyle', selectedTextStyle));
|
||||
}
|
||||
}
|
||||
233
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/dropdown_button.dart
vendored
Normal file
233
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/dropdown_button.dart
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
const double _kVerticalOffset = 20.0;
|
||||
const Widget _kDefaultDropdownButtonTrailing = Icon(
|
||||
FluentIcons.chevron_down,
|
||||
size: 10,
|
||||
);
|
||||
|
||||
typedef DropDownButtonBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
VoidCallback? onOpen,
|
||||
);
|
||||
|
||||
/// A `DropDownButton` is a button that shows a chevron as a visual indicator that
|
||||
/// it has an attached flyout that contains more options. It has the same
|
||||
/// behavior as a standard Button control with a flyout; only the appearance is
|
||||
/// different.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Flyout], a light dismiss container that can show arbitrary UI as its
|
||||
/// content. Used to back this button
|
||||
/// * [Combobox], a list of items that a user can select from
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/buttons#create-a-drop-down-button>
|
||||
class DropDownButton extends StatefulWidget {
|
||||
/// Creates a dropdown button.
|
||||
const DropDownButton({
|
||||
Key? key,
|
||||
this.buttonBuilder,
|
||||
required this.items,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.trailing,
|
||||
this.verticalOffset = _kVerticalOffset,
|
||||
this.closeAfterClick = true,
|
||||
this.disabled = false,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.buttonStyle,
|
||||
this.placement = FlyoutPlacement.center,
|
||||
this.menuShape,
|
||||
this.menuColor,
|
||||
}) : assert(items.length > 0, 'You must provide at least one item'),
|
||||
super(key: key);
|
||||
|
||||
/// A builder for the button. If null, a [Button] with [leading], [title] and
|
||||
/// [trailing] is used.
|
||||
///
|
||||
/// If [disabled] is true, [DropDownButtonBuilder.onOpen] will be null
|
||||
final DropDownButtonBuilder? buttonBuilder;
|
||||
|
||||
/// The content at the start of this widget.
|
||||
///
|
||||
/// Usually an [Icon]
|
||||
final Widget? leading;
|
||||
|
||||
/// Title show a content at the center of this widget.
|
||||
///
|
||||
/// Usually a [Text]
|
||||
final Widget? title;
|
||||
|
||||
/// Trailing show a content at the right of this widget.
|
||||
///
|
||||
/// If null, a chevron_down is displayed.
|
||||
final Widget? trailing;
|
||||
|
||||
/// The space between the button and the flyout.
|
||||
///
|
||||
/// 20.0 is used by default
|
||||
final double verticalOffset;
|
||||
|
||||
/// The items in the flyout. Must not be empty
|
||||
final List<MenuFlyoutItem> items;
|
||||
|
||||
/// Whether the flyout will be closed after an item is tapped.
|
||||
///
|
||||
/// Defaults to `true`
|
||||
final bool closeAfterClick;
|
||||
|
||||
/// If `true`, the button won't be clickable.
|
||||
final bool disabled;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// Customizes the button's appearance.
|
||||
@Deprecated('buttonStyle was deprecated in 3.11.1. Use buttonBuilder instead')
|
||||
final ButtonStyle? buttonStyle;
|
||||
|
||||
/// The placement of the flyout.
|
||||
///
|
||||
/// [FlyoutPlacement.center] is used by default
|
||||
final FlyoutPlacement placement;
|
||||
|
||||
/// The menu shape
|
||||
final ShapeBorder? menuShape;
|
||||
|
||||
/// The menu color. If null, [ThemeData.menuColor] is used
|
||||
final Color? menuColor;
|
||||
|
||||
@override
|
||||
State<DropDownButton> createState() => _DropDownButtonState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(IterableProperty<MenuFlyoutItemInterface>('items', items))
|
||||
..add(DoubleProperty(
|
||||
'verticalOffset',
|
||||
verticalOffset,
|
||||
defaultValue: _kVerticalOffset,
|
||||
))
|
||||
..add(FlagProperty(
|
||||
'close after click',
|
||||
value: closeAfterClick,
|
||||
defaultValue: false,
|
||||
ifFalse: 'do not close after click',
|
||||
))
|
||||
..add(EnumProperty<FlyoutPlacement>('placement', placement))
|
||||
..add(DiagnosticsProperty<ShapeBorder>('menu shape', menuShape))
|
||||
..add(ColorProperty('menu color', menuColor));
|
||||
}
|
||||
}
|
||||
|
||||
class _DropDownButtonState extends State<DropDownButton> {
|
||||
final flyoutController = FlyoutController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
flyoutController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
|
||||
final buttonChildren = <Widget>[
|
||||
if (widget.leading != null)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 8.0),
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(size: 20.0),
|
||||
child: widget.leading!,
|
||||
),
|
||||
),
|
||||
if (widget.title != null) widget.title!,
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 8.0),
|
||||
child: widget.trailing ?? _kDefaultDropdownButtonTrailing,
|
||||
),
|
||||
];
|
||||
|
||||
return Flyout(
|
||||
placement: widget.placement,
|
||||
position: FlyoutPosition.below,
|
||||
controller: flyoutController,
|
||||
verticalOffset: widget.verticalOffset,
|
||||
child: Builder(builder: (context) {
|
||||
return widget.buttonBuilder?.call(
|
||||
context,
|
||||
widget.disabled ? null : flyoutController.open,
|
||||
) ??
|
||||
Button(
|
||||
onPressed: widget.disabled ? null : flyoutController.open,
|
||||
autofocus: widget.autofocus,
|
||||
focusNode: widget.focusNode,
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
style: widget.buttonStyle,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: buttonChildren,
|
||||
),
|
||||
);
|
||||
}),
|
||||
content: (context) {
|
||||
return MenuFlyout(
|
||||
color: widget.menuColor,
|
||||
shape: widget.menuShape,
|
||||
items: widget.items.map((item) {
|
||||
if (widget.closeAfterClick) {
|
||||
return MenuFlyoutItem(
|
||||
onPressed: () {
|
||||
item.onPressed?.call();
|
||||
flyoutController.close();
|
||||
},
|
||||
onRightPressed: item.onRightPressed,
|
||||
key: item.key,
|
||||
leading: item.leading,
|
||||
text: item.text,
|
||||
trailing: item.trailing,
|
||||
);
|
||||
}
|
||||
return item;
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An item used by [DropDownButton].
|
||||
@Deprecated('DropDownButtonItem is deprecated. Use MenuFlyoutItem instead')
|
||||
class DropDownButtonItem extends MenuFlyoutItem {
|
||||
/// Creates a drop down button item
|
||||
DropDownButtonItem({
|
||||
Key? key,
|
||||
required VoidCallback? onTap,
|
||||
Widget? leading,
|
||||
Widget? title,
|
||||
Widget? trailing,
|
||||
}) : assert(
|
||||
leading != null || title != null || trailing != null,
|
||||
'You must provide at least one property: leading, title or trailing',
|
||||
),
|
||||
super(
|
||||
key: key,
|
||||
leading: leading,
|
||||
text: title ?? const SizedBox.shrink(),
|
||||
trailing: trailing,
|
||||
onPressed: onTap,
|
||||
);
|
||||
}
|
||||
303
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/pill_button_bar.dart
vendored
Normal file
303
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/pill_button_bar.dart
vendored
Normal file
@@ -0,0 +1,303 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
const double _kMinHeight = 28.0;
|
||||
const double _kMaxHeight = 46.0;
|
||||
|
||||
const double _kButtonsSpacing = 8.0;
|
||||
|
||||
const double _kMinButtonWidth = 56.0;
|
||||
const double _kMaxButtonHeight = _kMinHeight;
|
||||
|
||||
/// The item used by [PillButtonBar]
|
||||
class PillButtonBarItem {
|
||||
/// The text
|
||||
final Widget text;
|
||||
|
||||
/// Create a new pill button item
|
||||
const PillButtonBarItem({required this.text});
|
||||
}
|
||||
|
||||
/// Pill button bar is a horizontal scrollable list of pill-shaped
|
||||
/// text buttons in which only one button can be selected at a given
|
||||
/// time.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [PillButtonBarItem], the item used by pill button bar
|
||||
/// * [PillButtonBarTheme], used to style the pill button bar
|
||||
class PillButtonBar extends StatelessWidget {
|
||||
/// Creates a pill button bar.
|
||||
///
|
||||
/// [selected] must be in the range of 0 to [items.length]
|
||||
const PillButtonBar({
|
||||
Key? key,
|
||||
required this.items,
|
||||
required this.selected,
|
||||
this.onChanged,
|
||||
this.controller,
|
||||
}) : assert(items.length >= 2),
|
||||
assert(selected >= 0 && selected < items.length),
|
||||
super(key: key);
|
||||
|
||||
/// The items of the bar. There must be at least 2 items in the list
|
||||
final List<PillButtonBarItem> items;
|
||||
|
||||
/// The current selected item index. It must be in the range
|
||||
/// of 0 to [items.length]
|
||||
final int selected;
|
||||
|
||||
/// Called when the items are changed. If null, the bar is
|
||||
/// considered disabled.
|
||||
final ValueChanged<int>? onChanged;
|
||||
|
||||
/// The scroll controller used to control the current scroll
|
||||
/// position of the bar.
|
||||
final ScrollController? controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = PillButtonBarTheme.of(context);
|
||||
final visualDensity = FluentTheme.of(context).visualDensity;
|
||||
Widget bar = Container(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: _kMinHeight + visualDensity.vertical,
|
||||
maxHeight: _kMaxHeight + visualDensity.vertical,
|
||||
),
|
||||
color: theme.backgroundColor ?? FluentTheme.of(context).accentColor,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
controller: controller,
|
||||
semanticChildCount: items.length,
|
||||
children: List<Widget>.generate(items.length, (index) {
|
||||
final item = items[index];
|
||||
return _PillButtonBarItem(
|
||||
item: item,
|
||||
selected: selected == index,
|
||||
onPressed: onChanged == null ? null : () => onChanged!(index),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
return Align(alignment: AlignmentDirectional.topStart, child: bar);
|
||||
}
|
||||
}
|
||||
|
||||
class _PillButtonBarItem extends StatelessWidget {
|
||||
const _PillButtonBarItem({
|
||||
Key? key,
|
||||
required this.item,
|
||||
this.selected = false,
|
||||
this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
final PillButtonBarItem item;
|
||||
final bool selected;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = PillButtonBarTheme.of(context);
|
||||
final VisualDensity visualDensity = FluentTheme.of(context).visualDensity;
|
||||
return HoverButton(
|
||||
onPressed: onPressed,
|
||||
builder: (context, states) {
|
||||
final Color selectedColor =
|
||||
theme.selectedColor?.resolve(states) ?? Colors.transparent;
|
||||
final Color unselectedColor = theme.unselectedColor?.resolve(states) ??
|
||||
FluentTheme.of(context).accentColor.dark;
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? selectedColor : unselectedColor,
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
minWidth: _kMinButtonWidth + visualDensity.horizontal,
|
||||
maxHeight: _kMaxButtonHeight + visualDensity.vertical,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16.0 + visualDensity.horizontal,
|
||||
vertical: 3.0,
|
||||
),
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: _kButtonsSpacing + visualDensity.horizontal,
|
||||
vertical: _kButtonsSpacing + visualDensity.vertical,
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: (selected
|
||||
? theme.selectedTextStyle
|
||||
: theme.unselectedTextStyle) ??
|
||||
TextStyle(
|
||||
color: selected
|
||||
? selectedColor
|
||||
: unselectedColor.basedOnLuminance(),
|
||||
),
|
||||
child: item.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [PillButtonBar]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [PillButtonBar] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class PillButtonBarTheme extends InheritedTheme {
|
||||
/// Creates a button theme that controls the configurations for
|
||||
/// [PillButtonBar].
|
||||
const PillButtonBarTheme({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.data,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [PillButtonBar] widgets.
|
||||
final PillButtonBarThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [PillButtonBar]s should
|
||||
/// look like, and merges in the current button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required PillButtonBarThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return PillButtonBarTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The data from the closest instance of this class that encloses the given
|
||||
/// context.
|
||||
///
|
||||
/// Defaults to [ThemeData.pillButtonBarTheme]
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// PillButtonBarThemeData theme = PillButtonBarTheme.of(context);
|
||||
/// ```
|
||||
static PillButtonBarThemeData of(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return PillButtonBarThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
static PillButtonBarThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme =
|
||||
context.dependOnInheritedWidgetOfExactType<PillButtonBarTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).pillButtonBarTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return PillButtonBarTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(PillButtonBarTheme oldWidget) {
|
||||
return oldWidget.data != data;
|
||||
}
|
||||
}
|
||||
|
||||
/// See also:
|
||||
///
|
||||
/// * [PillButtonBar], the widget styled by this theme data
|
||||
/// * [PillButtonBarTheme], an inherited theme that required this
|
||||
/// theme data
|
||||
@immutable
|
||||
class PillButtonBarThemeData with Diagnosticable {
|
||||
final Color? backgroundColor;
|
||||
final ButtonState<Color?>? selectedColor;
|
||||
final ButtonState<Color?>? unselectedColor;
|
||||
final TextStyle? selectedTextStyle;
|
||||
final TextStyle? unselectedTextStyle;
|
||||
|
||||
const PillButtonBarThemeData({
|
||||
this.backgroundColor,
|
||||
this.selectedColor,
|
||||
this.unselectedColor,
|
||||
this.selectedTextStyle,
|
||||
this.unselectedTextStyle,
|
||||
});
|
||||
|
||||
factory PillButtonBarThemeData.standard(ThemeData style) {
|
||||
Color _applyOpacity(Color color, Set<ButtonStates> states) {
|
||||
return color.withOpacity(
|
||||
states.isPressing
|
||||
? 0.925
|
||||
: states.isFocused
|
||||
? 0.4
|
||||
: states.isHovering
|
||||
? 0.85
|
||||
: 1.0,
|
||||
);
|
||||
}
|
||||
|
||||
final isLight = style.brightness.isLight;
|
||||
final unselectedColor =
|
||||
isLight ? style.accentColor.dark : const Color(0xFF141414);
|
||||
|
||||
return PillButtonBarThemeData(
|
||||
backgroundColor: isLight ? style.accentColor : const Color(0xFF212121),
|
||||
selectedColor: ButtonState.resolveWith((states) {
|
||||
return _applyOpacity(
|
||||
isLight ? Colors.white : const Color(0xFF404040), states);
|
||||
}),
|
||||
unselectedColor: ButtonState.resolveWith((states) {
|
||||
return _applyOpacity(unselectedColor, states);
|
||||
}),
|
||||
selectedTextStyle:
|
||||
TextStyle(color: isLight ? Colors.black : Colors.white),
|
||||
unselectedTextStyle: TextStyle(
|
||||
color: isLight ? unselectedColor.basedOnLuminance() : Colors.white),
|
||||
);
|
||||
}
|
||||
|
||||
static PillButtonBarThemeData lerp(
|
||||
PillButtonBarThemeData? a,
|
||||
PillButtonBarThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return PillButtonBarThemeData(
|
||||
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
|
||||
selectedTextStyle:
|
||||
TextStyle.lerp(a?.selectedTextStyle, b?.selectedTextStyle, t),
|
||||
unselectedTextStyle:
|
||||
TextStyle.lerp(a?.unselectedTextStyle, b?.unselectedTextStyle, t),
|
||||
selectedColor:
|
||||
ButtonState.lerp(a?.selectedColor, b?.selectedColor, t, Color.lerp),
|
||||
unselectedColor: ButtonState.lerp(
|
||||
a?.unselectedColor, b?.unselectedColor, t, Color.lerp),
|
||||
);
|
||||
}
|
||||
|
||||
PillButtonBarThemeData merge(PillButtonBarThemeData? other) {
|
||||
if (other == null) return this;
|
||||
return PillButtonBarThemeData(
|
||||
backgroundColor: other.backgroundColor ?? backgroundColor,
|
||||
selectedColor: other.selectedColor ?? selectedColor,
|
||||
unselectedColor: other.unselectedColor ?? unselectedColor,
|
||||
selectedTextStyle: other.selectedTextStyle ?? selectedTextStyle,
|
||||
unselectedTextStyle: other.unselectedTextStyle ?? unselectedTextStyle,
|
||||
);
|
||||
}
|
||||
}
|
||||
290
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/radio_button.dart
vendored
Normal file
290
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/radio_button.dart
vendored
Normal file
@@ -0,0 +1,290 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// Radio buttons, also called option buttons, let users select
|
||||
/// one option from a collection of two or more mutually exclusive,
|
||||
/// but related, options. Radio buttons are always used in groups,
|
||||
/// and each option is represented by one radio button in the group.
|
||||
///
|
||||
/// In the default state, no radio button in a RadioButtons group is
|
||||
/// selected. That is, all radio buttons are cleared. However, once a
|
||||
/// user has selected a radio button, the user can't deselect the
|
||||
/// button to restore the group to its initial cleared state.
|
||||
///
|
||||
/// The singular behavior of a RadioButtons group distinguishes it
|
||||
/// from check boxes, which support multi-selection and deselection,
|
||||
/// or clearing.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Slider], which let the user lie within a range of values,
|
||||
/// (for example, 10, 20, 30, ... 100).
|
||||
/// * [Checkbox], which let the user select multiple options.
|
||||
/// * [ComboBox], which let the user select multiple options, when
|
||||
/// there's more than eight options.
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/radio-button>
|
||||
class RadioButton extends StatelessWidget {
|
||||
/// Creates a radio button.
|
||||
const RadioButton({
|
||||
Key? key,
|
||||
required this.checked,
|
||||
required this.onChanged,
|
||||
this.style,
|
||||
this.content,
|
||||
this.semanticLabel,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Whether this radio button is checked.
|
||||
final bool checked;
|
||||
|
||||
/// Called when the value of the radio button should change.
|
||||
///
|
||||
/// The radio button passes the new value to the callback but does
|
||||
/// not actually change state until the parent widget rebuilds the
|
||||
/// radio button with the new value.
|
||||
///
|
||||
/// If this callback is null, the radio button will be displayed as
|
||||
/// disabled and will not respond to input gestures.
|
||||
final ValueChanged<bool>? onChanged;
|
||||
|
||||
/// The style of the radio buttonbutton.
|
||||
///
|
||||
/// If non-null, this is merged with the closest [RadioButtonTheme].
|
||||
/// If null, the closest [RadioButtonTheme] is used.
|
||||
final RadioButtonThemeData? style;
|
||||
|
||||
/// The content of the radio button.
|
||||
///
|
||||
/// This, if non-null, is displayed at the right of the radio button,
|
||||
/// and is affected by user touch.
|
||||
///
|
||||
/// Usually a [Text] or [Icon] widget
|
||||
final Widget? content;
|
||||
|
||||
/// {@macro fluent_ui.controls.inputs.HoverButton.semanticLabel}
|
||||
final String? semanticLabel;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(FlagProperty('checked', value: checked, ifFalse: 'unchecked'))
|
||||
..add(FlagProperty('disabled',
|
||||
value: onChanged == null, ifFalse: 'enabled'))
|
||||
..add(ObjectFlagProperty.has('style', style))
|
||||
..add(
|
||||
FlagProperty('autofocus', value: autofocus, ifFalse: 'manual focus'))
|
||||
..add(StringProperty('semanticLabel', semanticLabel));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final style = RadioButtonTheme.of(context).merge(this.style);
|
||||
return HoverButton(
|
||||
autofocus: autofocus,
|
||||
focusNode: focusNode,
|
||||
onPressed: onChanged == null ? null : () => onChanged!(!checked),
|
||||
builder: (context, state) {
|
||||
final BoxDecoration decoration = (checked
|
||||
? style.checkedDecoration?.resolve(state)
|
||||
: style.uncheckedDecoration?.resolve(state)) ??
|
||||
const BoxDecoration(shape: BoxShape.circle);
|
||||
Widget child = AnimatedContainer(
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
height: 20,
|
||||
width: 20,
|
||||
decoration: decoration.copyWith(color: Colors.transparent),
|
||||
|
||||
/// We need two boxes here because flutter draws the color
|
||||
/// behind the border, and it results in an weird effect. This
|
||||
/// way, the inner color will only be rendered within the
|
||||
/// bounds of the border.
|
||||
child: AnimatedContainer(
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
decoration: BoxDecoration(
|
||||
color: decoration.color ?? Colors.transparent,
|
||||
shape: decoration.shape,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (content != null) {
|
||||
child = Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
child,
|
||||
const SizedBox(width: 6.0),
|
||||
content!,
|
||||
]);
|
||||
}
|
||||
return Semantics(
|
||||
label: semanticLabel,
|
||||
selected: checked,
|
||||
child: FocusBorder(focused: state.isFocused, child: child),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [RadioButton]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [RadioButton] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class RadioButtonTheme extends InheritedTheme {
|
||||
/// Creates a radio button theme that controls the configurations for
|
||||
/// [RadioButton].
|
||||
const RadioButtonTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [RadioButton] widgets.
|
||||
final RadioButtonThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [RadioButton]s should
|
||||
/// look like, and merges in the current radio button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required RadioButtonThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return RadioButtonTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static RadioButtonThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final RadioButtonTheme? theme =
|
||||
context.dependOnInheritedWidgetOfExactType<RadioButtonTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).radioButtonTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [RadioButtonTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.radioButtonTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// RadioButtonThemeData theme = RadioButtonTheme.of(context);
|
||||
/// ```
|
||||
static RadioButtonThemeData of(BuildContext context) {
|
||||
return RadioButtonThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return RadioButtonTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(RadioButtonTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class RadioButtonThemeData with Diagnosticable {
|
||||
final ButtonState<BoxDecoration?>? checkedDecoration;
|
||||
final ButtonState<BoxDecoration?>? uncheckedDecoration;
|
||||
|
||||
const RadioButtonThemeData({
|
||||
this.checkedDecoration,
|
||||
this.uncheckedDecoration,
|
||||
});
|
||||
|
||||
factory RadioButtonThemeData.standard(ThemeData style) {
|
||||
return RadioButtonThemeData(
|
||||
checkedDecoration: ButtonState.resolveWith((states) {
|
||||
return BoxDecoration(
|
||||
border: Border.all(
|
||||
color: !states.isDisabled
|
||||
? style.accentColor.light
|
||||
: style.brightness.isLight
|
||||
? const Color.fromRGBO(0, 0, 0, 0.2169)
|
||||
: const Color.fromRGBO(255, 255, 255, 0.1581),
|
||||
width: !states.isDisabled
|
||||
? states.isHovering && !states.isPressing
|
||||
? 3.4
|
||||
: 5.0
|
||||
: 4.0,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
color: !states.isDisabled
|
||||
? style.brightness.isLight
|
||||
? Colors.white
|
||||
: Colors.black
|
||||
: style.brightness.isLight
|
||||
? Colors.white
|
||||
: const Color.fromRGBO(255, 255, 255, 0.5302),
|
||||
);
|
||||
}),
|
||||
uncheckedDecoration: ButtonState.resolveWith((states) {
|
||||
final backgroundColor = style.inactiveBackgroundColor;
|
||||
return BoxDecoration(
|
||||
color: states.isPressing
|
||||
? backgroundColor
|
||||
: states.isHovering
|
||||
? backgroundColor.withOpacity(0.8)
|
||||
: backgroundColor.withOpacity(0.0),
|
||||
border: Border.all(
|
||||
width: states.isPressing ? 4.5 : 1,
|
||||
color: !states.isDisabled
|
||||
? states.isPressing
|
||||
? style.accentColor
|
||||
: style.borderInputColor
|
||||
: style.brightness.isLight
|
||||
? const Color.fromRGBO(0, 0, 0, 0.2169)
|
||||
: const Color.fromRGBO(255, 255, 255, 0.1581),
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static RadioButtonThemeData lerp(
|
||||
RadioButtonThemeData? a, RadioButtonThemeData? b, double t) {
|
||||
return RadioButtonThemeData(
|
||||
checkedDecoration: ButtonState.lerp(
|
||||
a?.checkedDecoration, b?.checkedDecoration, t, BoxDecoration.lerp),
|
||||
uncheckedDecoration: ButtonState.lerp(a?.uncheckedDecoration,
|
||||
b?.uncheckedDecoration, t, BoxDecoration.lerp),
|
||||
);
|
||||
}
|
||||
|
||||
RadioButtonThemeData merge(RadioButtonThemeData? style) {
|
||||
return RadioButtonThemeData(
|
||||
checkedDecoration: style?.checkedDecoration ?? checkedDecoration,
|
||||
uncheckedDecoration: style?.uncheckedDecoration ?? uncheckedDecoration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<ButtonState<BoxDecoration?>?>(
|
||||
'checkedDecoration', checkedDecoration));
|
||||
properties.add(DiagnosticsProperty<ButtonState<BoxDecoration?>?>(
|
||||
'uncheckedDecoration', uncheckedDecoration));
|
||||
}
|
||||
}
|
||||
381
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/rating.dart
vendored
Normal file
381
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/rating.dart
vendored
Normal file
@@ -0,0 +1,381 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const IconData kRatingBarIcon = FluentIcons.favorite_star_fill;
|
||||
|
||||
/// The rating bar allows users to view and set ratings that
|
||||
/// reflect degrees of satisfaction with content and services.
|
||||
/// Users can interact with the rating control with touch, pen,
|
||||
/// mouse, gamepad or keyboard. The follow guidance shows how to
|
||||
/// use the rating control's features to provide flexibility and
|
||||
/// customization.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
/// - [Slider]
|
||||
class RatingBar extends StatefulWidget {
|
||||
/// Creates a new rating bar.
|
||||
///
|
||||
/// [rating] must be greater than 0 and less than [amount]
|
||||
///
|
||||
/// [starSpacing] and [amount] must be greater than 0
|
||||
const RatingBar({
|
||||
Key? key,
|
||||
required this.rating,
|
||||
this.onChanged,
|
||||
this.amount = 5,
|
||||
this.animationDuration = Duration.zero,
|
||||
this.animationCurve,
|
||||
this.icon,
|
||||
this.iconSize,
|
||||
this.ratedIconColor,
|
||||
this.unratedIconColor,
|
||||
this.semanticLabel,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.starSpacing = 0,
|
||||
this.dragStartBehavior = DragStartBehavior.down,
|
||||
}) : assert(rating >= 0 && rating <= amount),
|
||||
assert(starSpacing >= 0),
|
||||
assert(amount > 0),
|
||||
super(key: key);
|
||||
|
||||
/// The amount of stars in the bar. The default amount is 5
|
||||
final int amount;
|
||||
|
||||
/// The current rating of the bar.
|
||||
/// It must be more or equal to 0 and less than [amount]
|
||||
final double rating;
|
||||
|
||||
/// Called when the [rating] is changed.
|
||||
/// If this is `null`, the RatingBar will not detect touch inputs
|
||||
final ValueChanged<double>? onChanged;
|
||||
|
||||
/// The duration of the animation
|
||||
final Duration animationDuration;
|
||||
|
||||
/// The curve of the animation. If `null`, uses [ThemeData.animationCurve]
|
||||
final Curve? animationCurve;
|
||||
|
||||
/// The icon used in the bar. If `null`, uses [kRatingBarIcon]
|
||||
final IconData? icon;
|
||||
|
||||
/// The size of the icon. If `null`, uses [IconThemeData.size]
|
||||
final double? iconSize;
|
||||
|
||||
/// The space between each icon
|
||||
final double starSpacing;
|
||||
|
||||
/// The color of the icons that are rated. If `null`, uses [ThemeData.accentColor]
|
||||
final Color? ratedIconColor;
|
||||
|
||||
/// The color of the icons that are not rated. If `null`, uses [ThemeData.disabled]
|
||||
final Color? unratedIconColor;
|
||||
|
||||
/// Semantic label for the bar
|
||||
///
|
||||
/// Announced in accessibility modes (e.g TalkBack/VoiceOver). This
|
||||
/// label does not show in the UI.
|
||||
///
|
||||
/// * [SemanticsProperties.label], which is set to [semanticLabel]
|
||||
/// in the underlying [Semantics] widget.
|
||||
final String? semanticLabel;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// Determines the way that drag start behavior is handled.
|
||||
final DragStartBehavior dragStartBehavior;
|
||||
|
||||
@override
|
||||
_RatingBarState createState() => _RatingBarState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IntProperty('amount', amount, defaultValue: 5));
|
||||
properties.add(DoubleProperty('rating', rating));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Duration>('animationDuration', animationDuration),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<Curve>('animationCurve', animationCurve),
|
||||
);
|
||||
properties.add(DoubleProperty('iconSize', iconSize));
|
||||
properties.add(IconDataProperty('icon', icon));
|
||||
properties.add(ColorProperty('ratedIconColor', ratedIconColor));
|
||||
properties.add(ColorProperty('unratedIconColor', unratedIconColor));
|
||||
properties.add(ObjectFlagProperty<FocusNode>.has('focusNode', focusNode));
|
||||
properties.add(FlagProperty(
|
||||
'autofocus',
|
||||
value: autofocus,
|
||||
ifFalse: 'manual focus',
|
||||
));
|
||||
properties.add(DoubleProperty('starSpacing', starSpacing, defaultValue: 0));
|
||||
properties.add(EnumProperty(
|
||||
'dragStartBehavior',
|
||||
dragStartBehavior,
|
||||
defaultValue: DragStartBehavior.down,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _RatingBarState extends State<RatingBar> {
|
||||
late FocusNode _focusNode;
|
||||
late Map<LogicalKeySet, Intent> _shortcutMap;
|
||||
late Map<Type, Action<Intent>> _actionMap;
|
||||
|
||||
bool _showFocusHighlight = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_shortcutMap = <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowUp): const _AdjustSliderIntent.up(),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowDown):
|
||||
const _AdjustSliderIntent.down(),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowLeft):
|
||||
const _AdjustSliderIntent.left(),
|
||||
LogicalKeySet(LogicalKeyboardKey.arrowRight):
|
||||
const _AdjustSliderIntent.right(),
|
||||
};
|
||||
_actionMap = <Type, Action<Intent>>{
|
||||
_AdjustSliderIntent: CallbackAction<_AdjustSliderIntent>(
|
||||
onInvoke: _actionHandler,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.focusNode == null) _focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _actionHandler(_AdjustSliderIntent intent) {
|
||||
final directionality = Directionality.of(context);
|
||||
void increase() {
|
||||
if (widget.rating == widget.amount) {
|
||||
return;
|
||||
}
|
||||
widget.onChanged?.call(
|
||||
(widget.rating + 1).clamp(0, widget.amount).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
void decrease() {
|
||||
if (widget.rating == 0) {
|
||||
return;
|
||||
}
|
||||
widget.onChanged?.call(
|
||||
(widget.rating - 1).clamp(0, widget.amount).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
switch (intent) {
|
||||
case _AdjustSliderIntent.right():
|
||||
switch (directionality) {
|
||||
case TextDirection.rtl:
|
||||
decrease();
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
increase();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case _AdjustSliderIntent.left():
|
||||
switch (directionality) {
|
||||
case TextDirection.rtl:
|
||||
increase();
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
decrease();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case _AdjustSliderIntent.up():
|
||||
increase();
|
||||
break;
|
||||
case _AdjustSliderIntent.down():
|
||||
decrease();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleUpdate(double x, double? size) {
|
||||
final iSize = (widget.iconSize ?? size ?? 24);
|
||||
final value = (x / iSize) - (widget.starSpacing / widget.amount);
|
||||
if (value <= widget.amount && !value.isNegative) {
|
||||
widget.onChanged?.call(value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final double size = widget.iconSize ?? 22;
|
||||
return Semantics(
|
||||
label: widget.semanticLabel,
|
||||
// It's only a slider if its value can be changed
|
||||
slider: widget.onChanged != null,
|
||||
maxValueLength: widget.amount,
|
||||
value: widget.rating.toStringAsFixed(2),
|
||||
focusable: true,
|
||||
focused: _focusNode.hasFocus,
|
||||
child: FocusableActionDetector(
|
||||
focusNode: _focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
enabled: widget.onChanged != null,
|
||||
actions: _actionMap,
|
||||
shortcuts: _shortcutMap,
|
||||
onShowFocusHighlight: (v) {
|
||||
setState(() => _showFocusHighlight = v);
|
||||
},
|
||||
child: GestureDetector(
|
||||
dragStartBehavior: widget.dragStartBehavior,
|
||||
onTapDown: (d) => _handleUpdate(d.localPosition.dx, size),
|
||||
onHorizontalDragStart: (d) => _handleUpdate(d.localPosition.dx, size),
|
||||
onHorizontalDragUpdate: (d) =>
|
||||
_handleUpdate(d.localPosition.dx, size),
|
||||
child: FocusBorder(
|
||||
focused: widget.onChanged != null && _showFocusHighlight,
|
||||
useStackApproach: true,
|
||||
child: TweenAnimationBuilder<double>(
|
||||
builder: (context, value, child) {
|
||||
double v = value + 1;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: () {
|
||||
final items = List.generate(widget.amount, (index) {
|
||||
double r = v - 1;
|
||||
v -= 1;
|
||||
if (r > 1) {
|
||||
r = 1;
|
||||
} else if (r < 0) {
|
||||
r = 0;
|
||||
}
|
||||
Widget icon = RatingIcon(
|
||||
rating: r,
|
||||
icon: widget.icon ?? kRatingBarIcon,
|
||||
ratedColor: widget.ratedIconColor,
|
||||
unratedColor: widget.unratedIconColor,
|
||||
size: widget.iconSize ?? size,
|
||||
);
|
||||
if (index != widget.amount - 1) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: widget.starSpacing),
|
||||
child: icon,
|
||||
);
|
||||
}
|
||||
return icon;
|
||||
});
|
||||
return items;
|
||||
}(),
|
||||
);
|
||||
},
|
||||
duration: widget.animationDuration,
|
||||
curve: widget.animationCurve ?? Curves.linear,
|
||||
tween: Tween<double>(begin: 0, end: widget.rating),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AdjustSliderIntent extends Intent {
|
||||
const _AdjustSliderIntent({required this.type});
|
||||
|
||||
const _AdjustSliderIntent.right() : type = _SliderAdjustmentType.right;
|
||||
|
||||
const _AdjustSliderIntent.left() : type = _SliderAdjustmentType.left;
|
||||
|
||||
const _AdjustSliderIntent.up() : type = _SliderAdjustmentType.up;
|
||||
|
||||
const _AdjustSliderIntent.down() : type = _SliderAdjustmentType.down;
|
||||
|
||||
final _SliderAdjustmentType type;
|
||||
}
|
||||
|
||||
enum _SliderAdjustmentType {
|
||||
right,
|
||||
left,
|
||||
up,
|
||||
down,
|
||||
}
|
||||
|
||||
class RatingIcon extends StatelessWidget {
|
||||
const RatingIcon({
|
||||
Key? key,
|
||||
required this.rating,
|
||||
this.ratedColor,
|
||||
this.unratedColor,
|
||||
this.icon = kRatingBarIcon,
|
||||
this.size,
|
||||
}) : assert(rating >= 0.0 && rating <= 1.0),
|
||||
super(key: key);
|
||||
|
||||
/// The rating of the icon. Must be more or equal to 0 and less or equal than 1.0
|
||||
final double rating;
|
||||
|
||||
/// The icon.
|
||||
final IconData icon;
|
||||
|
||||
/// The color used by the rated part. If `null`, uses [ThemeData.accentColor]
|
||||
final Color? ratedColor;
|
||||
|
||||
/// The color used by the unrated part. If `null`, uses [ThemeData.disabledColor]
|
||||
final Color? unratedColor;
|
||||
|
||||
/// The size of the icon
|
||||
final double? size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final style = FluentTheme.of(context);
|
||||
final icon = this.icon;
|
||||
final size = this.size;
|
||||
if (rating == 1.0) {
|
||||
return Icon(icon, color: ratedColor ?? style.accentColor, size: size);
|
||||
} else if (rating == 0.0) {
|
||||
return Icon(icon, color: unratedColor ?? style.disabledColor, size: size);
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Icon(icon, color: unratedColor ?? style.disabledColor, size: size),
|
||||
ClipRect(
|
||||
clipper: _StarClipper(rating),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: ratedColor ?? style.accentColor,
|
||||
size: size,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StarClipper extends CustomClipper<Rect> {
|
||||
final double value;
|
||||
|
||||
_StarClipper(this.value);
|
||||
|
||||
@override
|
||||
Rect getClip(Size size) {
|
||||
final rect = Rect.fromLTWH(0, 0, size.width * value, size.height);
|
||||
return rect;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(_StarClipper oldClipper) => oldClipper.value != value;
|
||||
}
|
||||
855
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/slider.dart
vendored
Normal file
855
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/slider.dart
vendored
Normal file
@@ -0,0 +1,855 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/material.dart' as m;
|
||||
|
||||
/// A slider is a control that lets the user select from a
|
||||
/// range of values by moving a thumb control along a track.
|
||||
///
|
||||
/// A slider is a good choice when you know that users think
|
||||
/// of the value as a relative quantity, not a numeric value.
|
||||
/// For example, users think about setting their audio volume
|
||||
/// to low or medium—not about setting the value to 2 or 5.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
/// - [RatingBar]
|
||||
class Slider extends StatefulWidget {
|
||||
const Slider({
|
||||
Key? key,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
this.onChangeStart,
|
||||
this.onChangeEnd,
|
||||
this.min = 0.0,
|
||||
this.max = 100.0,
|
||||
this.divisions,
|
||||
this.style,
|
||||
this.label,
|
||||
this.focusNode,
|
||||
this.vertical = false,
|
||||
this.autofocus = false,
|
||||
this.mouseCursor = MouseCursor.defer,
|
||||
}) : assert(value >= min && value <= max),
|
||||
assert(divisions == null || divisions > 0),
|
||||
super(key: key);
|
||||
|
||||
/// The currently selected value for this slider.
|
||||
///
|
||||
/// The slider's thumb is drawn at a position that corresponds to this value.
|
||||
final double value;
|
||||
|
||||
/// Called during a drag when the user is selecting a new value for the slider
|
||||
/// by dragging.
|
||||
///
|
||||
/// The slider passes the new value to the callback but does not actually
|
||||
/// change state until the parent widget rebuilds the slider with the new
|
||||
/// value.
|
||||
///
|
||||
/// If null, the slider will be displayed as disabled.
|
||||
///
|
||||
/// The callback provided to onChanged should update the state of the parent
|
||||
/// [StatefulWidget] using the [State.setState] method, so that the parent
|
||||
/// gets rebuilt; for example:
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// ```dart
|
||||
/// Slider(
|
||||
/// value: _duelCommandment.toDouble(),
|
||||
/// min: 1.0,
|
||||
/// max: 10.0,
|
||||
/// divisions: 10,
|
||||
/// label: '$_duelCommandment',
|
||||
/// onChanged: (double newValue) {
|
||||
/// setState(() {
|
||||
/// _duelCommandment = newValue.round();
|
||||
/// });
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onChangeStart] for a callback that is called when the user starts
|
||||
/// changing the value.
|
||||
/// * [onChangeEnd] for a callback that is called when the user stops
|
||||
/// changing the value.
|
||||
final ValueChanged<double>? onChanged;
|
||||
|
||||
/// Called when the user starts selecting a new value for the slider.
|
||||
///
|
||||
/// This callback shouldn't be used to update the slider [value] (use
|
||||
/// [onChanged] for that), but rather to be notified when the user has started
|
||||
/// selecting a new value by starting a drag or with a tap.
|
||||
///
|
||||
/// The value passed will be the last [value] that the slider had before the
|
||||
/// change began.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// ```dart
|
||||
/// Slider(
|
||||
/// value: _duelCommandment.toDouble(),
|
||||
/// min: 1.0,
|
||||
/// max: 10.0,
|
||||
/// divisions: 10,
|
||||
/// label: '$_duelCommandment',
|
||||
/// onChanged: (double newValue) {
|
||||
/// setState(() {
|
||||
/// _duelCommandment = newValue.round();
|
||||
/// });
|
||||
/// },
|
||||
/// onChangeStart: (double startValue) {
|
||||
/// print('Started change at $startValue');
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onChangeEnd] for a callback that is called when the value change is
|
||||
/// complete.
|
||||
final ValueChanged<double>? onChangeStart;
|
||||
|
||||
/// Called when the user is done selecting a new value for the slider.
|
||||
///
|
||||
/// This callback shouldn't be used to update the slider [value] (use
|
||||
/// [onChanged] for that), but rather to know when the user has completed
|
||||
/// selecting a new [value] by ending a drag or a click.
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// ```dart
|
||||
/// Slider(
|
||||
/// value: _duelCommandment.toDouble(),
|
||||
/// min: 1.0,
|
||||
/// max: 10.0,
|
||||
/// divisions: 10,
|
||||
/// label: '$_duelCommandment',
|
||||
/// onChanged: (double newValue) {
|
||||
/// setState(() {
|
||||
/// _duelCommandment = newValue.round();
|
||||
/// });
|
||||
/// },
|
||||
/// onChangeEnd: (double newValue) {
|
||||
/// print('Ended change on $newValue');
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [onChangeStart] for a callback that is called when a value change
|
||||
/// begins.
|
||||
final ValueChanged<double>? onChangeEnd;
|
||||
|
||||
/// The maximum value the user can select.
|
||||
///
|
||||
/// Defaults to 1.0. Must be greater than or equal to [min].
|
||||
///
|
||||
/// If the [max] is equal to the [min], then the slider is disabled.
|
||||
final double min;
|
||||
|
||||
/// The minimum value the user can select.
|
||||
///
|
||||
/// Defaults to 0.0. Must be less than or equal to [max].
|
||||
///
|
||||
/// If the [max] is equal to the [min], then the slider is disabled.
|
||||
final double max;
|
||||
|
||||
/// The number of discrete divisions.
|
||||
///
|
||||
/// Typically used with [label] to show the current discrete value.
|
||||
///
|
||||
/// If null, the slider is continuous.
|
||||
final int? divisions;
|
||||
|
||||
/// The style used in this slider. It's mescled with [ThemeData.sliderThemeData]
|
||||
final SliderThemeData? style;
|
||||
|
||||
/// A label to show above the slider, or at the left
|
||||
/// of the slider if [vertical] is `true` when the slider is active.
|
||||
final String? label;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// Whether the slider is vertical or not
|
||||
///
|
||||
/// Use a vertical slider if the slider represents a
|
||||
/// real-world value that is normally shown vertically
|
||||
/// (such as temperature).
|
||||
final bool vertical;
|
||||
|
||||
/// {@macro fluent_ui.controls.inputs.HoverButton.mouseCursor}
|
||||
final MouseCursor mouseCursor;
|
||||
|
||||
@override
|
||||
_SliderState createState() => _SliderState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DoubleProperty('value', value))
|
||||
..add(DoubleProperty('min', min))
|
||||
..add(DoubleProperty('max', max))
|
||||
..add(IntProperty('divisions', divisions))
|
||||
..add(StringProperty('label', label))
|
||||
..add(ObjectFlagProperty<FocusNode>.has('focusNode', focusNode))
|
||||
..add(DiagnosticsProperty<SliderThemeData>('style', style))
|
||||
..add(FlagProperty('vertical', value: vertical, ifFalse: 'horizontal'));
|
||||
}
|
||||
}
|
||||
|
||||
class _SliderState extends m.State<Slider> {
|
||||
bool _showFocusHighlight = false;
|
||||
|
||||
late FocusNode _focusNode;
|
||||
bool _sliding = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = widget.focusNode ?? FocusNode();
|
||||
_focusNode.addListener(_handleFocusChanged);
|
||||
}
|
||||
|
||||
void _handleFocusChanged() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.removeListener(_handleFocusChanged);
|
||||
// Only dispose the focus node manually created
|
||||
if (widget.focusNode == null) _focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final style = SliderTheme.of(context).merge(widget.style);
|
||||
final direction = Directionality.of(context);
|
||||
Widget child = HoverButton(
|
||||
onPressed: widget.onChanged == null ? null : () {},
|
||||
margin: style.margin ?? EdgeInsets.zero,
|
||||
cursor: widget.mouseCursor,
|
||||
builder: (context, states) => m.Material(
|
||||
type: m.MaterialType.transparency,
|
||||
child: TweenAnimationBuilder<double>(
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
tween: Tween<double>(
|
||||
begin: 1.0,
|
||||
end: states.isPressing || _sliding
|
||||
? 0.45
|
||||
: states.isHovering
|
||||
? 0.66
|
||||
: 0.5,
|
||||
),
|
||||
builder: (context, innerFactor, child) => m.SliderTheme(
|
||||
data: m.SliderThemeData(
|
||||
showValueIndicator: m.ShowValueIndicator.always,
|
||||
thumbColor: style.thumbColor ?? style.activeColor,
|
||||
overlayShape: const m.RoundSliderOverlayShape(overlayRadius: 0),
|
||||
thumbShape: SliderThumbShape(
|
||||
elevation: 0,
|
||||
pressedElevation: 0,
|
||||
useBall: style.useThumbBall ?? true,
|
||||
innerFactor: innerFactor,
|
||||
brightness: FluentTheme.of(context).brightness,
|
||||
),
|
||||
valueIndicatorShape: _RectangularSliderValueIndicatorShape(
|
||||
backgroundColor: style.labelBackgroundColor,
|
||||
vertical: widget.vertical,
|
||||
ltr: direction == TextDirection.ltr,
|
||||
),
|
||||
trackHeight: 1.75,
|
||||
trackShape: _CustomTrackShape(),
|
||||
disabledThumbColor: style.disabledThumbColor,
|
||||
disabledInactiveTrackColor: style.disabledInactiveColor,
|
||||
disabledActiveTrackColor: style.disabledActiveColor,
|
||||
),
|
||||
child: child!,
|
||||
),
|
||||
child: m.Slider(
|
||||
value: widget.value,
|
||||
max: widget.max,
|
||||
min: widget.min,
|
||||
onChanged: widget.onChanged,
|
||||
onChangeEnd: (v) {
|
||||
widget.onChangeEnd?.call(v);
|
||||
setState(() => _sliding = false);
|
||||
},
|
||||
onChangeStart: (v) {
|
||||
widget.onChangeStart?.call(v);
|
||||
setState(() => _sliding = true);
|
||||
},
|
||||
activeColor: style.activeColor,
|
||||
inactiveColor: style.inactiveColor,
|
||||
divisions: widget.divisions,
|
||||
label: widget.label,
|
||||
focusNode: _focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
mouseCursor: MouseCursor.defer,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
child = FocusableActionDetector(
|
||||
onShowFocusHighlight: (v) => setState(() => _showFocusHighlight = v),
|
||||
child: FocusBorder(
|
||||
focused: _showFocusHighlight && (_focusNode.hasPrimaryFocus),
|
||||
useStackApproach: true,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
if (widget.vertical) {
|
||||
return RotatedBox(
|
||||
quarterTurns: direction == TextDirection.ltr ? 3 : 5,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
/// This is used to remove the padding the Material Slider adds automatically
|
||||
class _CustomTrackShape extends m.RoundedRectSliderTrackShape {
|
||||
@override
|
||||
Rect getPreferredRect({
|
||||
required RenderBox parentBox,
|
||||
Offset offset = Offset.zero,
|
||||
required m.SliderThemeData sliderTheme,
|
||||
bool isEnabled = false,
|
||||
bool isDiscrete = false,
|
||||
}) {
|
||||
final double trackHeight = sliderTheme.trackHeight!;
|
||||
final double trackLeft = offset.dx;
|
||||
final double trackTop =
|
||||
offset.dy + (parentBox.size.height - trackHeight) / 2;
|
||||
final double trackWidth = parentBox.size.width;
|
||||
return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/// The default shape of a [Slider]'s thumb.
|
||||
///
|
||||
/// There is a shadow for the resting, pressed, hovered, and focused state.
|
||||
class SliderThumbShape extends m.SliderComponentShape {
|
||||
/// Create a fluent-styled slider thumb;
|
||||
const SliderThumbShape({
|
||||
this.enabledThumbRadius = 10.0,
|
||||
this.disabledThumbRadius,
|
||||
this.elevation = 1.0,
|
||||
this.pressedElevation = 6.0,
|
||||
this.useBall = true,
|
||||
this.innerFactor = 1.0,
|
||||
this.brightness = Brightness.light,
|
||||
});
|
||||
|
||||
final double innerFactor;
|
||||
|
||||
final Brightness brightness;
|
||||
|
||||
/// Whether to draw a ball instead of a line
|
||||
final bool useBall;
|
||||
|
||||
/// The preferred radius of the round thumb shape when the slider is enabled.
|
||||
///
|
||||
/// If it is not provided, then the material default of 10 is used.
|
||||
final double enabledThumbRadius;
|
||||
|
||||
/// The preferred radius of the round thumb shape when the slider is disabled.
|
||||
///
|
||||
/// If no disabledRadius is provided, then it is equal to the
|
||||
/// [enabledThumbRadius]
|
||||
final double? disabledThumbRadius;
|
||||
double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius;
|
||||
|
||||
/// The resting elevation adds shadow to the unpressed thumb.
|
||||
///
|
||||
/// The default is 1.
|
||||
///
|
||||
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
|
||||
/// example, a value of 12 will create a very large shadow.
|
||||
///
|
||||
final double elevation;
|
||||
|
||||
/// The pressed elevation adds shadow to the pressed thumb.
|
||||
///
|
||||
/// The default is 6.
|
||||
///
|
||||
/// Use 0 for no shadow. The higher the value, the larger the shadow. For
|
||||
/// example, a value of 12 will create a very large shadow.
|
||||
final double pressedElevation;
|
||||
|
||||
@override
|
||||
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
|
||||
return Size.fromRadius(
|
||||
isEnabled == true ? enabledThumbRadius : _disabledThumbRadius);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
required Animation<double> activationAnimation,
|
||||
required Animation<double> enableAnimation,
|
||||
required bool isDiscrete,
|
||||
required TextPainter labelPainter,
|
||||
required RenderBox parentBox,
|
||||
required m.SliderThemeData sliderTheme,
|
||||
required TextDirection textDirection,
|
||||
required double value,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
}) {
|
||||
assert(sliderTheme.disabledThumbColor != null);
|
||||
assert(sliderTheme.thumbColor != null);
|
||||
|
||||
final Canvas canvas = context.canvas;
|
||||
final Tween<double> radiusTween = Tween<double>(
|
||||
begin: _disabledThumbRadius,
|
||||
end: enabledThumbRadius,
|
||||
);
|
||||
final ColorTween colorTween = ColorTween(
|
||||
begin: sliderTheme.disabledThumbColor,
|
||||
end: sliderTheme.thumbColor,
|
||||
);
|
||||
|
||||
final Color color = colorTween.evaluate(enableAnimation)!;
|
||||
final double radius = radiusTween.evaluate(enableAnimation);
|
||||
|
||||
if (!useBall) {
|
||||
canvas.drawLine(
|
||||
center - const Offset(0, 6),
|
||||
center + const Offset(0, 6),
|
||||
Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeWidth = 8.0,
|
||||
);
|
||||
} else {
|
||||
final Tween<double> elevationTween = Tween<double>(
|
||||
begin: elevation,
|
||||
end: pressedElevation,
|
||||
);
|
||||
final double evaluatedElevation =
|
||||
elevationTween.evaluate(activationAnimation);
|
||||
final Path path = Path()
|
||||
..addArc(
|
||||
Rect.fromCenter(
|
||||
center: center, width: 2 * radius, height: 2 * radius),
|
||||
0,
|
||||
math.pi * 2);
|
||||
canvas.drawShadow(path, Colors.black, evaluatedElevation, true);
|
||||
canvas.drawCircle(
|
||||
center,
|
||||
radius,
|
||||
Paint()
|
||||
..color = brightness == Brightness.light
|
||||
? Colors.white
|
||||
: const Color(0xFF454545),
|
||||
);
|
||||
canvas.drawCircle(
|
||||
center,
|
||||
radius * innerFactor,
|
||||
Paint()..color = color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [Slider]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [Slider] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class SliderTheme extends InheritedTheme {
|
||||
/// Creates a slider theme that controls the configurations for
|
||||
/// [Slider].
|
||||
const SliderTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [Slider] widgets.
|
||||
final SliderThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [Slider]s should
|
||||
/// look like, and merges in the current slider theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required SliderThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return SliderTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static SliderThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme = context.dependOnInheritedWidgetOfExactType<SliderTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).sliderTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [SliderTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.sliderTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// SliderThemeData theme = SliderTheme.of(context);
|
||||
/// ```
|
||||
static SliderThemeData of(BuildContext context) {
|
||||
return SliderThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return SliderTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(SliderTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class SliderThemeData with Diagnosticable {
|
||||
final Color? thumbColor;
|
||||
final Color? disabledThumbColor;
|
||||
final Color? labelBackgroundColor;
|
||||
|
||||
final bool? useThumbBall;
|
||||
|
||||
final Color? activeColor;
|
||||
final Color? inactiveColor;
|
||||
|
||||
final Color? disabledActiveColor;
|
||||
final Color? disabledInactiveColor;
|
||||
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
const SliderThemeData({
|
||||
this.margin,
|
||||
this.thumbColor,
|
||||
this.disabledThumbColor,
|
||||
this.activeColor,
|
||||
this.disabledActiveColor,
|
||||
this.inactiveColor,
|
||||
this.disabledInactiveColor,
|
||||
this.labelBackgroundColor,
|
||||
this.useThumbBall,
|
||||
});
|
||||
|
||||
factory SliderThemeData.standard(ThemeData? style) {
|
||||
final def = SliderThemeData(
|
||||
thumbColor: style?.accentColor,
|
||||
activeColor: style?.accentColor,
|
||||
inactiveColor: style?.disabledColor.withOpacity(1),
|
||||
margin: EdgeInsets.zero,
|
||||
disabledActiveColor: style?.disabledColor.withOpacity(1),
|
||||
disabledThumbColor: style?.disabledColor.withOpacity(1),
|
||||
disabledInactiveColor: style?.disabledColor,
|
||||
useThumbBall: true,
|
||||
);
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
static SliderThemeData lerp(
|
||||
SliderThemeData? a, SliderThemeData? b, double t) {
|
||||
return SliderThemeData(
|
||||
margin: EdgeInsetsGeometry.lerp(a?.margin, b?.margin, t),
|
||||
thumbColor: Color.lerp(a?.thumbColor, b?.thumbColor, t),
|
||||
activeColor: Color.lerp(a?.activeColor, b?.activeColor, t),
|
||||
inactiveColor: Color.lerp(a?.inactiveColor, b?.inactiveColor, t),
|
||||
disabledActiveColor:
|
||||
Color.lerp(a?.disabledActiveColor, b?.disabledActiveColor, t),
|
||||
disabledInactiveColor:
|
||||
Color.lerp(a?.disabledInactiveColor, b?.disabledInactiveColor, t),
|
||||
disabledThumbColor:
|
||||
Color.lerp(a?.disabledThumbColor, b?.disabledThumbColor, t),
|
||||
labelBackgroundColor:
|
||||
Color.lerp(a?.labelBackgroundColor, b?.labelBackgroundColor, t),
|
||||
useThumbBall: t < 0.5 ? a?.useThumbBall : b?.useThumbBall,
|
||||
);
|
||||
}
|
||||
|
||||
SliderThemeData merge(SliderThemeData? style) {
|
||||
return SliderThemeData(
|
||||
margin: style?.margin ?? margin,
|
||||
thumbColor: style?.thumbColor ?? thumbColor,
|
||||
activeColor: style?.activeColor ?? activeColor,
|
||||
inactiveColor: style?.inactiveColor ?? inactiveColor,
|
||||
disabledActiveColor: style?.disabledActiveColor ?? disabledActiveColor,
|
||||
disabledInactiveColor:
|
||||
style?.disabledInactiveColor ?? disabledInactiveColor,
|
||||
disabledThumbColor: style?.disabledThumbColor ?? disabledThumbColor,
|
||||
labelBackgroundColor: style?.labelBackgroundColor ?? labelBackgroundColor,
|
||||
useThumbBall: style?.useThumbBall ?? useThumbBall,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry?>('margin', margin));
|
||||
properties.add(ColorProperty('thumbColor', thumbColor));
|
||||
properties.add(ColorProperty('activeColor', activeColor));
|
||||
properties.add(ColorProperty('inactiveColor', inactiveColor));
|
||||
properties.add(ColorProperty('disabledActiveColor', disabledActiveColor));
|
||||
properties
|
||||
.add(ColorProperty('disabledInactiveColor', disabledInactiveColor));
|
||||
properties.add(ColorProperty('disabledThumbColor', disabledThumbColor));
|
||||
properties.add(ColorProperty('labelBackgroundColor', labelBackgroundColor));
|
||||
}
|
||||
}
|
||||
|
||||
class _RectangularSliderValueIndicatorShape extends m.SliderComponentShape {
|
||||
final Color? backgroundColor;
|
||||
final bool vertical;
|
||||
final bool ltr;
|
||||
|
||||
/// Create a slider value indicator that resembles a rectangular tooltip.
|
||||
const _RectangularSliderValueIndicatorShape({
|
||||
this.backgroundColor,
|
||||
this.vertical = false,
|
||||
this.ltr = false,
|
||||
});
|
||||
|
||||
get _pathPainter => _RectangularSliderValueIndicatorPathPainter(
|
||||
vertical,
|
||||
ltr,
|
||||
);
|
||||
|
||||
@override
|
||||
Size getPreferredSize(
|
||||
bool isEnabled,
|
||||
bool isDiscrete, {
|
||||
TextPainter? labelPainter,
|
||||
double? textScaleFactor,
|
||||
}) {
|
||||
assert(labelPainter != null);
|
||||
assert(textScaleFactor != null && textScaleFactor >= 0);
|
||||
return _pathPainter.getPreferredSize(labelPainter!, textScaleFactor!);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(
|
||||
PaintingContext context,
|
||||
Offset center, {
|
||||
required Animation<double> activationAnimation,
|
||||
required Animation<double> enableAnimation,
|
||||
required bool isDiscrete,
|
||||
required TextPainter labelPainter,
|
||||
required RenderBox parentBox,
|
||||
required m.SliderThemeData sliderTheme,
|
||||
required TextDirection textDirection,
|
||||
required double value,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
}) {
|
||||
final Canvas canvas = context.canvas;
|
||||
final double scale = activationAnimation.value;
|
||||
_pathPainter.paint(
|
||||
parentBox: parentBox,
|
||||
canvas: canvas,
|
||||
center: center,
|
||||
scale: scale,
|
||||
labelPainter: labelPainter,
|
||||
textScaleFactor: textScaleFactor,
|
||||
sizeWithOverflow: sizeWithOverflow,
|
||||
backgroundPaintColor: backgroundColor ?? sliderTheme.valueIndicatorColor!,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RectangularSliderValueIndicatorPathPainter {
|
||||
final bool vertical;
|
||||
|
||||
/// Whether the current [Directionality] is [TextDirection.ltr]
|
||||
final bool ltr;
|
||||
|
||||
const _RectangularSliderValueIndicatorPathPainter([
|
||||
this.vertical = false,
|
||||
this.ltr = false,
|
||||
]);
|
||||
|
||||
static const double _triangleHeight = 8.0;
|
||||
static const double _labelPadding = 8.0;
|
||||
static const double _preferredHeight = 32.0;
|
||||
static const double _minLabelWidth = 16.0;
|
||||
static const double _bottomTipYOffset = 14.0;
|
||||
static const double _preferredHalfHeight = _preferredHeight / 2;
|
||||
static const double _upperRectRadius = 4;
|
||||
|
||||
Size getPreferredSize(TextPainter labelPainter, double textScaleFactor) {
|
||||
return Size(
|
||||
_upperRectangleWidth(labelPainter, 1, textScaleFactor),
|
||||
labelPainter.height + _labelPadding,
|
||||
);
|
||||
}
|
||||
|
||||
double getHorizontalShift({
|
||||
required RenderBox parentBox,
|
||||
required Offset center,
|
||||
required TextPainter labelPainter,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
required double scale,
|
||||
}) {
|
||||
assert(!sizeWithOverflow.isEmpty);
|
||||
|
||||
const double edgePadding = 8.0;
|
||||
final double rectangleWidth =
|
||||
_upperRectangleWidth(labelPainter, scale, textScaleFactor);
|
||||
|
||||
/// Value indicator draws on the Overlay and by using the global Offset
|
||||
/// we are making sure we use the bounds of the Overlay instead of the Slider.
|
||||
final Offset globalCenter = parentBox.localToGlobal(center);
|
||||
|
||||
// The rectangle must be shifted towards the center so that it minimizes the
|
||||
// chance of it rendering outside the bounds of the render box. If the shift
|
||||
// is negative, then the lobe is shifted from right to left, and if it is
|
||||
// positive, then the lobe is shifted from left to right.
|
||||
final double overflowLeft =
|
||||
math.max(0, rectangleWidth / 2 - globalCenter.dx + edgePadding);
|
||||
final double overflowRight = math.max(
|
||||
0,
|
||||
rectangleWidth / 2 -
|
||||
(sizeWithOverflow.width - globalCenter.dx - edgePadding));
|
||||
|
||||
if (rectangleWidth < sizeWithOverflow.width) {
|
||||
return overflowLeft - overflowRight;
|
||||
} else if (overflowLeft - overflowRight > 0) {
|
||||
return overflowLeft - (edgePadding * textScaleFactor);
|
||||
} else {
|
||||
return -overflowRight + (edgePadding * textScaleFactor);
|
||||
}
|
||||
}
|
||||
|
||||
double _upperRectangleWidth(
|
||||
TextPainter labelPainter,
|
||||
double scale,
|
||||
double textScaleFactor,
|
||||
) {
|
||||
final double unscaledWidth =
|
||||
math.max(_minLabelWidth * textScaleFactor, labelPainter.width) +
|
||||
_labelPadding;
|
||||
return unscaledWidth * scale;
|
||||
}
|
||||
|
||||
void paint({
|
||||
required RenderBox parentBox,
|
||||
required Canvas canvas,
|
||||
required Offset center,
|
||||
required double scale,
|
||||
required TextPainter labelPainter,
|
||||
required double textScaleFactor,
|
||||
required Size sizeWithOverflow,
|
||||
required Color backgroundPaintColor,
|
||||
Color? strokePaintColor,
|
||||
}) {
|
||||
if (scale == 0.0) {
|
||||
// Zero scale essentially means "do not draw anything", so it's safe to just return.
|
||||
return;
|
||||
}
|
||||
assert(!sizeWithOverflow.isEmpty);
|
||||
|
||||
final double rectangleWidth = _upperRectangleWidth(
|
||||
labelPainter,
|
||||
scale,
|
||||
textScaleFactor,
|
||||
);
|
||||
final double horizontalShift = getHorizontalShift(
|
||||
parentBox: parentBox,
|
||||
center: center,
|
||||
labelPainter: labelPainter,
|
||||
textScaleFactor: textScaleFactor,
|
||||
sizeWithOverflow: sizeWithOverflow,
|
||||
scale: scale,
|
||||
);
|
||||
|
||||
final double rectHeight = labelPainter.height + _labelPadding;
|
||||
final Rect upperRect = Rect.fromLTWH(
|
||||
-rectangleWidth / 2 + horizontalShift,
|
||||
-_triangleHeight - rectHeight,
|
||||
rectangleWidth,
|
||||
rectHeight,
|
||||
);
|
||||
|
||||
final Path trianglePath = Path()..close();
|
||||
final Paint fillPaint = Paint()..color = backgroundPaintColor;
|
||||
final RRect upperRRect = RRect.fromRectAndRadius(
|
||||
upperRect,
|
||||
const Radius.circular(_upperRectRadius),
|
||||
);
|
||||
trianglePath.addRRect(upperRRect);
|
||||
canvas.save();
|
||||
// Prepare the canvas for the base of the tooltip, which is relative to the
|
||||
// center of the thumb.
|
||||
final double verticalFactor = ltr ? 20.0 : 10.0;
|
||||
canvas.translate(
|
||||
center.dx +
|
||||
(vertical
|
||||
? ltr
|
||||
? -verticalFactor
|
||||
: verticalFactor * 2
|
||||
: 0),
|
||||
center.dy -
|
||||
_bottomTipYOffset +
|
||||
(vertical
|
||||
? ltr
|
||||
? -verticalFactor
|
||||
: -verticalFactor * 2
|
||||
: 0),
|
||||
);
|
||||
canvas.scale(scale, scale);
|
||||
// Rotate the label if it's vertical
|
||||
if (vertical) canvas.rotate((ltr ? 1 : -1) * math.pi / 2);
|
||||
if (strokePaintColor != null) {
|
||||
final Paint strokePaint = Paint()
|
||||
..color = strokePaintColor
|
||||
..strokeWidth = 1.0
|
||||
..style = PaintingStyle.stroke;
|
||||
canvas.drawPath(trianglePath, strokePaint);
|
||||
}
|
||||
canvas.drawPath(trianglePath, fillPaint);
|
||||
|
||||
// The label text is centered within the value indicator.
|
||||
final double bottomTipToUpperRectTranslateY =
|
||||
-_preferredHalfHeight / 2 - upperRect.height;
|
||||
canvas.translate(0, bottomTipToUpperRectTranslateY);
|
||||
final Offset boxCenter = Offset(horizontalShift, upperRect.height / 2);
|
||||
final Offset halfLabelPainterOffset =
|
||||
Offset(labelPainter.width / 2, labelPainter.height / 2);
|
||||
final Offset labelOffset = boxCenter - halfLabelPainterOffset;
|
||||
labelPainter.paint(canvas, labelOffset);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
217
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/split_button.dart
vendored
Normal file
217
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/split_button.dart
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// A Split Button has two parts that can be invoked separately.
|
||||
/// One part behaves like a standard button and invokes an immediate action.
|
||||
/// The other part invokes a flyout that contains additional options that the
|
||||
/// user can choose from.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
/// - [Button]
|
||||
/// - [IconButton]
|
||||
class SplitButtonBar extends StatelessWidget {
|
||||
/// Creates a button bar with space in between the buttons.
|
||||
///
|
||||
/// It provides a [ButtonThemeData] above each button to make them
|
||||
/// fell natural within the bar.
|
||||
const SplitButtonBar({
|
||||
Key? key,
|
||||
required this.buttons,
|
||||
this.style,
|
||||
}) : assert(buttons.length == 2, 'There must 2 buttons'),
|
||||
super(key: key);
|
||||
|
||||
/// The buttons in this button bar. Must be only two buttons
|
||||
///
|
||||
/// Usually a List of [Button]s
|
||||
final List<Widget> buttons;
|
||||
|
||||
/// The style applied to this button bar. If non-null, it's
|
||||
/// merged with [ThemeData.splitButtonThemeData]
|
||||
final SplitButtonThemeData? style;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(IntProperty('buttonsAmount', buttons.length))
|
||||
..add(DiagnosticsProperty<SplitButtonThemeData?>('style', style));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
final style = SplitButtonTheme.of(context).merge(this.style);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(buttons.length, (index) {
|
||||
final buttonStyle = index == buttons.length - 1
|
||||
? style.actionButtonStyle
|
||||
: style.primaryButtonStyle;
|
||||
final button = ButtonTheme.merge(
|
||||
data: ButtonThemeData.all(
|
||||
ButtonStyle(
|
||||
shape: ButtonState.all(
|
||||
RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: theme.disabledColor.withOpacity(0.75),
|
||||
width: 0.1,
|
||||
),
|
||||
borderRadius: BorderRadiusDirectional.horizontal(
|
||||
start: index == 0
|
||||
? style.borderRadius?.topLeft ?? Radius.zero
|
||||
: Radius.zero,
|
||||
end: index == buttons.length - 1
|
||||
? style.borderRadius?.topRight ?? Radius.zero
|
||||
: Radius.zero,
|
||||
),
|
||||
),
|
||||
),
|
||||
).merge(buttonStyle),
|
||||
),
|
||||
child: FocusTheme(
|
||||
data: const FocusThemeData(renderOutside: false),
|
||||
child: buttons[index],
|
||||
),
|
||||
);
|
||||
if (index == 0) return button;
|
||||
return Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: style.interval ?? 0),
|
||||
child: button,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SplitButtonTheme extends InheritedTheme {
|
||||
/// Creates a button theme that controls how descendant [SplitButtonBar]s should
|
||||
/// look like.
|
||||
const SplitButtonTheme({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.data,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final SplitButtonThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [SplitButtonBar]s should
|
||||
/// look like, and merges in the current button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required SplitButtonThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return SplitButtonTheme(
|
||||
key: key,
|
||||
data: _getInheritedSplitButtonThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The data from the closest instance of this class that encloses the given
|
||||
/// context.
|
||||
///
|
||||
/// Defaults to [ThemeData.splitButtonTheme]
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// SplitButtonThemeData theme = SplitButtonTheme.of(context);
|
||||
/// ```
|
||||
static SplitButtonThemeData of(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return SplitButtonThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedSplitButtonThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
static SplitButtonThemeData _getInheritedSplitButtonThemeData(
|
||||
BuildContext context) {
|
||||
final SplitButtonTheme? checkboxTheme =
|
||||
context.dependOnInheritedWidgetOfExactType<SplitButtonTheme>();
|
||||
return checkboxTheme?.data ?? FluentTheme.of(context).splitButtonTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return SplitButtonTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(SplitButtonTheme oldWidget) {
|
||||
return oldWidget.data != data;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class SplitButtonThemeData with Diagnosticable {
|
||||
final BorderRadius? borderRadius;
|
||||
final double? interval;
|
||||
|
||||
final ButtonStyle? primaryButtonStyle;
|
||||
final ButtonStyle? actionButtonStyle;
|
||||
|
||||
const SplitButtonThemeData({
|
||||
this.borderRadius,
|
||||
this.interval,
|
||||
this.primaryButtonStyle,
|
||||
this.actionButtonStyle,
|
||||
});
|
||||
|
||||
factory SplitButtonThemeData.standard(ThemeData style) {
|
||||
return SplitButtonThemeData(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
interval: 1,
|
||||
primaryButtonStyle: ButtonStyle(
|
||||
padding: ButtonState.all(EdgeInsets.zero),
|
||||
),
|
||||
actionButtonStyle: ButtonStyle(
|
||||
padding: ButtonState.all(const EdgeInsets.all(6)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static SplitButtonThemeData lerp(
|
||||
SplitButtonThemeData? a,
|
||||
SplitButtonThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return SplitButtonThemeData(
|
||||
borderRadius: BorderRadius.lerp(a?.borderRadius, b?.borderRadius, t),
|
||||
interval: lerpDouble(a?.interval, b?.interval, t),
|
||||
primaryButtonStyle:
|
||||
ButtonStyle.lerp(a?.primaryButtonStyle, b?.primaryButtonStyle, t),
|
||||
actionButtonStyle:
|
||||
ButtonStyle.lerp(a?.actionButtonStyle, b?.actionButtonStyle, t),
|
||||
);
|
||||
}
|
||||
|
||||
SplitButtonThemeData merge(SplitButtonThemeData? style) {
|
||||
return SplitButtonThemeData(
|
||||
borderRadius: style?.borderRadius ?? borderRadius,
|
||||
interval: style?.interval ?? interval,
|
||||
primaryButtonStyle: style?.primaryButtonStyle ?? primaryButtonStyle,
|
||||
actionButtonStyle: style?.actionButtonStyle ?? actionButtonStyle,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<BorderRadiusGeometry>(
|
||||
'borderRadius', borderRadius))
|
||||
..add(DoubleProperty('interval', interval))
|
||||
..add(DiagnosticsProperty('primaryButtonStyle', primaryButtonStyle))
|
||||
..add(DiagnosticsProperty('actionButtonStyle', actionButtonStyle));
|
||||
}
|
||||
}
|
||||
208
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/toggle_button.dart
vendored
Normal file
208
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/toggle_button.dart
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// A button that can be on or off.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Checkbox], which is used to select or deselect action items
|
||||
/// * [ToggleSwitch], which use used to turn things on and off
|
||||
class ToggleButton extends StatelessWidget {
|
||||
/// Creates a toggle button
|
||||
const ToggleButton({
|
||||
Key? key,
|
||||
required this.checked,
|
||||
required this.onChanged,
|
||||
this.child,
|
||||
this.style,
|
||||
this.semanticLabel,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The content of the button
|
||||
final Widget? child;
|
||||
|
||||
/// Whether this [ToggleButton] is checked
|
||||
final bool checked;
|
||||
|
||||
/// Whenever the value of this [ToggleButton] should change
|
||||
final ValueChanged<bool>? onChanged;
|
||||
|
||||
/// The style of the button.
|
||||
/// This style is merged with [ThemeData.toggleButtonThemeData]
|
||||
final ToggleButtonThemeData? style;
|
||||
|
||||
/// The semantics label of the button
|
||||
final String? semanticLabel;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(FlagProperty(
|
||||
'checked',
|
||||
value: checked,
|
||||
ifFalse: 'unchecked',
|
||||
));
|
||||
properties.add(
|
||||
ObjectFlagProperty('onChanged', onChanged, ifNull: 'disabled'),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<ToggleButtonThemeData>('style', style));
|
||||
properties.add(StringProperty('semanticLabel', semanticLabel));
|
||||
properties.add(ObjectFlagProperty<FocusNode>.has('focusNode', focusNode));
|
||||
properties.add(
|
||||
FlagProperty('autofocus', value: autofocus, ifFalse: 'manual focus'));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = ToggleButtonTheme.of(context).merge(style);
|
||||
return Button(
|
||||
autofocus: autofocus,
|
||||
focusNode: focusNode,
|
||||
onPressed: onChanged == null ? null : () => onChanged!(!checked),
|
||||
style: checked ? theme.checkedButtonStyle : theme.uncheckedButtonStyle,
|
||||
child: Semantics(selected: checked, child: child),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [ToggleButton]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [ToggleButton] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class ToggleButtonTheme extends InheritedTheme {
|
||||
/// Creates a toggle button theme that controls the configurations for
|
||||
/// [ToggleButton].
|
||||
const ToggleButtonTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [ToggleButton] widgets.
|
||||
final ToggleButtonThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [ToggleButton]s should
|
||||
/// look like, and merges in the current toggle button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required ToggleButtonThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return ToggleButtonTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static ToggleButtonThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme =
|
||||
context.dependOnInheritedWidgetOfExactType<ToggleButtonTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).toggleButtonTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [ToggleButtonTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.toggleButtonTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// ToggleButtonThemeData theme = ToggleButtonTheme.of(context);
|
||||
/// ```
|
||||
static ToggleButtonThemeData of(BuildContext context) {
|
||||
return ToggleButtonThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return ToggleButtonTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ToggleButtonTheme oldWidget) =>
|
||||
data != oldWidget.data;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ToggleButtonThemeData with Diagnosticable {
|
||||
final ButtonStyle? checkedButtonStyle;
|
||||
final ButtonStyle? uncheckedButtonStyle;
|
||||
|
||||
const ToggleButtonThemeData({
|
||||
this.checkedButtonStyle,
|
||||
this.uncheckedButtonStyle,
|
||||
});
|
||||
|
||||
factory ToggleButtonThemeData.standard(ThemeData theme) {
|
||||
return ToggleButtonThemeData(
|
||||
checkedButtonStyle: ButtonStyle(
|
||||
backgroundColor: ButtonState.resolveWith(
|
||||
(states) => FilledButton.backgroundColor(
|
||||
theme,
|
||||
states,
|
||||
),
|
||||
),
|
||||
shape: ButtonState.all(RoundedRectangleBorder(
|
||||
side: const BorderSide(
|
||||
color: Colors.transparent,
|
||||
width: 1,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
)),
|
||||
foregroundColor: ButtonState.resolveWith(
|
||||
(states) => FilledButton.backgroundColor(
|
||||
theme,
|
||||
states,
|
||||
).basedOnLuminance(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static ToggleButtonThemeData lerp(
|
||||
ToggleButtonThemeData? a,
|
||||
ToggleButtonThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return ToggleButtonThemeData(
|
||||
checkedButtonStyle:
|
||||
ButtonStyle.lerp(a?.checkedButtonStyle, b?.checkedButtonStyle, t),
|
||||
uncheckedButtonStyle:
|
||||
ButtonStyle.lerp(a?.uncheckedButtonStyle, b?.uncheckedButtonStyle, t),
|
||||
);
|
||||
}
|
||||
|
||||
ToggleButtonThemeData merge(ToggleButtonThemeData? other) {
|
||||
if (other == null) return this;
|
||||
return ToggleButtonThemeData(
|
||||
checkedButtonStyle: other.checkedButtonStyle ?? checkedButtonStyle,
|
||||
uncheckedButtonStyle: other.uncheckedButtonStyle ?? uncheckedButtonStyle,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<ButtonStyle>(
|
||||
'checkedButtonStyle', checkedButtonStyle))
|
||||
..add(DiagnosticsProperty<ButtonStyle>(
|
||||
'uncheckedButtonStyle', uncheckedButtonStyle));
|
||||
}
|
||||
}
|
||||
426
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/toggle_switch.dart
vendored
Normal file
426
dependencies/fluent_ui-3.12.0/lib/src/controls/inputs/toggle_switch.dart
vendored
Normal file
@@ -0,0 +1,426 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
/// The toggle switch represents a physical switch that allows users to
|
||||
/// turn things on or off, like a light switch. Use toggle switch controls
|
||||
/// to present users with two mutually exclusive options (such as on/off),
|
||||
/// where choosing an option provides immediate results.
|
||||
///
|
||||
/// Use a toggle switch for binary operations that take effect right after
|
||||
/// the user flips the toggle switch
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// Think of the toggle switch as a physical power switch for a device: you
|
||||
/// flip it on or off when you want to enable or disable the action performed
|
||||
/// by the device.
|
||||
///
|
||||
/// See also:
|
||||
/// - [Checkbox]
|
||||
/// - [RadioButton]
|
||||
/// - [ToggleButton]
|
||||
/// - [RadioButton]
|
||||
class ToggleSwitch extends StatefulWidget {
|
||||
/// Creates a toggle switch.
|
||||
const ToggleSwitch({
|
||||
Key? key,
|
||||
required this.checked,
|
||||
required this.onChanged,
|
||||
this.style,
|
||||
this.content,
|
||||
this.semanticLabel,
|
||||
this.thumb,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.onDisabledPress,
|
||||
this.enabled = true
|
||||
}) : super(key: key);
|
||||
|
||||
/// Whether the [ToggleSwitch] is checked
|
||||
final bool checked;
|
||||
|
||||
/// Called when the value of the [ToggleSwitch] should change.
|
||||
///
|
||||
/// This callback passes a new value, but doesn't update its state
|
||||
/// internally.
|
||||
///
|
||||
/// If this callback is null, the ToggleSwitch is disabled.
|
||||
final ValueChanged<bool>? onChanged;
|
||||
|
||||
/// The thumb of this [ToggleSwitch]. If this is null, defaults to [DefaultToggleSwitchThumb]
|
||||
final Widget? thumb;
|
||||
|
||||
/// The style of this [ToggleSwitch].
|
||||
///
|
||||
/// This style is mescled with [ThemeData.toggleSwitchThemeData]
|
||||
final ToggleSwitchThemeData? style;
|
||||
|
||||
/// The content of the radio button.
|
||||
///
|
||||
/// This, if non-null, is displayed at the right of the switcher,
|
||||
/// and is affected by user touch.
|
||||
///
|
||||
/// Usually a [Text] or [Icon] widget
|
||||
final Widget? content;
|
||||
|
||||
/// The `semanticLabel` of this [ToggleSwitch]
|
||||
final String? semanticLabel;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
final Function()? onDisabledPress;
|
||||
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(FlagProperty('checked', value: checked, ifFalse: 'unchecked'))
|
||||
..add(ObjectFlagProperty('onChanged', onChanged, ifNull: 'disabled'))
|
||||
..add(
|
||||
FlagProperty('autofocus', value: autofocus, ifFalse: 'manual focus'))
|
||||
..add(DiagnosticsProperty<ToggleSwitchThemeData>('style', style))
|
||||
..add(StringProperty('semanticLabel', semanticLabel))
|
||||
..add(ObjectFlagProperty<FocusNode>.has('focusNode', focusNode));
|
||||
}
|
||||
|
||||
@override
|
||||
_ToggleSwitchState createState() => _ToggleSwitchState();
|
||||
}
|
||||
|
||||
class _ToggleSwitchState extends State<ToggleSwitch> {
|
||||
bool _dragging = false;
|
||||
|
||||
Alignment? _alignment;
|
||||
|
||||
void _handleAlignmentChanged(
|
||||
Offset localPosition, double sliderGestureWidth) {
|
||||
setState(() {
|
||||
_alignment = Alignment(
|
||||
(localPosition.dx / sliderGestureWidth).clamp(-1, 1),
|
||||
0,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final ToggleSwitchThemeData style =
|
||||
ToggleSwitchTheme.of(context).merge(widget.style);
|
||||
final sliderGestureWidth = 45.0 + (style.padding?.horizontal ?? 0.0);
|
||||
return HoverButton(
|
||||
autofocus: widget.autofocus,
|
||||
semanticLabel: widget.semanticLabel,
|
||||
margin: style.margin,
|
||||
focusNode: widget.focusNode,
|
||||
onPressed: !widget.enabled ? () => widget.onDisabledPress?.call() : () => widget.onChanged!(!widget.checked),
|
||||
onHorizontalDragStart: (e) {
|
||||
if (!widget.enabled) {
|
||||
widget.onDisabledPress?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
_handleAlignmentChanged(e.localPosition, sliderGestureWidth);
|
||||
setState(() => _dragging = true);
|
||||
},
|
||||
onHorizontalDragUpdate: (e) {
|
||||
if (!widget.enabled) {
|
||||
widget.onDisabledPress?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
_handleAlignmentChanged(e.localPosition, sliderGestureWidth);
|
||||
if (!_dragging) setState(() => _dragging = true);
|
||||
},
|
||||
onHorizontalDragEnd: (e) {
|
||||
if (!widget.enabled) {
|
||||
widget.onDisabledPress?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_alignment != null) {
|
||||
if (_alignment!.x >= 0.5) {
|
||||
widget.onChanged!(true);
|
||||
} else {
|
||||
widget.onChanged!(false);
|
||||
}
|
||||
setState(() {
|
||||
_alignment = null;
|
||||
_dragging = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
builder: (context, states) {
|
||||
Widget child = AnimatedContainer(
|
||||
alignment: _alignment ??
|
||||
(widget.checked ? Alignment.centerRight : Alignment.centerLeft),
|
||||
height: 20,
|
||||
width: 40,
|
||||
duration: style.animationDuration ?? Duration.zero,
|
||||
curve: style.animationCurve ?? Curves.linear,
|
||||
padding: style.padding,
|
||||
decoration: widget.checked
|
||||
? style.checkedDecoration?.resolve(states)
|
||||
: style.uncheckedDecoration?.resolve(states),
|
||||
child: widget.thumb ??
|
||||
DefaultToggleSwitchThumb(
|
||||
checked: widget.checked,
|
||||
style: style,
|
||||
states: _dragging ? {ButtonStates.pressing} : states,
|
||||
),
|
||||
);
|
||||
if (widget.content != null) {
|
||||
child = Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
child,
|
||||
const SizedBox(width: 10.0),
|
||||
widget.content!,
|
||||
]);
|
||||
}
|
||||
return Semantics(
|
||||
checked: widget.checked,
|
||||
child: FocusBorder(
|
||||
focused: states.isFocused,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DefaultToggleSwitchThumb extends StatelessWidget {
|
||||
const DefaultToggleSwitchThumb({
|
||||
Key? key,
|
||||
required this.checked,
|
||||
required this.style,
|
||||
required this.states,
|
||||
}) : super(key: key);
|
||||
|
||||
final bool checked;
|
||||
final ToggleSwitchThemeData? style;
|
||||
final Set<ButtonStates> states;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const checkedFactor = 1;
|
||||
return AnimatedContainer(
|
||||
duration: style?.animationDuration ?? Duration.zero,
|
||||
curve: style?.animationCurve ?? Curves.linear,
|
||||
margin: states.isHovering
|
||||
? const EdgeInsets.all(1.0 + checkedFactor)
|
||||
: const EdgeInsets.symmetric(
|
||||
horizontal: 2.0 + checkedFactor,
|
||||
vertical: 2.0 + checkedFactor,
|
||||
),
|
||||
height: 18,
|
||||
width: 12 + (states.isHovering ? 2 : 0) + (states.isPressing ? 5 : 0),
|
||||
decoration: checked
|
||||
? style?.checkedThumbDecoration?.resolve(states)
|
||||
: style?.uncheckedThumbDecoration?.resolve(states),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ToggleSwitchTheme extends InheritedTheme {
|
||||
/// Creates a button theme that controls how descendant [ToggleSwitch]es should
|
||||
/// look like.
|
||||
const ToggleSwitchTheme({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.data,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final ToggleSwitchThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [ToggleSwitch]es should
|
||||
/// look like, and merges in the current button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required ToggleSwitchThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return ToggleSwitchTheme(
|
||||
key: key,
|
||||
data: _getInheritedToggleSwitchThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The data from the closest instance of this class that encloses the given
|
||||
/// context.
|
||||
///
|
||||
/// Defaults to [ThemeData.toggleSwitchTheme]
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// ToggleSwitchThemeData theme = ToggleSwitchTheme.of(context);
|
||||
/// ```
|
||||
static ToggleSwitchThemeData of(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return ToggleSwitchThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedToggleSwitchThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
static ToggleSwitchThemeData _getInheritedToggleSwitchThemeData(
|
||||
BuildContext context) {
|
||||
final ToggleSwitchTheme? checkboxTheme =
|
||||
context.dependOnInheritedWidgetOfExactType<ToggleSwitchTheme>();
|
||||
return checkboxTheme?.data ?? FluentTheme.of(context).toggleSwitchTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return ToggleSwitchTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ToggleSwitchTheme oldWidget) {
|
||||
return oldWidget.data != data;
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ToggleSwitchThemeData with Diagnosticable {
|
||||
final ButtonState<Decoration?>? checkedThumbDecoration;
|
||||
final ButtonState<Decoration?>? uncheckedThumbDecoration;
|
||||
|
||||
final ButtonState<Decoration?>? checkedDecoration;
|
||||
final ButtonState<Decoration?>? uncheckedDecoration;
|
||||
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
final Duration? animationDuration;
|
||||
final Curve? animationCurve;
|
||||
|
||||
const ToggleSwitchThemeData({
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.animationDuration,
|
||||
this.animationCurve,
|
||||
this.checkedThumbDecoration,
|
||||
this.uncheckedThumbDecoration,
|
||||
this.checkedDecoration,
|
||||
this.uncheckedDecoration,
|
||||
});
|
||||
|
||||
factory ToggleSwitchThemeData.standard(ThemeData style) {
|
||||
final defaultThumbDecoration = BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
);
|
||||
|
||||
final defaultDecoration = BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
);
|
||||
|
||||
return ToggleSwitchThemeData(
|
||||
checkedDecoration: ButtonState.resolveWith((states) {
|
||||
return defaultDecoration.copyWith(
|
||||
color: ButtonThemeData.checkedInputColor(style, states),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
);
|
||||
}),
|
||||
uncheckedDecoration: ButtonState.resolveWith((states) {
|
||||
return defaultDecoration.copyWith(
|
||||
color: ButtonThemeData.uncheckedInputColor(style, states),
|
||||
border: Border.all(
|
||||
width: 1,
|
||||
color: style.borderInputColor
|
||||
),
|
||||
);
|
||||
}),
|
||||
margin: const EdgeInsets.all(4),
|
||||
animationDuration: style.fastAnimationDuration,
|
||||
animationCurve: style.animationCurve,
|
||||
checkedThumbDecoration: ButtonState.resolveWith((states) {
|
||||
return defaultThumbDecoration.copyWith(
|
||||
color: style.checkedColor
|
||||
);
|
||||
}),
|
||||
uncheckedThumbDecoration: ButtonState.resolveWith((states) {
|
||||
return defaultThumbDecoration.copyWith(
|
||||
color: style.uncheckedColor
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static ToggleSwitchThemeData lerp(
|
||||
ToggleSwitchThemeData? a,
|
||||
ToggleSwitchThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return ToggleSwitchThemeData(
|
||||
margin: EdgeInsetsGeometry.lerp(a?.margin, b?.margin, t),
|
||||
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
|
||||
animationCurve: t < 0.5 ? a?.animationCurve : b?.animationCurve,
|
||||
animationDuration: lerpDuration(a?.animationDuration ?? Duration.zero,
|
||||
b?.animationDuration ?? Duration.zero, t),
|
||||
checkedThumbDecoration: ButtonState.lerp(a?.checkedThumbDecoration,
|
||||
b?.checkedThumbDecoration, t, Decoration.lerp),
|
||||
uncheckedThumbDecoration: ButtonState.lerp(a?.uncheckedThumbDecoration,
|
||||
b?.uncheckedThumbDecoration, t, Decoration.lerp),
|
||||
checkedDecoration: ButtonState.lerp(
|
||||
a?.checkedDecoration, b?.checkedDecoration, t, Decoration.lerp),
|
||||
uncheckedDecoration: ButtonState.lerp(
|
||||
a?.uncheckedDecoration, b?.uncheckedDecoration, t, Decoration.lerp),
|
||||
);
|
||||
}
|
||||
|
||||
ToggleSwitchThemeData merge(ToggleSwitchThemeData? style) {
|
||||
return ToggleSwitchThemeData(
|
||||
margin: style?.margin ?? margin,
|
||||
padding: style?.padding ?? padding,
|
||||
animationCurve: style?.animationCurve ?? animationCurve,
|
||||
animationDuration: style?.animationDuration ?? animationDuration,
|
||||
checkedThumbDecoration:
|
||||
style?.checkedThumbDecoration ?? checkedThumbDecoration,
|
||||
uncheckedThumbDecoration:
|
||||
style?.uncheckedThumbDecoration ?? uncheckedThumbDecoration,
|
||||
checkedDecoration: style?.checkedDecoration ?? checkedDecoration,
|
||||
uncheckedDecoration: style?.uncheckedDecoration ?? uncheckedDecoration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry?>('margin', margin));
|
||||
properties
|
||||
.add(DiagnosticsProperty<EdgeInsetsGeometry?>('padding', padding));
|
||||
properties
|
||||
.add(DiagnosticsProperty<Curve?>('animationCurve', animationCurve));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Duration?>('animationDuration', animationDuration));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Decoration?>?>.has(
|
||||
'checkedDecoration',
|
||||
checkedDecoration,
|
||||
));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Decoration?>?>.has(
|
||||
'uncheckedDecoration',
|
||||
uncheckedDecoration,
|
||||
));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Decoration?>?>.has(
|
||||
'checkedThumbDecoration',
|
||||
checkedThumbDecoration,
|
||||
));
|
||||
properties.add(ObjectFlagProperty<ButtonState<Decoration?>?>.has(
|
||||
'uncheckedThumbDecoration',
|
||||
uncheckedThumbDecoration,
|
||||
));
|
||||
}
|
||||
}
|
||||
294
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/bottom_navigation.dart
vendored
Normal file
294
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/bottom_navigation.dart
vendored
Normal file
@@ -0,0 +1,294 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
const double _kBottomNavigationHeight = 48.0;
|
||||
|
||||
/// The navigation item used by [BottomNavigation]
|
||||
class BottomNavigationItem {
|
||||
/// The label of the item. If not provided, only [icon] is shown
|
||||
///
|
||||
/// Usually a [Text] widget
|
||||
final Widget? title;
|
||||
|
||||
/// The icon that represents this item.
|
||||
///
|
||||
/// Usually an [Icon] or an [AnimatedIcon]
|
||||
final Widget icon;
|
||||
|
||||
/// The icon that represents this item when selected. If null, [icon]
|
||||
/// is displayed.
|
||||
///
|
||||
/// Usually an [Icon]
|
||||
final Widget? selectedIcon;
|
||||
|
||||
/// Creates a new bottom navigation item.
|
||||
const BottomNavigationItem({
|
||||
required this.icon,
|
||||
this.selectedIcon,
|
||||
this.title,
|
||||
});
|
||||
}
|
||||
|
||||
/// The bottom navigation displays icons and optional text at the
|
||||
/// bottom of the screen for switching between different primary
|
||||
/// destinations in an app.
|
||||
///
|
||||
/// It's usually used on [ScaffoldPage.bottomBar]
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
/// * [BottomNavigationItem], the items used by this widget
|
||||
/// * [BottomNavigationThemeData], used to style this widget
|
||||
/// * [ScaffoldPage], used to layout pages
|
||||
class BottomNavigation extends StatelessWidget {
|
||||
/// Creates a bottom navigation
|
||||
///
|
||||
/// [items] must have at least 2 items
|
||||
///
|
||||
/// [index] must be in the range of 0 to [items.length]
|
||||
const BottomNavigation({
|
||||
Key? key,
|
||||
required this.items,
|
||||
required this.index,
|
||||
this.onChanged,
|
||||
this.style,
|
||||
}) : assert(items.length >= 2),
|
||||
assert(index >= 0 && index < items.length),
|
||||
super(key: key);
|
||||
|
||||
/// The items displayed by this widget. There must be at least 2
|
||||
/// items in the list.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [BottomNavigationItem], the items used on this bottom navigation
|
||||
final List<BottomNavigationItem> items;
|
||||
|
||||
/// The current selected index. This must be in the range of 0 to [items.length]
|
||||
final int index;
|
||||
|
||||
/// Called when the current index should be changed. If null, the bottom
|
||||
/// navigation items are considered disabled.
|
||||
///
|
||||
/// {@toolSnippet}
|
||||
/// ```dart
|
||||
///
|
||||
/// int index = 0;
|
||||
///
|
||||
/// BottomNavigation(
|
||||
/// index: index,
|
||||
/// onChanged: (i) => setState(() => index = i),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
final ValueChanged<int>? onChanged;
|
||||
|
||||
/// Used to style this bottom navigation bar. If non-null,
|
||||
/// it's mescled with [ThemeData.bottomNavigationTheme]
|
||||
final BottomNavigationThemeData? style;
|
||||
|
||||
bool get _disabled => onChanged == null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final style = BottomNavigationTheme.of(context).merge(this.style);
|
||||
return PhysicalModel(
|
||||
color: Colors.black,
|
||||
elevation: 8.0,
|
||||
shadowColor: FluentTheme.of(context).shadowColor,
|
||||
child: Container(
|
||||
height: _kBottomNavigationHeight,
|
||||
color: style.backgroundColor,
|
||||
child: Row(
|
||||
children: items.map((item) {
|
||||
final itemIndex = items.indexOf(item);
|
||||
return _BottomNavigationItem(
|
||||
key: ValueKey<BottomNavigationItem>(item),
|
||||
item: item,
|
||||
style: style,
|
||||
selected: index == itemIndex,
|
||||
onPressed: _disabled ? null : () => onChanged!(itemIndex),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomNavigationItem extends StatelessWidget {
|
||||
const _BottomNavigationItem({
|
||||
Key? key,
|
||||
required this.item,
|
||||
required this.selected,
|
||||
required this.style,
|
||||
this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
final BottomNavigationItem item;
|
||||
final bool selected;
|
||||
final VoidCallback? onPressed;
|
||||
final BottomNavigationThemeData style;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: HoverButton(
|
||||
onPressed: onPressed,
|
||||
builder: (context, state) {
|
||||
final content =
|
||||
Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
IconTheme.merge(
|
||||
data: IconThemeData(
|
||||
color: selected ? style.selectedColor : style.inactiveColor,
|
||||
),
|
||||
child: selected ? item.selectedIcon ?? item.icon : item.icon,
|
||||
),
|
||||
if (item.title != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 1.0),
|
||||
child: DefaultTextStyle(
|
||||
style: FluentTheme.of(context).typography.caption!.copyWith(
|
||||
color: selected
|
||||
? style.selectedColor
|
||||
: style.inactiveColor,
|
||||
),
|
||||
child: item.title!,
|
||||
),
|
||||
),
|
||||
]);
|
||||
return FocusBorder(
|
||||
focused: state.isFocused,
|
||||
renderOutside: false,
|
||||
child: Container(
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
state,
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [BottomNavigation]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [BottomNavigation] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class BottomNavigationTheme extends InheritedTheme {
|
||||
/// Creates a button theme that controls the configurations for
|
||||
/// [BottomNavigation].
|
||||
const BottomNavigationTheme({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.data,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [BottomNavigation] widgets.
|
||||
final BottomNavigationThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [BottomNavigation]s should
|
||||
/// look like, and merges in the current button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required BottomNavigationThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return BottomNavigationTheme(
|
||||
key: key,
|
||||
data: _getInheritedBottomNavigationThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The data from the closest instance of this class that encloses the given
|
||||
/// context.
|
||||
///
|
||||
/// Defaults to [ThemeData.bottomNavigationTheme]
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// BottomNavigationThemeData theme = BottomNavigationTheme.of(context);
|
||||
/// ```
|
||||
static BottomNavigationThemeData of(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return BottomNavigationThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedBottomNavigationThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
static BottomNavigationThemeData _getInheritedBottomNavigationThemeData(
|
||||
BuildContext context) {
|
||||
final theme =
|
||||
context.dependOnInheritedWidgetOfExactType<BottomNavigationTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).bottomNavigationTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return BottomNavigationTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(BottomNavigationTheme oldWidget) {
|
||||
return oldWidget.data != data;
|
||||
}
|
||||
}
|
||||
|
||||
/// See also:
|
||||
///
|
||||
/// * [BottomNavigation], the widget styled by this theme data
|
||||
/// * [BottomNavigationTheme], an inherited theme that required this
|
||||
/// theme data
|
||||
@immutable
|
||||
class BottomNavigationThemeData with Diagnosticable {
|
||||
final Color? backgroundColor;
|
||||
final Color? selectedColor;
|
||||
final Color? inactiveColor;
|
||||
|
||||
const BottomNavigationThemeData({
|
||||
this.backgroundColor,
|
||||
this.selectedColor,
|
||||
this.inactiveColor,
|
||||
});
|
||||
|
||||
factory BottomNavigationThemeData.standard(ThemeData style) {
|
||||
final isLight = style.brightness.isLight;
|
||||
return BottomNavigationThemeData(
|
||||
backgroundColor:
|
||||
isLight ? const Color(0xFFf8f8f8) : const Color(0xFF0c0c0c),
|
||||
selectedColor: style.accentColor,
|
||||
inactiveColor: style.disabledColor,
|
||||
);
|
||||
}
|
||||
|
||||
static BottomNavigationThemeData lerp(
|
||||
BottomNavigationThemeData? a,
|
||||
BottomNavigationThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return BottomNavigationThemeData(
|
||||
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
|
||||
selectedColor: Color.lerp(a?.selectedColor, b?.selectedColor, t),
|
||||
inactiveColor: Color.lerp(a?.inactiveColor, b?.inactiveColor, t),
|
||||
);
|
||||
}
|
||||
|
||||
BottomNavigationThemeData merge(BottomNavigationThemeData? other) {
|
||||
if (other == null) return this;
|
||||
return BottomNavigationThemeData(
|
||||
backgroundColor: other.backgroundColor ?? backgroundColor,
|
||||
selectedColor: other.selectedColor ?? selectedColor,
|
||||
inactiveColor: other.inactiveColor ?? inactiveColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
268
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/body.dart
vendored
Normal file
268
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/body.dart
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
// ignore_for_file: prefer_initializing_formals
|
||||
part of 'view.dart';
|
||||
|
||||
/// A helper widget that implements fluent page transitions into
|
||||
/// [NavigationView].
|
||||
///
|
||||
/// See also:
|
||||
/// * [NavigationView], used alongside this to navigate through pages
|
||||
/// * [NavigationAppBar], the app top bar
|
||||
class NavigationBody extends StatefulWidget {
|
||||
/// Creates a navigation body.
|
||||
///
|
||||
/// [index] must be greater than 0 and less than [children.length]
|
||||
const NavigationBody({
|
||||
Key? key,
|
||||
required this.index,
|
||||
required List<Widget> children,
|
||||
this.transitionBuilder,
|
||||
this.animationCurve,
|
||||
this.animationDuration,
|
||||
}) : assert(index >= 0 && index <= children.length),
|
||||
children = children,
|
||||
itemBuilder = null,
|
||||
itemCount = null,
|
||||
super(key: key);
|
||||
|
||||
/// Creates a navigation body that uses a builder to supply child pages
|
||||
///
|
||||
/// [index] must be greater than 0 and less than [itemCount] if it is provided
|
||||
const NavigationBody.builder({
|
||||
Key? key,
|
||||
required this.index,
|
||||
required IndexedWidgetBuilder itemBuilder,
|
||||
this.itemCount,
|
||||
this.transitionBuilder,
|
||||
this.animationCurve,
|
||||
this.animationDuration,
|
||||
}) : assert(index >= 0 && (itemCount == null || index <= itemCount)),
|
||||
itemBuilder = itemBuilder,
|
||||
children = null,
|
||||
super(key: key);
|
||||
|
||||
/// The pages this body can have
|
||||
final List<Widget>? children;
|
||||
|
||||
/// The builder that will be used to build the pages
|
||||
final IndexedWidgetBuilder? itemBuilder;
|
||||
|
||||
/// Optional number of items to assume builder can create.
|
||||
final int? itemCount;
|
||||
|
||||
/// The current page index.
|
||||
final int index;
|
||||
|
||||
/// The transition builder.
|
||||
///
|
||||
/// It can be detect the display mode of the parent [NavigationView], if any,
|
||||
/// and change the transition accordingly. By default, if the display mode is
|
||||
/// top, [EntrancePageTransition] is used, otherwise [DrillInPageTransition]
|
||||
/// is used.
|
||||
///
|
||||
/// ```dart
|
||||
/// NavigationBody(
|
||||
/// transitionBuilder: (child, animation) {
|
||||
/// return DrillInPageTransition(child: child, animation: animation);
|
||||
/// },
|
||||
/// ),
|
||||
/// ```
|
||||
final AnimatedSwitcherTransitionBuilder? transitionBuilder;
|
||||
|
||||
/// The curve used by the transition. [NavigationPaneThemeData.animationCurve]
|
||||
/// is used by default.
|
||||
///
|
||||
/// See also:
|
||||
/// * [Curves], a collection of common animation easing curves.
|
||||
final Curve? animationCurve;
|
||||
|
||||
/// The duration of the transition. [NavigationPaneThemeData.animationDuration]
|
||||
/// is used by default.
|
||||
///
|
||||
/// See also:
|
||||
/// * [ThemeData.fastAnimationDuration], the duration used by default.
|
||||
final Duration? animationDuration;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IntProperty('index', index));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Curve>('animationCurve', animationCurve),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<Duration>('animationDuration', animationDuration),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
_NavigationBodyState createState() => _NavigationBodyState();
|
||||
}
|
||||
|
||||
class _NavigationBodyState extends State<NavigationBody> {
|
||||
late int previousIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
previousIndex = widget.index;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(NavigationBody oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
previousIndex = oldWidget.index;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final body = InheritedNavigationView.maybeOf(context);
|
||||
final theme = FluentTheme.of(context);
|
||||
final NavigationPaneThemeData paneTheme = NavigationPaneTheme.of(context);
|
||||
return Container(
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12.0),
|
||||
child: AnimatedSwitcher(
|
||||
switchInCurve:
|
||||
widget.animationCurve ?? paneTheme.animationCurve ?? Curves.linear,
|
||||
duration: widget.animationDuration ??
|
||||
paneTheme.animationDuration ??
|
||||
Duration.zero,
|
||||
layoutBuilder: (child, children) {
|
||||
return SizedBox(child: child);
|
||||
},
|
||||
transitionBuilder: (child, animation) {
|
||||
if (widget.transitionBuilder != null) {
|
||||
return widget.transitionBuilder!(child, animation);
|
||||
}
|
||||
bool useDrillTransition = true;
|
||||
if (body != null && body.displayMode != null) {
|
||||
if (body.displayMode! == PaneDisplayMode.top) {
|
||||
useDrillTransition = false;
|
||||
}
|
||||
}
|
||||
if (useDrillTransition) {
|
||||
return DrillInPageTransition(
|
||||
animation: animation,
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
return EntrancePageTransition(
|
||||
animation: animation,
|
||||
vertical: true,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
key: ValueKey<int>(widget.index),
|
||||
child: widget.itemBuilder?.call(context, widget.index) ??
|
||||
widget.children![widget.index],
|
||||
),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that tells what's the the current state of a parent
|
||||
/// [NavigationView], if any.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationView], which provides the information for this
|
||||
/// * [NavigationBody], which is used to display the content on the view
|
||||
class InheritedNavigationView extends InheritedWidget {
|
||||
/// Creates an inherited navigation view.
|
||||
const InheritedNavigationView({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.displayMode,
|
||||
this.minimalPaneOpen = false,
|
||||
this.pane,
|
||||
this.oldIndex = 0,
|
||||
this.currentItemIndex = -1,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The current pane display mode according to the current state.
|
||||
final PaneDisplayMode? displayMode;
|
||||
|
||||
/// Whether the minimal pane is open or not
|
||||
final bool minimalPaneOpen;
|
||||
|
||||
/// The current navigation pane, if any
|
||||
final NavigationPane? pane;
|
||||
|
||||
/// The old index selected index. Usually used by [NavigationIndicator]s to
|
||||
/// display the animation from the old item to the new one.
|
||||
final int oldIndex;
|
||||
|
||||
/// Used by [NavigationIndicator] to know what's the current index of the
|
||||
/// item
|
||||
final int currentItemIndex;
|
||||
|
||||
static InheritedNavigationView? maybeOf(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<InheritedNavigationView>();
|
||||
}
|
||||
|
||||
static InheritedNavigationView of(BuildContext context) {
|
||||
return maybeOf(context)!;
|
||||
}
|
||||
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
int? currentItemIndex,
|
||||
NavigationPane? pane,
|
||||
PaneDisplayMode? displayMode,
|
||||
bool? minimalPaneOpen,
|
||||
int? oldIndex,
|
||||
}) {
|
||||
return Builder(builder: (context) {
|
||||
final current = InheritedNavigationView.maybeOf(context);
|
||||
return InheritedNavigationView(
|
||||
key: key,
|
||||
displayMode: displayMode ?? current?.displayMode,
|
||||
minimalPaneOpen: minimalPaneOpen ?? current?.minimalPaneOpen ?? false,
|
||||
currentItemIndex: currentItemIndex ?? current?.currentItemIndex ?? -1,
|
||||
pane: pane ?? current?.pane,
|
||||
oldIndex: oldIndex ?? current?.oldIndex ?? 0,
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedNavigationView oldWidget) {
|
||||
return oldWidget.displayMode != displayMode ||
|
||||
oldWidget.minimalPaneOpen != minimalPaneOpen ||
|
||||
oldWidget.pane != pane ||
|
||||
oldWidget.oldIndex != oldIndex ||
|
||||
oldWidget.currentItemIndex != oldWidget.currentItemIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes the [GlobalKey]s for [PaneItem]s accesible on the scope.
|
||||
class _PaneItemKeys extends InheritedWidget {
|
||||
const _PaneItemKeys({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.keys,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final Map<int, GlobalKey> keys;
|
||||
|
||||
/// Gets the item global key based on the index
|
||||
static GlobalKey of(int index, BuildContext context) {
|
||||
final reference =
|
||||
context.dependOnInheritedWidgetOfExactType<_PaneItemKeys>()!;
|
||||
return reference.keys[index]!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_PaneItemKeys oldWidget) {
|
||||
return keys != oldWidget.keys;
|
||||
}
|
||||
}
|
||||
340
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/indicators.dart
vendored
Normal file
340
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/indicators.dart
vendored
Normal file
@@ -0,0 +1,340 @@
|
||||
// ignore_for_file: use_key_in_widget_constructors
|
||||
|
||||
part of 'view.dart';
|
||||
|
||||
const kIndicatorAnimationDuration = Duration(milliseconds: 500);
|
||||
|
||||
/// A indicator used by [NavigationPane] to render the selected
|
||||
/// indicator.
|
||||
class NavigationIndicator extends StatefulWidget {
|
||||
/// Creates a navigation indicator used by [NavigationPane]
|
||||
/// to render the selected indicator.
|
||||
const NavigationIndicator({
|
||||
this.curve = Curves.linear,
|
||||
this.color,
|
||||
this.duration = kIndicatorAnimationDuration,
|
||||
}) : super();
|
||||
|
||||
/// The curve used on the animation, if any
|
||||
///
|
||||
/// For sticky navigation indicator, [Curves.easeIn] is recommended
|
||||
final Curve curve;
|
||||
|
||||
/// The duration used on the animation, if any
|
||||
///
|
||||
/// 500 milliseconds is used by default
|
||||
final Duration duration;
|
||||
|
||||
/// The highlight color
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
.add(DiagnosticsProperty('curve', curve, defaultValue: Curves.linear));
|
||||
properties.add(ColorProperty('highlight color', color));
|
||||
}
|
||||
|
||||
@override
|
||||
NavigationIndicatorState createState() => NavigationIndicatorState();
|
||||
}
|
||||
|
||||
class NavigationIndicatorState<T extends NavigationIndicator> extends State<T> {
|
||||
List<Offset>? offsets;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
fetch();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
void fetch() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final localOffsets = pane.effectiveItems._getPaneItemsOffsets(
|
||||
pane.paneKey,
|
||||
);
|
||||
if (mounted && (offsets != localOffsets)) {
|
||||
offsets = localOffsets;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
NavigationPane get pane {
|
||||
return InheritedNavigationView.of(context).pane!;
|
||||
}
|
||||
|
||||
int get index {
|
||||
return pane.selected ?? 0;
|
||||
}
|
||||
|
||||
bool get isSelected {
|
||||
return pane.isSelected(pane.effectiveItems[itemIndex]);
|
||||
}
|
||||
|
||||
Axis get axis {
|
||||
if (InheritedNavigationView.maybeOf(context)?.displayMode ==
|
||||
PaneDisplayMode.top) {
|
||||
return Axis.vertical;
|
||||
}
|
||||
return Axis.horizontal;
|
||||
}
|
||||
|
||||
int get itemIndex {
|
||||
return InheritedNavigationView.maybeOf(context)?.currentItemIndex ?? -1;
|
||||
}
|
||||
|
||||
int get oldIndex {
|
||||
return InheritedNavigationView.maybeOf(context)?.oldIndex ?? -1;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
/// The end navigation indicator
|
||||
class EndNavigationIndicator extends NavigationIndicator {
|
||||
const EndNavigationIndicator({
|
||||
Color? color,
|
||||
this.unselectedColor = Colors.transparent,
|
||||
}) : super(color: color);
|
||||
|
||||
/// The color of the indicator when the item is not selected
|
||||
final Color unselectedColor;
|
||||
|
||||
@override
|
||||
_EndNavigationIndicatorState createState() => _EndNavigationIndicatorState();
|
||||
}
|
||||
|
||||
class _EndNavigationIndicatorState
|
||||
extends NavigationIndicatorState<EndNavigationIndicator> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
|
||||
final bool isTop = axis == Axis.vertical;
|
||||
final theme = NavigationPaneTheme.of(context);
|
||||
|
||||
return IgnorePointer(
|
||||
child: Align(
|
||||
alignment: isTop
|
||||
? AlignmentDirectional.bottomCenter
|
||||
: AlignmentDirectional.centerStart,
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 75),
|
||||
reverseDuration: Duration.zero,
|
||||
child: Container(
|
||||
key: ValueKey<int>(itemIndex),
|
||||
margin: EdgeInsets.symmetric(
|
||||
vertical: isTop ? 0.0 : 10.0,
|
||||
horizontal: isTop ? 10.0 : 0.0,
|
||||
),
|
||||
width: isTop ? 20.0 : 6.0,
|
||||
height: isTop ? 4.5 : double.infinity,
|
||||
color: itemIndex != index
|
||||
? widget.unselectedColor
|
||||
: widget.color ?? theme.highlightColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A sticky navigation indicator.
|
||||
class StickyNavigationIndicator extends NavigationIndicator {
|
||||
/// Creates a sticky navigation indicator.
|
||||
const StickyNavigationIndicator({
|
||||
Curve curve = Curves.easeIn,
|
||||
Color? color,
|
||||
Duration duration = kIndicatorAnimationDuration,
|
||||
this.topPadding = 12.0,
|
||||
this.leftPadding = 10.0,
|
||||
}) : super(curve: curve, color: color, duration: duration);
|
||||
|
||||
/// The padding used on both horizontal sides of the indicator when the
|
||||
/// current display mode is top.
|
||||
///
|
||||
/// Defaults to 12.0
|
||||
final double topPadding;
|
||||
|
||||
/// The padding used on both vertical sides of the indicator when the current
|
||||
/// display mode is not top.
|
||||
///
|
||||
/// Defaults to 10.0
|
||||
final double leftPadding;
|
||||
|
||||
@override
|
||||
_StickyNavigationIndicatorState createState() =>
|
||||
_StickyNavigationIndicatorState();
|
||||
}
|
||||
|
||||
class _StickyNavigationIndicatorState
|
||||
extends NavigationIndicatorState<StickyNavigationIndicator>
|
||||
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||
late AnimationController upController;
|
||||
late AnimationController downController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
upController = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
value: 1.0,
|
||||
)..addListener(_updateListener);
|
||||
downController = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.duration,
|
||||
value: 1.0,
|
||||
)..addListener(_updateListener);
|
||||
}
|
||||
|
||||
void _updateListener() => setState(() {});
|
||||
|
||||
Animation<double>? upAnimation;
|
||||
Animation<double>? downAnimation;
|
||||
|
||||
int _old = -1;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
upController.dispose();
|
||||
downController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(StickyNavigationIndicator oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.duration != oldWidget.duration) {
|
||||
upController.duration = downController.duration = widget.duration;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isShowing {
|
||||
if (itemIndex.isNegative) return false;
|
||||
if (itemIndex == oldIndex && _old != oldIndex) {
|
||||
return true;
|
||||
}
|
||||
return itemIndex == index;
|
||||
}
|
||||
|
||||
bool get isAbove => oldIndex < index;
|
||||
bool get isBelow => oldIndex > index;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
animate();
|
||||
}
|
||||
|
||||
void animate() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isShowing && _old != oldIndex) {
|
||||
if (isBelow) {
|
||||
if (isSelected) {
|
||||
downAnimation = Tween<double>(begin: 0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
curve: Interval(0.5, 1.0, curve: widget.curve),
|
||||
parent: downController,
|
||||
),
|
||||
);
|
||||
await downController.forward(from: 0.0);
|
||||
} else {
|
||||
upAnimation = Tween<double>(begin: 0, end: 1.0).animate(
|
||||
CurvedAnimation(curve: widget.curve, parent: upController),
|
||||
);
|
||||
await upController.reverse(from: 1.0);
|
||||
}
|
||||
} else if (isAbove) {
|
||||
if (isSelected) {
|
||||
upAnimation = Tween<double>(begin: 0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
curve: Interval(0.5, 1.0, curve: widget.curve),
|
||||
parent: upController,
|
||||
),
|
||||
);
|
||||
await upController.forward(from: 0.0);
|
||||
} else {
|
||||
downAnimation = Tween<double>(begin: 0, end: 1.0).animate(
|
||||
CurvedAnimation(curve: widget.curve, parent: downController),
|
||||
);
|
||||
await downController.reverse(from: 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_old = oldIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (offsets == null || !isShowing) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// Ensure it is only kept alive after if it's showing and after the offets
|
||||
// are fetched
|
||||
super.build(context);
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
|
||||
final NavigationPaneThemeData theme = NavigationPaneTheme.of(context);
|
||||
final bool isHorizontal = axis == Axis.horizontal;
|
||||
|
||||
return SizedBox(
|
||||
height: double.infinity,
|
||||
child: IgnorePointer(
|
||||
child: Builder(builder: (context) {
|
||||
final decoration = BoxDecoration(
|
||||
color: widget.color ?? theme.highlightColor,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
);
|
||||
final child = isHorizontal
|
||||
? Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Container(width: 2.5, decoration: decoration),
|
||||
)
|
||||
: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(height: 2.5, decoration: decoration),
|
||||
);
|
||||
if (!isSelected) {
|
||||
if (upController.status == AnimationStatus.dismissed ||
|
||||
downController.status == AnimationStatus.dismissed) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
} else {
|
||||
if (upAnimation?.value == 0.0 || downAnimation?.value == 0.0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
return Padding(
|
||||
padding: isHorizontal
|
||||
? EdgeInsets.only(
|
||||
left: offsets![itemIndex].dx,
|
||||
top: widget.leftPadding * (upAnimation?.value ?? 1.0),
|
||||
bottom: widget.leftPadding * (downAnimation?.value ?? 1.0),
|
||||
)
|
||||
: EdgeInsetsDirectional.only(
|
||||
start: widget.topPadding * (upAnimation?.value ?? 1.0),
|
||||
end: widget.topPadding * (downAnimation?.value ?? 1.0),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
965
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/pane.dart
vendored
Normal file
965
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/pane.dart
vendored
Normal file
@@ -0,0 +1,965 @@
|
||||
part of 'view.dart';
|
||||
|
||||
const double _kCompactNavigationPanelWidth = 50.0;
|
||||
const double _kOpenNavigationPanelWidth = 320.0;
|
||||
|
||||
/// You can use the PaneDisplayMode property to configure different
|
||||
/// navigation styles, or display modes, for the NavigationView
|
||||
///
|
||||
/// 
|
||||
enum PaneDisplayMode {
|
||||
/// The pane is positioned above the content.
|
||||
///
|
||||
/// Use top navigation when:
|
||||
/// * You have 5 or fewer top-level navigation categories that
|
||||
/// are equally important, and any additional top-level navigation
|
||||
/// categories that end up in the dropdown overflow menu are
|
||||
/// considered less important.
|
||||
/// * You need to show all navigation options on screen.
|
||||
/// * You want more space for your app content.
|
||||
/// * Icons cannot clearly describe your app's navigation categories.
|
||||
///
|
||||
/// 
|
||||
top,
|
||||
|
||||
/// The pane is expanded and positioned to the left of the content.
|
||||
///
|
||||
/// Use open navigation when:
|
||||
/// * You have 5-10 equally important top-level navigation categories.
|
||||
/// * You want navigation categories to be very prominent, with less
|
||||
/// space for other app content.
|
||||
///
|
||||
/// 
|
||||
open,
|
||||
|
||||
/// The pane shows only icons until opened and is positioned to the left
|
||||
/// of the content.
|
||||
///
|
||||
/// 
|
||||
compact,
|
||||
|
||||
/// Only the menu button is shown until the pane is opened. When opened,
|
||||
/// it's positioned to the left of the content.
|
||||
///
|
||||
/// 
|
||||
minimal,
|
||||
|
||||
/// Let the [NavigationPane] decide what display mode should be used based on
|
||||
/// the width. This is used by default on [NavigationPane]. In Auto mode, the
|
||||
/// [NavigationPane] adapts between [minimal] when the window is narrow, to
|
||||
/// [compact], and then [open] as the window gets wider.
|
||||
///
|
||||
/// - An expanded left pane on large window widths (1008px or greater).
|
||||
/// - A left, icon-only, nav pane (LeftCompact) on medium window widths
|
||||
/// (641px to 1007px).
|
||||
/// - Only a menu button (LeftMinimal) on small window widths (640px or less).
|
||||
///
|
||||
/// 
|
||||
auto,
|
||||
}
|
||||
|
||||
/// The pane used by [NavigationView].
|
||||
///
|
||||
/// The [NavigationView] doesn't perform any navigation tasks automatically.
|
||||
/// When the user taps on a navigation item, [onChanged], if non-null, is called.
|
||||
///
|
||||
/// See also:
|
||||
/// * [NavigationView], used alongside this to navigate through pages
|
||||
/// * [PaneDisplayMode], that defines how this pane is rendered
|
||||
/// * [NavigationBody], the widget that implement transitions to the pages
|
||||
class NavigationPane with Diagnosticable {
|
||||
/// Creates a navigation pane.
|
||||
///
|
||||
/// If [selected] is non-null, [selected] must be greater or equal to 0
|
||||
NavigationPane({
|
||||
this.key,
|
||||
this.selected,
|
||||
this.onChanged,
|
||||
this.size,
|
||||
this.header,
|
||||
this.items = const [],
|
||||
this.footerItems = const [],
|
||||
this.autoSuggestBox,
|
||||
this.autoSuggestBoxReplacement,
|
||||
this.displayMode = PaneDisplayMode.auto,
|
||||
this.customPane,
|
||||
this.menuButton,
|
||||
this.scrollController,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.indicator = const StickyNavigationIndicator(),
|
||||
}) : assert(
|
||||
selected == null || !selected.isNegative,
|
||||
'The selected index must not be negative',
|
||||
);
|
||||
|
||||
final Key? key;
|
||||
|
||||
final GlobalKey paneKey = GlobalKey();
|
||||
|
||||
/// Use this property to customize how the pane will be displayed.
|
||||
/// [PaneDisplayMode.auto] is used by default.
|
||||
final PaneDisplayMode displayMode;
|
||||
|
||||
final NavigationPaneWidget? customPane;
|
||||
|
||||
/// The menu button used by this pane. If null and [onDisplayModeRequested]
|
||||
/// is null
|
||||
final Widget? menuButton;
|
||||
|
||||
/// The size of the pane in its various mode.
|
||||
final NavigationPaneSize? size;
|
||||
|
||||
/// The header of the pane.
|
||||
///
|
||||
/// Usually a [Text] or an [Image].
|
||||
///
|
||||
/// 
|
||||
/// 
|
||||
final Widget? header;
|
||||
|
||||
/// The items used by this panel. These items are displayed before
|
||||
/// [autoSuggestBox] and [footerItems].
|
||||
///
|
||||
/// Only [PaneItem], [PaneItemSeparator] and [PaneItemHeader] are
|
||||
/// accepted types. If other type is detected, an [UnsupportedError]
|
||||
/// is thrown.
|
||||
final List<NavigationPaneItem> items;
|
||||
|
||||
/// The footer items used by this panel. These items are displayed at
|
||||
/// the end of the panel and they can't be overflown.
|
||||
///
|
||||
/// Only [PaneItem], [PaneItemSeparator] and [PaneItemHeader] are
|
||||
/// accepted types. If other type is detected, an [UnsupportedError]
|
||||
/// is thrown.
|
||||
///
|
||||
/// | Top | Left |
|
||||
/// | --- | --- |
|
||||
/// |  |  |
|
||||
final List<NavigationPaneItem> footerItems;
|
||||
|
||||
/// An optional control to allow for app-level search. Usually
|
||||
/// an [AutoSuggestBox]
|
||||
final Widget? autoSuggestBox;
|
||||
|
||||
/// Used when the current display mode is [PaneDisplayMode.compact]
|
||||
/// as a replacement to [autoSuggestBox]. It's only displayed if
|
||||
/// [autoSuggestBox] is non-null.
|
||||
///
|
||||
/// It's usually an [Icon] with [FluentIcons.search] as the icon.
|
||||
final Widget? autoSuggestBoxReplacement;
|
||||
|
||||
/// The current selected index.
|
||||
///
|
||||
/// If null, none of the items is selected. If non-null, it must be
|
||||
/// a positive number.
|
||||
///
|
||||
/// This property is called as the index of [allItems], that means it
|
||||
/// must be in the range of 0 to [allItems.length]
|
||||
///
|
||||
/// See also:
|
||||
/// * [allItems], a getter that merge [items] + [footerItems] into
|
||||
/// a single list
|
||||
final int? selected;
|
||||
|
||||
/// Called when the current index changes.
|
||||
final ValueChanged<int>? onChanged;
|
||||
|
||||
/// The scroll controller used by the pane when [displayMode]
|
||||
/// is [PaneDisplayMode.compact] and [PaneDisplayMode.open].
|
||||
///
|
||||
/// If null, a local scroll controller is created to control
|
||||
/// the scrolling and keep the state of the scroll when the
|
||||
/// display mode is toggled.
|
||||
final ScrollController? scrollController;
|
||||
|
||||
/// The leading Widget for the Pane
|
||||
final Widget? leading;
|
||||
|
||||
/// The leading Widget for the Pane
|
||||
final Widget? trailing;
|
||||
|
||||
/// A function called when building the navigation indicator
|
||||
final Widget? indicator;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(EnumProperty('displayMode', displayMode));
|
||||
properties.add(IterableProperty('items', items));
|
||||
properties.add(IterableProperty('footerItems', footerItems));
|
||||
properties.add(IntProperty('selected', selected));
|
||||
properties
|
||||
.add(ObjectFlagProperty('onChanged', onChanged, ifNull: 'disabled'));
|
||||
properties.add(DiagnosticsProperty<ScrollController>(
|
||||
'scrollController',
|
||||
scrollController,
|
||||
));
|
||||
}
|
||||
|
||||
void _changeTo(NavigationPaneItem item) {
|
||||
final index = effectiveIndexOf(item);
|
||||
if (selected != index && !index.isNegative) onChanged?.call(index);
|
||||
}
|
||||
|
||||
/// A list of all of the items displayed on this pane.
|
||||
List<NavigationPaneItem> get allItems {
|
||||
return items + footerItems;
|
||||
}
|
||||
|
||||
List<NavigationPaneItem> get effectiveItems {
|
||||
return (allItems
|
||||
..removeWhere((i) => i is! PaneItem || i is PaneItemAction));
|
||||
}
|
||||
|
||||
/// Check if the provided [item] is selected on not.
|
||||
bool isSelected(NavigationPaneItem item) {
|
||||
return effectiveIndexOf(item) == selected;
|
||||
}
|
||||
|
||||
/// Get the current selected item
|
||||
PaneItem get selectedItem {
|
||||
assert(selected != null, 'There is no item selected');
|
||||
return effectiveItems[selected!] as PaneItem;
|
||||
}
|
||||
|
||||
/// Get the effective index of the navigation pane.
|
||||
int effectiveIndexOf(NavigationPaneItem item) {
|
||||
return effectiveItems.indexOf(item);
|
||||
}
|
||||
|
||||
static Widget buildMenuButton(
|
||||
BuildContext context,
|
||||
Widget itemTitle,
|
||||
NavigationPane pane, {
|
||||
EdgeInsetsGeometry padding = EdgeInsets.zero,
|
||||
required VoidCallback onPressed,
|
||||
}) {
|
||||
if (pane.menuButton != null) return pane.menuButton!;
|
||||
return Container(
|
||||
width: pane.size?.compactWidth ?? _kCompactNavigationPanelWidth,
|
||||
margin: padding,
|
||||
child: PaneItem(
|
||||
title: itemTitle,
|
||||
icon: const Icon(FluentIcons.global_nav_button),
|
||||
).build(
|
||||
context,
|
||||
false,
|
||||
onPressed,
|
||||
displayMode: PaneDisplayMode.compact,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is NavigationPane &&
|
||||
other.key == key &&
|
||||
other.displayMode == displayMode &&
|
||||
other.customPane == customPane &&
|
||||
other.menuButton == menuButton &&
|
||||
other.size == size &&
|
||||
other.header == header &&
|
||||
listEquals(other.items, items) &&
|
||||
listEquals(other.footerItems, footerItems) &&
|
||||
other.autoSuggestBox == autoSuggestBox &&
|
||||
other.autoSuggestBoxReplacement == autoSuggestBoxReplacement &&
|
||||
other.selected == selected &&
|
||||
other.onChanged == onChanged &&
|
||||
other.scrollController == scrollController &&
|
||||
other.indicator == indicator;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return key.hashCode ^
|
||||
displayMode.hashCode ^
|
||||
customPane.hashCode ^
|
||||
menuButton.hashCode ^
|
||||
size.hashCode ^
|
||||
header.hashCode ^
|
||||
items.hashCode ^
|
||||
footerItems.hashCode ^
|
||||
autoSuggestBox.hashCode ^
|
||||
autoSuggestBoxReplacement.hashCode ^
|
||||
selected.hashCode ^
|
||||
onChanged.hashCode ^
|
||||
scrollController.hashCode ^
|
||||
indicator.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure the size of the pane in its various mode.
|
||||
///
|
||||
/// ```dart
|
||||
/// NavigationView(
|
||||
/// pane: NavigationPane(
|
||||
/// size: NavigationPaneSize(
|
||||
/// openWidth: MediaQuery.of(context).size.width / 5,
|
||||
/// openMinWidth: 250,
|
||||
/// openMaxWidth: 320,
|
||||
/// ),
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [NavigationPane], which this configures the size of
|
||||
/// * [NavigationView], used to display [NavigationPane]s
|
||||
class NavigationPaneSize {
|
||||
/// The height of the pane when he is in top mode.
|
||||
///
|
||||
/// If the value is null, [kOneLineTileHeight] is used.
|
||||
final double? topHeight;
|
||||
|
||||
/// The width of the pane when he is in compact mode.
|
||||
///
|
||||
/// If the value is null, [_kCompactNavigationPanelWidth] is used.
|
||||
final double? compactWidth;
|
||||
|
||||
/// The width of the pane when he is open.
|
||||
///
|
||||
/// If the value is null, [_kOpenNavigationPanelWidth] is used.
|
||||
/// The width can be based on MediaQuery and used
|
||||
/// with [minWidth] and [maxWidth].
|
||||
final double? openWidth;
|
||||
|
||||
/// The minimum width of the pane when he is open.
|
||||
///
|
||||
/// If width is smaller than minWidth, minWidth is used as width.
|
||||
/// minWidth must be smaller or equal to maxWidth.
|
||||
final double? openMinWidth;
|
||||
|
||||
/// The maximum width of the pane when he is open.
|
||||
///
|
||||
/// If width is greater than maxWidth, maxWidth is used as width.
|
||||
/// maxWidth must be greater or equal than minWidth.
|
||||
final double? openMaxWidth;
|
||||
|
||||
/// The height of the header in NavigationPane.
|
||||
///
|
||||
/// Only used when NavigationPane mode is open.
|
||||
/// If the value is null, [_kOneLineTileHeight] is used.
|
||||
final double? headerHeight;
|
||||
|
||||
const NavigationPaneSize({
|
||||
this.topHeight,
|
||||
this.compactWidth,
|
||||
this.openWidth,
|
||||
this.openMinWidth,
|
||||
this.openMaxWidth,
|
||||
this.headerHeight,
|
||||
}) : assert(
|
||||
openMinWidth == null ||
|
||||
openMaxWidth == null ||
|
||||
openMinWidth <= openMaxWidth,
|
||||
'openMinWidth should be greater than openMaxWidth',
|
||||
);
|
||||
}
|
||||
|
||||
class NavigationPaneWidgetData {
|
||||
const NavigationPaneWidgetData({
|
||||
required this.content,
|
||||
required this.appBar,
|
||||
required this.scrollController,
|
||||
required this.paneKey,
|
||||
required this.listKey,
|
||||
required this.pane,
|
||||
});
|
||||
|
||||
final Widget content;
|
||||
final Widget appBar;
|
||||
final ScrollController scrollController;
|
||||
final Key? paneKey;
|
||||
final GlobalKey? listKey;
|
||||
final NavigationPane pane;
|
||||
}
|
||||
|
||||
/// Base class for creating custom navigation panes.
|
||||
///
|
||||
/// ```dart
|
||||
/// class CustomNavigationPane extends NavigationPaneWidget {
|
||||
/// CustomNavigationPane();
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context, NavigationPaneWidgetData data) {
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
abstract class NavigationPaneWidget {
|
||||
Widget build(BuildContext context, NavigationPaneWidgetData data);
|
||||
}
|
||||
|
||||
/// Creates a top navigation pane.
|
||||
///
|
||||
/// 
|
||||
class _TopNavigationPane extends StatefulWidget {
|
||||
_TopNavigationPane({
|
||||
required this.pane,
|
||||
this.listKey,
|
||||
this.appBar,
|
||||
}) : super(key: pane.key);
|
||||
|
||||
final NavigationPane pane;
|
||||
final GlobalKey? listKey;
|
||||
final NavigationAppBar? appBar;
|
||||
|
||||
@override
|
||||
State<_TopNavigationPane> createState() => _TopNavigationPaneState();
|
||||
}
|
||||
|
||||
class _TopNavigationPaneState extends State<_TopNavigationPane> {
|
||||
final overflowController = FlyoutController();
|
||||
List<int> hiddenPaneItems = [];
|
||||
late List<int> _localItemHold;
|
||||
void generateLocalItemHold() {
|
||||
_localItemHold = List.generate(
|
||||
widget.pane.items.length,
|
||||
(index) => index,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
generateLocalItemHold();
|
||||
}
|
||||
|
||||
void _onPressed(PaneItem item) {
|
||||
widget.pane._changeTo(item);
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, NavigationPaneItem item) {
|
||||
if (item is PaneItemHeader) {
|
||||
return item.build(context);
|
||||
} else if (item is PaneItemSeparator) {
|
||||
return item.build(context, Axis.vertical);
|
||||
} else if (item is PaneItem) {
|
||||
final selected = widget.pane.isSelected(item);
|
||||
return item.build(
|
||||
context,
|
||||
selected,
|
||||
() => _onPressed(item),
|
||||
// only show the text if the item is not in the footer
|
||||
showTextOnTop: !widget.pane.footerItems.contains(item),
|
||||
displayMode: PaneDisplayMode.top,
|
||||
);
|
||||
} else {
|
||||
throw UnsupportedError(
|
||||
'${item.runtimeType} is not a supported navigation pane item type.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
overflowController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _TopNavigationPane oldWidget) {
|
||||
// update the items
|
||||
if (oldWidget.pane.items.length != widget.pane.items.length) {
|
||||
generateLocalItemHold();
|
||||
}
|
||||
|
||||
// if the selected item changed
|
||||
if (widget.pane.selected != oldWidget.pane.selected) {
|
||||
final selectedItem = widget.pane.items.indexOf(
|
||||
widget.pane.selectedItem,
|
||||
);
|
||||
|
||||
// if the selected item is part of the middle items and
|
||||
// if there is a non-hidden item
|
||||
// and if the selected item is hidden
|
||||
if (!selectedItem.isNegative &&
|
||||
!hiddenPaneItems.contains(0) &&
|
||||
hiddenPaneItems.contains(_localItemHold.indexOf(selectedItem))) {
|
||||
generateLocalItemHold();
|
||||
|
||||
int item = hiddenPaneItems.first - 1;
|
||||
while (widget.pane.items[item] is! PaneItem) {
|
||||
item--;
|
||||
if (item.isNegative) {
|
||||
item++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_localItemHold
|
||||
..remove(selectedItem)
|
||||
..insert(item, selectedItem);
|
||||
// print(
|
||||
// 's$selectedItem to$item - i$_localItemHold - h$hiddenPaneItems',
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final height = widget.pane.size?.topHeight ?? kOneLineTileHeight;
|
||||
return SizedBox(
|
||||
key: widget.pane.paneKey,
|
||||
height: height,
|
||||
child: Stack(
|
||||
children: [
|
||||
MoveWindow(),
|
||||
|
||||
Row(children: [
|
||||
if (widget.pane.leading != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 6.0,
|
||||
),
|
||||
child: widget.pane.leading!,
|
||||
),
|
||||
if (widget.pane.header != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 6.0,
|
||||
),
|
||||
child: widget.pane.header!,
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(4.0),
|
||||
child: DynamicOverflow(
|
||||
overflowWidgetAlignment: MainAxisAlignment.start,
|
||||
overflowWidget: Flyout(
|
||||
controller: overflowController,
|
||||
placement: FlyoutPlacement.end,
|
||||
content: (context) => MenuFlyout(
|
||||
items: _localItemHold.sublist(hiddenPaneItems.first).map((i) {
|
||||
final item = widget.pane.items[i];
|
||||
return buildMenuPaneItem(context, item);
|
||||
}).toList(),
|
||||
),
|
||||
child: PaneItem(icon: const Icon(FluentIcons.more)).build(
|
||||
context,
|
||||
false,
|
||||
overflowController.open,
|
||||
showTextOnTop: false,
|
||||
displayMode: PaneDisplayMode.top,
|
||||
),
|
||||
),
|
||||
overflowChangedCallback: (hiddenItems) {
|
||||
setState(() {
|
||||
// indexes should always be valid
|
||||
assert(() {
|
||||
for (var i = 0; i < hiddenItems.length; i++) {
|
||||
if (hiddenItems[i] < 0 ||
|
||||
hiddenItems[i] >= widget.pane.items.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
hiddenPaneItems = hiddenItems;
|
||||
});
|
||||
},
|
||||
children: _localItemHold.map((index) {
|
||||
final item = widget.pane.items[index];
|
||||
return SizedBox(
|
||||
height: height,
|
||||
child: _buildItem(context, item),
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
)
|
||||
),
|
||||
if (widget.pane.autoSuggestBox != null)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 30.0),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 100.0,
|
||||
maxWidth: 215.0,
|
||||
),
|
||||
child: widget.pane.autoSuggestBox!,
|
||||
),
|
||||
...widget.pane.footerItems.map((item) {
|
||||
return _buildItem(context, item);
|
||||
}).toList(),
|
||||
|
||||
if (widget.pane.trailing != null)
|
||||
widget.pane.trailing!
|
||||
])
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
MenuFlyoutItemInterface buildMenuPaneItem(
|
||||
BuildContext context, NavigationPaneItem item) {
|
||||
if (item is PaneItemSeparator) {
|
||||
return const MenuFlyoutSeparator();
|
||||
} else if (item is PaneItem) {
|
||||
return _MenuFlyoutPaneItem(
|
||||
item: item,
|
||||
onPressed: () => _onPressed(item),
|
||||
);
|
||||
} else {
|
||||
throw UnsupportedError('${item.runtimeType} is not supported');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _MenuFlyoutPaneItem extends MenuFlyoutItemInterface {
|
||||
_MenuFlyoutPaneItem({
|
||||
Key? key,
|
||||
required this.item,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
final PaneItem item;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = PopupContentSizeInfo.of(context).size;
|
||||
final NavigationPaneThemeData theme = NavigationPaneTheme.of(context);
|
||||
|
||||
final String titleText = item._getPropertyFromTitle<String>() ?? '';
|
||||
final TextStyle baseStyle =
|
||||
item._getPropertyFromTitle<TextStyle>() ?? const TextStyle();
|
||||
|
||||
final textResult = titleText.isNotEmpty
|
||||
? Padding(
|
||||
padding: theme.labelPadding ?? EdgeInsets.zero,
|
||||
child: RichText(
|
||||
text: item._getPropertyFromTitle<InlineSpan>(baseStyle)!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign:
|
||||
item._getPropertyFromTitle<TextAlign>() ?? TextAlign.start,
|
||||
textHeightBehavior:
|
||||
item._getPropertyFromTitle<TextHeightBehavior>(),
|
||||
textWidthBasis: item._getPropertyFromTitle<TextWidthBasis>() ??
|
||||
TextWidthBasis.parent,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
|
||||
return HoverButton(
|
||||
onPressed: onPressed,
|
||||
builder: (context, states) {
|
||||
return Container(
|
||||
width: size.isEmpty ? null : size.width,
|
||||
padding: MenuFlyout.itemsPadding,
|
||||
height: 36.0,
|
||||
color: ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
states,
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Padding(
|
||||
padding: theme.iconPadding ?? EdgeInsets.zero,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(
|
||||
color: theme.unselectedIconColor?.resolve(states) ??
|
||||
baseStyle.color,
|
||||
size: 16.0,
|
||||
),
|
||||
child: Center(child: item.icon),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
fit: size.isEmpty ? FlexFit.loose : FlexFit.tight,
|
||||
child: textResult,
|
||||
),
|
||||
if (item.infoBadge != null) item.infoBadge!,
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CompactNavigationPane extends StatelessWidget {
|
||||
_CompactNavigationPane({
|
||||
required this.pane,
|
||||
this.paneKey,
|
||||
this.listKey,
|
||||
this.onToggle,
|
||||
}) : super(key: pane.key);
|
||||
|
||||
final NavigationPane pane;
|
||||
final Key? paneKey;
|
||||
final GlobalKey? listKey;
|
||||
final VoidCallback? onToggle;
|
||||
|
||||
Widget _buildItem(BuildContext context, NavigationPaneItem item) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
if (item is PaneItemHeader) {
|
||||
// Item Header is not visible on compact pane
|
||||
return const SizedBox();
|
||||
} else if (item is PaneItemSeparator) {
|
||||
return item.build(context, Axis.horizontal);
|
||||
} else if (item is PaneItem) {
|
||||
final selected = pane.isSelected(item);
|
||||
return item.build(
|
||||
context,
|
||||
selected,
|
||||
() {
|
||||
pane._changeTo(item);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw UnsupportedError(
|
||||
'${item.runtimeType} is not a supported pane item type.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = NavigationPaneTheme.of(context);
|
||||
const EdgeInsetsGeometry topPadding = EdgeInsets.only(bottom: 8.0);
|
||||
final bool showReplacement =
|
||||
pane.autoSuggestBox != null && pane.autoSuggestBoxReplacement != null;
|
||||
return AnimatedContainer(
|
||||
key: paneKey,
|
||||
duration: theme.animationDuration ?? Duration.zero,
|
||||
curve: theme.animationCurve ?? Curves.linear,
|
||||
width: pane.size?.compactWidth ?? _kCompactNavigationPanelWidth,
|
||||
child: Align(
|
||||
key: pane.paneKey,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
() {
|
||||
if (pane.menuButton != null) return pane.menuButton!;
|
||||
if (onToggle != null) {
|
||||
return NavigationPane.buildMenuButton(
|
||||
context,
|
||||
Text(FluentLocalizations.of(context).openNavigationTooltip),
|
||||
pane,
|
||||
onPressed: () {
|
||||
onToggle?.call();
|
||||
},
|
||||
padding: showReplacement ? EdgeInsets.zero : topPadding,
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}(),
|
||||
if (showReplacement)
|
||||
Padding(
|
||||
padding: topPadding,
|
||||
child: PaneItem(
|
||||
title: Text(FluentLocalizations.of(context).clickToSearch),
|
||||
icon: pane.autoSuggestBoxReplacement!,
|
||||
).build(
|
||||
context,
|
||||
false,
|
||||
() {
|
||||
onToggle?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
key: listKey,
|
||||
primary: true,
|
||||
children: pane.items.map((item) {
|
||||
return _buildItem(context, item);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
ListView(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
primary: false,
|
||||
children: pane.footerItems.map((item) {
|
||||
return _buildItem(context, item);
|
||||
}).toList(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OpenNavigationPane extends StatefulWidget {
|
||||
_OpenNavigationPane({
|
||||
required this.pane,
|
||||
required this.theme,
|
||||
this.paneKey,
|
||||
this.listKey,
|
||||
this.onToggle,
|
||||
this.onItemSelected,
|
||||
}) : super(key: pane.key);
|
||||
|
||||
final NavigationPane pane;
|
||||
final Key? paneKey;
|
||||
final GlobalKey? listKey;
|
||||
final VoidCallback? onToggle;
|
||||
final VoidCallback? onItemSelected;
|
||||
|
||||
final NavigationPaneThemeData theme;
|
||||
|
||||
static Widget buildItem(
|
||||
BuildContext context,
|
||||
NavigationPane pane,
|
||||
NavigationPaneItem item, [
|
||||
VoidCallback? onChanged,
|
||||
bool autofocus = false,
|
||||
]) {
|
||||
if (item is PaneItemHeader) {
|
||||
return item.build(context);
|
||||
} else if (item is PaneItemSeparator) {
|
||||
return item.build(context, Axis.horizontal);
|
||||
} else if (item is PaneItem) {
|
||||
final selected = pane.isSelected(item);
|
||||
return item.build(
|
||||
context,
|
||||
selected,
|
||||
() {
|
||||
pane._changeTo(item);
|
||||
onChanged?.call();
|
||||
},
|
||||
autofocus: autofocus,
|
||||
);
|
||||
} else {
|
||||
throw UnsupportedError(
|
||||
'${item.runtimeType} is not a supported pane item type.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
State<_OpenNavigationPane> createState() => _OpenNavigationPaneState();
|
||||
}
|
||||
|
||||
class _OpenNavigationPaneState extends State<_OpenNavigationPane>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController controller;
|
||||
|
||||
NavigationPaneThemeData get theme => widget.theme;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: theme.animationDuration,
|
||||
);
|
||||
controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
const EdgeInsetsGeometry topPadding = EdgeInsets.only(bottom: 6.0);
|
||||
final menuButton = () {
|
||||
if (widget.pane.menuButton != null) return widget.pane.menuButton!;
|
||||
if (widget.onToggle != null) {
|
||||
return NavigationPane.buildMenuButton(
|
||||
context,
|
||||
Text(FluentLocalizations.of(context).closeNavigationTooltip),
|
||||
widget.pane,
|
||||
onPressed: () {
|
||||
widget.onToggle?.call();
|
||||
},
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
}();
|
||||
double paneWidth =
|
||||
widget.pane.size?.openWidth ?? _kOpenNavigationPanelWidth;
|
||||
if (widget.pane.size?.openMaxWidth != null &&
|
||||
paneWidth > widget.pane.size!.openMaxWidth!) {
|
||||
paneWidth = widget.pane.size!.openMaxWidth!;
|
||||
}
|
||||
if (widget.pane.size?.openMinWidth != null &&
|
||||
paneWidth < widget.pane.size!.openMinWidth!) {
|
||||
paneWidth = widget.pane.size!.openMinWidth!;
|
||||
}
|
||||
|
||||
return SizeTransition(
|
||||
axisAlignment: -1,
|
||||
axis: Axis.horizontal,
|
||||
sizeFactor: Tween<double>(begin: 0, end: 1.0).animate(controller),
|
||||
child: AnimatedContainer(
|
||||
key: widget.paneKey,
|
||||
duration: Duration.zero,
|
||||
curve: Curves.linear,
|
||||
width: paneWidth,
|
||||
child: Column(key: widget.pane.paneKey, children: [
|
||||
Container(
|
||||
margin: widget.pane.autoSuggestBox != null
|
||||
? EdgeInsets.zero
|
||||
: topPadding,
|
||||
height: widget.pane.size?.headerHeight ?? kOneLineTileHeight,
|
||||
child: () {
|
||||
if (widget.pane.header != null) {
|
||||
return Row(children: [
|
||||
menuButton,
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: widget.pane.header!,
|
||||
),
|
||||
),
|
||||
]);
|
||||
} else {
|
||||
return menuButton;
|
||||
}
|
||||
}(),
|
||||
),
|
||||
if (widget.pane.autoSuggestBox != null)
|
||||
Container(
|
||||
padding: theme.iconPadding ?? EdgeInsets.zero,
|
||||
height: 41.0,
|
||||
alignment: Alignment.center,
|
||||
margin: topPadding,
|
||||
child: widget.pane.autoSuggestBox!,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
key: widget.listKey,
|
||||
primary: true,
|
||||
children: widget.pane.items.map((item) {
|
||||
return _OpenNavigationPane.buildItem(
|
||||
context,
|
||||
widget.pane,
|
||||
item,
|
||||
widget.onItemSelected,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
ListView(
|
||||
primary: false,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: widget.pane.footerItems.map((item) {
|
||||
return _OpenNavigationPane.buildItem(
|
||||
context,
|
||||
widget.pane,
|
||||
item,
|
||||
widget.onItemSelected,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
556
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/pane_items.dart
vendored
Normal file
556
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/pane_items.dart
vendored
Normal file
@@ -0,0 +1,556 @@
|
||||
part of 'view.dart';
|
||||
|
||||
class NavigationPaneItem with Diagnosticable {
|
||||
final GlobalKey itemKey = GlobalKey();
|
||||
|
||||
NavigationPaneItem();
|
||||
}
|
||||
|
||||
/// The item used by [NavigationView] to display the tiles.
|
||||
///
|
||||
/// On [PaneDisplayMode.compact], only [icon] is displayed, and [title] is
|
||||
/// used as a tooltip. On the other display modes, [icon] and [title] are
|
||||
/// displayed in a [Row].
|
||||
///
|
||||
/// This is the only [NavigationPaneItem] that is affected by [NavigationIndicator]s
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [PaneItemSeparator], used to group navigation items
|
||||
/// * [PaneItemHeader], used to label groups of items.
|
||||
/// * [PaneItemAction], the item used for execute an action on click
|
||||
class PaneItem extends NavigationPaneItem {
|
||||
/// Creates a pane item.
|
||||
PaneItem({
|
||||
required this.icon,
|
||||
this.title,
|
||||
this.infoBadge,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.mouseCursor,
|
||||
this.tileColor,
|
||||
this.selectedTileColor,
|
||||
});
|
||||
|
||||
/// The title used by this item. If the display mode is top
|
||||
/// or compact, this is shown as a tooltip. If it's open, this
|
||||
/// is shown by the side of the [icon].
|
||||
///
|
||||
/// The text style is fetched from the closest [NavigationPaneThemeData]
|
||||
///
|
||||
/// If this is a [Text], its [Text.data] is used to display the tooltip. The
|
||||
/// tooltip is only displayed only on compact mode and when the item is not
|
||||
/// disabled.
|
||||
/// It is also used by [Semantics] to allow screen readers to
|
||||
/// read the screen.
|
||||
///
|
||||
/// Usually a [Text] widget.
|
||||
final Widget? title;
|
||||
|
||||
/// The icon used by this item.
|
||||
///
|
||||
/// Usually an [Icon] widget
|
||||
final Widget icon;
|
||||
|
||||
/// The info badge used by this item
|
||||
final InfoBadge? infoBadge;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro fluent_ui.controls.inputs.HoverButton.mouseCursor}
|
||||
final MouseCursor? mouseCursor;
|
||||
|
||||
/// The color of the tile when unselected.
|
||||
/// If null, [NavigationPaneThemeData.tileColor] is used
|
||||
final ButtonState<Color?>? tileColor;
|
||||
|
||||
/// The color of the tile when unselected.
|
||||
/// If null, [NavigationPaneThemeData.tileColor]/hovering is used
|
||||
final ButtonState<Color?>? selectedTileColor;
|
||||
|
||||
T? _getPropertyFromTitle<T>([dynamic def]) {
|
||||
if (title is Text) {
|
||||
final title = this.title as Text;
|
||||
switch (T) {
|
||||
case String:
|
||||
return (title.data ?? title.textSpan?.toPlainText()) as T?;
|
||||
case InlineSpan:
|
||||
return (title.textSpan ??
|
||||
TextSpan(
|
||||
text: title.data ?? '',
|
||||
style: _getPropertyFromTitle<TextStyle>()
|
||||
?.merge(def as TextStyle?) ??
|
||||
def as TextStyle?,
|
||||
)) as T?;
|
||||
case TextStyle:
|
||||
return title.style as T?;
|
||||
case TextAlign:
|
||||
return title.textAlign as T?;
|
||||
case TextHeightBehavior:
|
||||
return title.textHeightBehavior as T?;
|
||||
case TextWidthBasis:
|
||||
return title.textWidthBasis as T?;
|
||||
}
|
||||
} else if (title is RichText) {
|
||||
final title = this.title as RichText;
|
||||
switch (T) {
|
||||
case String:
|
||||
return title.text.toPlainText() as T?;
|
||||
case InlineSpan:
|
||||
if (T is InlineSpan) {
|
||||
final span = title.text;
|
||||
span.style?.merge(def as TextStyle?);
|
||||
return span as T;
|
||||
}
|
||||
return title.text as T;
|
||||
case TextStyle:
|
||||
return (title.text.style as T?) ?? def as T?;
|
||||
case TextAlign:
|
||||
return title.textAlign as T?;
|
||||
case TextHeightBehavior:
|
||||
return title.textHeightBehavior as T?;
|
||||
case TextWidthBasis:
|
||||
return title.textWidthBasis as T?;
|
||||
}
|
||||
} else if (title is Icon) {
|
||||
final title = this.title as Icon;
|
||||
switch (T) {
|
||||
case String:
|
||||
if (title.icon?.codePoint == null) return null;
|
||||
return String.fromCharCode(title.icon!.codePoint) as T?;
|
||||
case InlineSpan:
|
||||
return TextSpan(
|
||||
text: String.fromCharCode(title.icon!.codePoint),
|
||||
style: _getPropertyFromTitle<TextStyle>(),
|
||||
) as T?;
|
||||
case TextStyle:
|
||||
return TextStyle(
|
||||
color: title.color,
|
||||
fontSize: title.size,
|
||||
fontFamily: title.icon?.fontFamily,
|
||||
package: title.icon?.fontPackage,
|
||||
) as T?;
|
||||
case TextAlign:
|
||||
return null;
|
||||
case TextHeightBehavior:
|
||||
return null;
|
||||
case TextWidthBasis:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Used to construct the pane items all around [NavigationView]. You can
|
||||
/// customize how the pane items should look like by overriding this method
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
bool selected,
|
||||
VoidCallback? onPressed, {
|
||||
PaneDisplayMode? displayMode,
|
||||
bool showTextOnTop = true,
|
||||
bool? autofocus,
|
||||
}) {
|
||||
final maybeBody = InheritedNavigationView.maybeOf(context);
|
||||
final PaneDisplayMode mode = displayMode ??
|
||||
maybeBody?.displayMode ??
|
||||
maybeBody?.pane?.displayMode ??
|
||||
PaneDisplayMode.minimal;
|
||||
assert(mode != PaneDisplayMode.auto);
|
||||
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
|
||||
final direction = Directionality.of(context);
|
||||
|
||||
final NavigationPaneThemeData theme = NavigationPaneTheme.of(context);
|
||||
final String titleText = _getPropertyFromTitle<String>() ?? '';
|
||||
|
||||
final TextStyle baseStyle =
|
||||
_getPropertyFromTitle<TextStyle>() ?? const TextStyle();
|
||||
|
||||
final bool isTop = mode == PaneDisplayMode.top;
|
||||
final bool isCompact = mode == PaneDisplayMode.compact;
|
||||
|
||||
final button = HoverButton(
|
||||
autofocus: autofocus ?? this.autofocus,
|
||||
focusNode: focusNode,
|
||||
onPressed: onPressed,
|
||||
cursor: mouseCursor,
|
||||
builder: (context, states) {
|
||||
TextStyle textStyle = baseStyle.merge(
|
||||
selected
|
||||
? theme.selectedTextStyle?.resolve(states)
|
||||
: theme.unselectedTextStyle?.resolve(states),
|
||||
);
|
||||
if (isTop && states.isPressing) {
|
||||
textStyle = textStyle.copyWith(
|
||||
color: textStyle.color?.withOpacity(0.75),
|
||||
);
|
||||
}
|
||||
final textResult = titleText.isNotEmpty
|
||||
? Padding(
|
||||
padding: theme.labelPadding ?? EdgeInsets.zero,
|
||||
child: RichText(
|
||||
text: _getPropertyFromTitle<InlineSpan>(textStyle)!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign:
|
||||
_getPropertyFromTitle<TextAlign>() ?? TextAlign.start,
|
||||
textHeightBehavior:
|
||||
_getPropertyFromTitle<TextHeightBehavior>(),
|
||||
textWidthBasis: _getPropertyFromTitle<TextWidthBasis>() ??
|
||||
TextWidthBasis.parent,
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
Widget result() {
|
||||
switch (mode) {
|
||||
case PaneDisplayMode.compact:
|
||||
return Container(
|
||||
key: itemKey,
|
||||
height: 36.0,
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: theme.iconPadding ?? EdgeInsets.zero,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(
|
||||
color: (selected
|
||||
? theme.selectedIconColor?.resolve(states)
|
||||
: theme.unselectedIconColor?.resolve(states)) ??
|
||||
textStyle.color,
|
||||
size: 16.0,
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: () {
|
||||
if (infoBadge != null) {
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
icon,
|
||||
Positioned(
|
||||
right: -8,
|
||||
top: -8,
|
||||
child: infoBadge!,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return icon;
|
||||
}()),
|
||||
),
|
||||
),
|
||||
);
|
||||
case PaneDisplayMode.minimal:
|
||||
case PaneDisplayMode.open:
|
||||
return SizedBox(
|
||||
key: itemKey,
|
||||
height: 36.0,
|
||||
child: Row(children: [
|
||||
Padding(
|
||||
padding: theme.iconPadding ?? EdgeInsets.zero,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(
|
||||
color: (selected
|
||||
? theme.selectedIconColor?.resolve(states)
|
||||
: theme.unselectedIconColor?.resolve(states)) ??
|
||||
textStyle.color,
|
||||
size: 16.0,
|
||||
),
|
||||
child: Center(child: icon),
|
||||
),
|
||||
),
|
||||
Expanded(child: textResult),
|
||||
if (infoBadge != null)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 8.0),
|
||||
child: infoBadge!,
|
||||
),
|
||||
]),
|
||||
);
|
||||
case PaneDisplayMode.top:
|
||||
Widget result = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: theme.iconPadding ?? EdgeInsets.zero,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(
|
||||
color: (selected
|
||||
? theme.selectedIconColor?.resolve(states)
|
||||
: theme.unselectedIconColor?.resolve(states)) ??
|
||||
textStyle.color,
|
||||
size: 16.0,
|
||||
),
|
||||
child: Center(child: icon),
|
||||
),
|
||||
),
|
||||
if (showTextOnTop) textResult,
|
||||
],
|
||||
);
|
||||
if (infoBadge != null) {
|
||||
return Stack(
|
||||
key: itemKey,
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
result,
|
||||
if (infoBadge != null)
|
||||
Positioned.directional(
|
||||
textDirection: direction,
|
||||
end: -3,
|
||||
top: 3,
|
||||
child: infoBadge!,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return KeyedSubtree(key: itemKey, child: result);
|
||||
default:
|
||||
throw '$mode is not a supported type';
|
||||
}
|
||||
}
|
||||
|
||||
return Semantics(
|
||||
label: titleText.isEmpty ? null : titleText,
|
||||
selected: selected,
|
||||
child: AnimatedContainer(
|
||||
duration: theme.animationDuration ?? Duration.zero,
|
||||
curve: theme.animationCurve ?? standardCurve,
|
||||
margin: const EdgeInsets.only(right: 6.0, left: 6.0),
|
||||
decoration: BoxDecoration(
|
||||
color: () {
|
||||
final ButtonState<Color?> tileColor = this.tileColor ??
|
||||
theme.tileColor ??
|
||||
kDefaultTileColor(
|
||||
context,
|
||||
isTop,
|
||||
);
|
||||
final newStates = states.toSet()..remove(ButtonStates.disabled);
|
||||
if (selected && selectedTileColor != null) {
|
||||
return selectedTileColor!.resolve(newStates);
|
||||
}
|
||||
return tileColor.resolve(
|
||||
selected
|
||||
? {
|
||||
states.isHovering
|
||||
? ButtonStates.pressing
|
||||
: ButtonStates.hovering,
|
||||
}
|
||||
: newStates,
|
||||
);
|
||||
}(),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: FocusBorder(
|
||||
focused: states.isFocused,
|
||||
renderOutside: false,
|
||||
child: () {
|
||||
final showTooltip = ((isTop && !showTextOnTop) || isCompact) &&
|
||||
titleText.isNotEmpty &&
|
||||
!states.isDisabled;
|
||||
|
||||
if (showTooltip) {
|
||||
return Tooltip(
|
||||
richMessage: _getPropertyFromTitle<InlineSpan>(),
|
||||
style: TooltipThemeData(textStyle: baseStyle),
|
||||
child: result(),
|
||||
);
|
||||
}
|
||||
|
||||
return result();
|
||||
}(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
final int? index = () {
|
||||
if (maybeBody?.pane?.indicator != null) {
|
||||
return maybeBody!.pane!.effectiveIndexOf(this);
|
||||
}
|
||||
}();
|
||||
|
||||
final GlobalKey? key = () {
|
||||
if (index != null && !index.isNegative) {
|
||||
return _PaneItemKeys.of(index, context);
|
||||
}
|
||||
}();
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: () {
|
||||
// If there is an indicator and the item is an effective item
|
||||
if (maybeBody?.pane?.indicator != null && index != -1) {
|
||||
return Stack(children: [
|
||||
button,
|
||||
Positioned.fill(
|
||||
child: InheritedNavigationView.merge(
|
||||
currentItemIndex: index,
|
||||
child: KeyedSubtree(
|
||||
key: index != null ? key : null,
|
||||
child: maybeBody!.pane!.indicator!,
|
||||
),
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return button;
|
||||
}(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Separators for grouping navigation items. Set the color property to
|
||||
/// [Colors.transparent] to render the separator as space. Uses a [Divider]
|
||||
/// under the hood, consequently uses the closest [DividerThemeData].
|
||||
///
|
||||
/// See also:
|
||||
/// * [PaneItem], the item used by [NavigationView] to render tiles
|
||||
/// * [PaneItemHeader], used to label groups of items.
|
||||
/// * [PaneItemAction], the item used for execute an action on click
|
||||
class PaneItemSeparator extends NavigationPaneItem {
|
||||
/// Creates an item separator.
|
||||
PaneItemSeparator({this.color, this.thickness});
|
||||
|
||||
/// The color used by the [Divider].
|
||||
final Color? color;
|
||||
|
||||
/// The separator thickness. Defaults to 1.0
|
||||
final double? thickness;
|
||||
|
||||
Widget build(BuildContext context, Axis direction) {
|
||||
return Divider(
|
||||
key: itemKey,
|
||||
direction: direction,
|
||||
style: DividerThemeData(
|
||||
thickness: thickness,
|
||||
decoration: color != null ? BoxDecoration(color: color) : null,
|
||||
verticalMargin: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 10.0,
|
||||
),
|
||||
horizontalMargin: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 10.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Headers for labeling groups of items. This is not displayed if the display
|
||||
/// mode is [PaneDisplayMode.compact]
|
||||
///
|
||||
/// See also:
|
||||
/// * [PaneItem], the item used by [NavigationView] to render tiles
|
||||
/// * [PaneItemSeparator], used to group navigation items
|
||||
/// * [PaneItemAction], the item used for execute an action on click
|
||||
class PaneItemHeader extends NavigationPaneItem {
|
||||
/// Creates a pane header.
|
||||
PaneItemHeader({required this.header});
|
||||
|
||||
/// The header. The default style is [NavigationPaneThemeData.itemHeaderTextStyle],
|
||||
/// but can be overriten by [Text.style].
|
||||
///
|
||||
/// Usually a [Text] widget.
|
||||
final Widget header;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = NavigationPaneTheme.of(context);
|
||||
return Padding(
|
||||
key: itemKey,
|
||||
padding: theme.iconPadding ?? EdgeInsets.zero,
|
||||
child: DefaultTextStyle(
|
||||
style: theme.itemHeaderTextStyle ?? const TextStyle(),
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
textAlign: TextAlign.left,
|
||||
child: header,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The item used by [NavigationView] to display the tiles.
|
||||
///
|
||||
/// On [PaneDisplayMode.compact], only [icon] is displayed, and [title] is
|
||||
/// used as a tooltip. On the other display modes, [icon] and [title] are
|
||||
/// displayed in a [Row].
|
||||
///
|
||||
/// The difference with [PaneItem] is that the item is not linked
|
||||
/// to a page but to an action passed in parameter (callback)
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [PaneItem], the item used by [NavigationView] to render tiles
|
||||
/// * [PaneItemSeparator], used to group navigation items
|
||||
/// * [PaneItemHeader], used to label groups of items.
|
||||
class PaneItemAction extends PaneItem {
|
||||
PaneItemAction({
|
||||
required Widget icon,
|
||||
required this.onTap,
|
||||
title,
|
||||
infoBadge,
|
||||
focusNode,
|
||||
autofocus = false,
|
||||
}) : super(
|
||||
icon: icon,
|
||||
title: title,
|
||||
infoBadge: infoBadge,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
);
|
||||
|
||||
/// The function that will be executed when the item is clicked
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
bool selected,
|
||||
VoidCallback? onPressed, {
|
||||
PaneDisplayMode? displayMode,
|
||||
bool showTextOnTop = true,
|
||||
bool? autofocus,
|
||||
int index = -1,
|
||||
}) {
|
||||
return super.build(
|
||||
context,
|
||||
selected,
|
||||
onTap,
|
||||
displayMode: displayMode,
|
||||
showTextOnTop: showTextOnTop,
|
||||
autofocus: autofocus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension _ItemsExtension on List<NavigationPaneItem> {
|
||||
/// Get the all the item offets in this list
|
||||
List<Offset> _getPaneItemsOffsets(GlobalKey<State<StatefulWidget>> paneKey) {
|
||||
return map((e) {
|
||||
// Gets the item global position
|
||||
final itemContext = e.itemKey.currentContext;
|
||||
if (itemContext == null) return Offset.zero;
|
||||
final box = itemContext.findRenderObject()! as RenderBox;
|
||||
final globalPosition = box.localToGlobal(Offset.zero);
|
||||
// And then convert it to the local position
|
||||
final paneContext = paneKey.currentContext;
|
||||
if (paneContext == null) return Offset.zero;
|
||||
final paneBox = paneKey.currentContext!.findRenderObject() as RenderBox;
|
||||
final position = paneBox.globalToLocal(globalPosition);
|
||||
return position;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
216
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/style.dart
vendored
Normal file
216
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/style.dart
vendored
Normal file
@@ -0,0 +1,216 @@
|
||||
part of 'view.dart';
|
||||
|
||||
ButtonState<Color?> kDefaultTileColor(BuildContext context, bool isTop) {
|
||||
return ButtonState.resolveWith((states) {
|
||||
// By default, if it's top, do not show any color
|
||||
if (isTop) return Colors.transparent;
|
||||
return ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
states,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [NavigationPane]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [NavigationPane] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class NavigationPaneTheme extends InheritedTheme {
|
||||
/// Creates a navigation pane theme that controls the configurations for
|
||||
/// [NavigationPane].
|
||||
const NavigationPaneTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [NavigationPane] widgets.
|
||||
final NavigationPaneThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [NavigationPane]s
|
||||
/// should look like, and merges in the current slider theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required NavigationPaneThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return NavigationPaneTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static NavigationPaneThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme =
|
||||
context.dependOnInheritedWidgetOfExactType<NavigationPaneTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).navigationPaneTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [NavigationPaneTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.navigationPaneTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// NavigationPaneThemeData theme = NavigationPaneTheme.of(context);
|
||||
/// ```
|
||||
static NavigationPaneThemeData of(BuildContext context) {
|
||||
return FluentTheme.of(context).navigationPaneTheme.merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return NavigationPaneTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(NavigationPaneTheme oldWidget) =>
|
||||
data != oldWidget.data;
|
||||
}
|
||||
|
||||
/// The theme data used by [NavigationView]. The default theme
|
||||
/// data used is [NavigationPaneThemeData.standard].
|
||||
class NavigationPaneThemeData with Diagnosticable {
|
||||
/// The pane background color. If null, [ThemeData.micaBackgroundColor]
|
||||
/// is used.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The color of the tiles. If null, [ButtonThemeData.uncheckedInputColor]
|
||||
/// is used
|
||||
final ButtonState<Color?>? tileColor;
|
||||
|
||||
/// The highlight color used on the tiles. If null, [ThemeData.accentColor]
|
||||
/// is used.
|
||||
final Color? highlightColor;
|
||||
|
||||
final EdgeInsetsGeometry? labelPadding;
|
||||
final EdgeInsetsGeometry? iconPadding;
|
||||
|
||||
final TextStyle? itemHeaderTextStyle;
|
||||
final ButtonState<TextStyle?>? selectedTextStyle;
|
||||
final ButtonState<TextStyle?>? unselectedTextStyle;
|
||||
final ButtonState<Color?>? selectedIconColor;
|
||||
final ButtonState<Color?>? unselectedIconColor;
|
||||
|
||||
final Duration? animationDuration;
|
||||
final Curve? animationCurve;
|
||||
|
||||
const NavigationPaneThemeData({
|
||||
this.backgroundColor,
|
||||
this.tileColor,
|
||||
this.highlightColor,
|
||||
this.labelPadding,
|
||||
this.iconPadding,
|
||||
this.itemHeaderTextStyle,
|
||||
this.selectedTextStyle,
|
||||
this.unselectedTextStyle,
|
||||
this.animationDuration,
|
||||
this.animationCurve,
|
||||
this.selectedIconColor,
|
||||
this.unselectedIconColor,
|
||||
});
|
||||
|
||||
factory NavigationPaneThemeData.standard({
|
||||
required Color disabledColor,
|
||||
required Duration animationDuration,
|
||||
required Curve animationCurve,
|
||||
required Color backgroundColor,
|
||||
required Color highlightColor,
|
||||
required Typography typography,
|
||||
required Color inactiveColor,
|
||||
}) {
|
||||
final disabledTextStyle = TextStyle(
|
||||
color: disabledColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
return NavigationPaneThemeData(
|
||||
animationDuration: animationDuration,
|
||||
animationCurve: animationCurve,
|
||||
backgroundColor: backgroundColor,
|
||||
highlightColor: highlightColor,
|
||||
itemHeaderTextStyle: typography.bodyStrong,
|
||||
selectedTextStyle: ButtonState.resolveWith((states) {
|
||||
return states.isDisabled ? disabledTextStyle : typography.body;
|
||||
}),
|
||||
unselectedTextStyle: ButtonState.resolveWith((states) {
|
||||
return states.isDisabled ? disabledTextStyle : typography.body!;
|
||||
}),
|
||||
labelPadding: const EdgeInsetsDirectional.only(end: 10.0),
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
);
|
||||
}
|
||||
|
||||
static NavigationPaneThemeData lerp(
|
||||
NavigationPaneThemeData? a,
|
||||
NavigationPaneThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return NavigationPaneThemeData(
|
||||
iconPadding: EdgeInsetsGeometry.lerp(a?.iconPadding, b?.iconPadding, t),
|
||||
labelPadding:
|
||||
EdgeInsetsGeometry.lerp(a?.labelPadding, b?.labelPadding, t),
|
||||
tileColor: ButtonState.lerp(a?.tileColor, b?.tileColor, t, Color.lerp),
|
||||
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
|
||||
itemHeaderTextStyle:
|
||||
TextStyle.lerp(a?.itemHeaderTextStyle, b?.itemHeaderTextStyle, t),
|
||||
selectedTextStyle: ButtonState.lerp(
|
||||
a?.selectedTextStyle, b?.selectedTextStyle, t, TextStyle.lerp),
|
||||
unselectedTextStyle: ButtonState.lerp(
|
||||
a?.unselectedTextStyle, b?.unselectedTextStyle, t, TextStyle.lerp),
|
||||
highlightColor: Color.lerp(a?.highlightColor, b?.highlightColor, t),
|
||||
animationCurve: t < 0.5 ? a?.animationCurve : b?.animationCurve,
|
||||
animationDuration: lerpDuration(a?.animationDuration ?? Duration.zero,
|
||||
b?.animationDuration ?? Duration.zero, t),
|
||||
selectedIconColor: ButtonState.lerp(
|
||||
a?.selectedIconColor, b?.selectedIconColor, t, Color.lerp),
|
||||
unselectedIconColor: ButtonState.lerp(
|
||||
a?.unselectedIconColor, b?.unselectedIconColor, t, Color.lerp),
|
||||
);
|
||||
}
|
||||
|
||||
NavigationPaneThemeData merge(NavigationPaneThemeData? style) {
|
||||
return NavigationPaneThemeData(
|
||||
iconPadding: style?.iconPadding ?? iconPadding,
|
||||
labelPadding: style?.labelPadding ?? labelPadding,
|
||||
tileColor: style?.tileColor ?? tileColor,
|
||||
backgroundColor: style?.backgroundColor ?? backgroundColor,
|
||||
itemHeaderTextStyle: style?.itemHeaderTextStyle ?? itemHeaderTextStyle,
|
||||
selectedTextStyle: style?.selectedTextStyle ?? selectedTextStyle,
|
||||
unselectedTextStyle: style?.unselectedTextStyle ?? unselectedTextStyle,
|
||||
highlightColor: style?.highlightColor ?? highlightColor,
|
||||
animationCurve: style?.animationCurve ?? animationCurve,
|
||||
animationDuration: style?.animationDuration ?? animationDuration,
|
||||
selectedIconColor: style?.selectedIconColor ?? selectedIconColor,
|
||||
unselectedIconColor: style?.unselectedIconColor ?? unselectedIconColor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty('tileColor', tileColor));
|
||||
properties.add(ColorProperty('backgroundColor', backgroundColor));
|
||||
properties.add(ColorProperty('highlightColor', highlightColor));
|
||||
properties.add(
|
||||
DiagnosticsProperty<EdgeInsetsGeometry>('labelPadding', labelPadding));
|
||||
properties.add(
|
||||
DiagnosticsProperty<EdgeInsetsGeometry>('iconPadding', iconPadding));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Duration>('animationDuration', animationDuration));
|
||||
properties
|
||||
.add(DiagnosticsProperty<Curve>('animationCurve', animationCurve));
|
||||
properties.add(DiagnosticsProperty('selectedTextStyle', selectedTextStyle));
|
||||
properties
|
||||
.add(DiagnosticsProperty('unselectedTextStyle', unselectedTextStyle));
|
||||
properties.add(DiagnosticsProperty('selectedIconColor', selectedIconColor));
|
||||
properties
|
||||
.add(DiagnosticsProperty('unselectedIconColor', unselectedIconColor));
|
||||
}
|
||||
}
|
||||
750
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/view.dart
vendored
Normal file
750
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/navigation_view/view.dart
vendored
Normal file
@@ -0,0 +1,750 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import '../../../utils/popup.dart';
|
||||
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
|
||||
part 'body.dart';
|
||||
|
||||
part 'indicators.dart';
|
||||
|
||||
part 'pane_items.dart';
|
||||
|
||||
part 'pane.dart';
|
||||
|
||||
part 'style.dart';
|
||||
|
||||
/// The default size used by the app top bar.
|
||||
///
|
||||
/// Value eyeballed from Windows 10 v10.0.19041.928
|
||||
const double _kDefaultAppBarHeight = 50.0;
|
||||
|
||||
/// The NavigationView control provides top-level navigation
|
||||
/// for your app. It adapts to a variety of screen sizes and
|
||||
/// supports both top and left navigation styles.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
/// * [NavigationBody], a widget that implement fluent
|
||||
/// transitions into [NavigationView]
|
||||
/// * [NavigationPane], the pane used by [NavigationView],
|
||||
/// that can be displayed either at the left and top
|
||||
/// * [TabView], a widget similar to [NavigationView], useful
|
||||
/// to display several pages of content while giving a user
|
||||
/// the capability to rearrange, open, or close new tabs.
|
||||
class NavigationView extends StatefulWidget {
|
||||
/// Creates a navigation view.
|
||||
const NavigationView({
|
||||
Key? key,
|
||||
this.appBar,
|
||||
this.pane,
|
||||
this.content = const SizedBox.shrink(),
|
||||
this.clipBehavior = Clip.antiAlias,
|
||||
this.contentShape,
|
||||
// If more properties are added here, make sure to
|
||||
// add them to the automatic mode as well.
|
||||
}) : super(key: key);
|
||||
|
||||
/// The app bar of the app.
|
||||
final NavigationAppBar? appBar;
|
||||
|
||||
/// The navigation pane, that can be displayed either on the
|
||||
/// left, on the top, or above [content].
|
||||
final NavigationPane? pane;
|
||||
|
||||
/// The content of the pane.
|
||||
///
|
||||
/// Usually an [NavigationBody].
|
||||
final Widget content;
|
||||
|
||||
/// {@macro flutter.rendering.ClipRectLayer.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.hardEdge].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// How the content should be clipped
|
||||
///
|
||||
/// The content is not clipped on when [PaneDisplayMode.displayMode]
|
||||
/// is [PaneDisplayMode.minimal]
|
||||
final ShapeBorder? contentShape;
|
||||
|
||||
static NavigationViewState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<NavigationViewState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty('appBar', appBar));
|
||||
properties.add(DiagnosticsProperty('pane', pane));
|
||||
}
|
||||
|
||||
@override
|
||||
NavigationViewState createState() => NavigationViewState();
|
||||
}
|
||||
|
||||
class NavigationViewState extends State<NavigationView> {
|
||||
/// The scroll controller used to keep the scrolling state of
|
||||
/// the list view when the display mode is switched between open
|
||||
/// and compact, and even keep it for the minimal state.
|
||||
///
|
||||
/// It's also used to display and control the [Scrollbar] introduced
|
||||
/// by the panes.
|
||||
late ScrollController scrollController;
|
||||
|
||||
/// The key used to animate between open and compact display mode
|
||||
final _panelKey = GlobalKey();
|
||||
final _listKey = GlobalKey();
|
||||
final _contentKey = GlobalKey();
|
||||
final _overlayKey = GlobalKey();
|
||||
|
||||
final Map<int, GlobalKey> _itemKeys = {};
|
||||
|
||||
/// The overlay entry used for minimal pane
|
||||
OverlayEntry? minimalOverlayEntry;
|
||||
|
||||
bool _minimalPaneOpen = false;
|
||||
bool _compactOverlayOpen = false;
|
||||
|
||||
int oldIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController = ScrollController(
|
||||
debugLabel: '${widget.runtimeType} scroll controller',
|
||||
keepScrollOffset: true,
|
||||
);
|
||||
scrollController.addListener(() {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
|
||||
generateKeys();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(NavigationView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.pane?.scrollController != scrollController) {
|
||||
scrollController = widget.pane?.scrollController ?? scrollController;
|
||||
}
|
||||
|
||||
if (oldWidget.pane?.selected != widget.pane?.selected) {
|
||||
oldIndex = oldWidget.pane?.selected ?? 0;
|
||||
}
|
||||
|
||||
if (oldWidget.pane?.effectiveItems.length !=
|
||||
widget.pane?.effectiveItems.length) {
|
||||
if (widget.pane?.effectiveItems.length != null) {
|
||||
generateKeys();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void generateKeys() {
|
||||
if (widget.pane == null) return;
|
||||
_itemKeys
|
||||
..clear()
|
||||
..addAll(
|
||||
Map.fromIterables(
|
||||
List.generate(widget.pane!.effectiveItems.length, (i) => i),
|
||||
List.generate(
|
||||
widget.pane!.effectiveItems.length,
|
||||
(_) => GlobalKey(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
|
||||
final Brightness brightness = FluentTheme.of(context).brightness;
|
||||
final NavigationPaneThemeData theme = NavigationPaneTheme.of(context);
|
||||
final localizations = FluentLocalizations.of(context);
|
||||
final direction = Directionality.of(context);
|
||||
|
||||
Color? _overlayBackgroundColor() {
|
||||
if (theme.backgroundColor?.alpha == 0) {
|
||||
if (brightness.isDark) {
|
||||
return const Color(0xFF202020);
|
||||
} else {
|
||||
return const Color(0xFFf7f7f7);
|
||||
}
|
||||
}
|
||||
return theme.backgroundColor;
|
||||
}
|
||||
|
||||
Widget appBar = () {
|
||||
if (widget.appBar != null) {
|
||||
final minimalLeading = PaneItem(
|
||||
title: Text(!_minimalPaneOpen
|
||||
? localizations.openNavigationTooltip
|
||||
: localizations.closeNavigationTooltip),
|
||||
icon: const Icon(FluentIcons.global_nav_button),
|
||||
).build(
|
||||
context,
|
||||
false,
|
||||
() async {
|
||||
setState(() => _minimalPaneOpen = !_minimalPaneOpen);
|
||||
},
|
||||
displayMode: PaneDisplayMode.compact,
|
||||
);
|
||||
return _NavigationAppBar(
|
||||
appBar: widget.appBar!,
|
||||
additionalLeading: widget.pane?.displayMode == PaneDisplayMode.minimal
|
||||
? minimalLeading
|
||||
: null,
|
||||
);
|
||||
}
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) =>
|
||||
SizedBox(width: constraints.maxWidth, height: 0),
|
||||
);
|
||||
}();
|
||||
|
||||
return LayoutBuilder(builder: (context, consts) {
|
||||
var displayMode = widget.pane?.displayMode ?? PaneDisplayMode.auto;
|
||||
|
||||
if (displayMode == PaneDisplayMode.auto) {
|
||||
/// For more info on the adaptive behavior, see
|
||||
/// https://docs.microsoft.com/en-us/windows/apps/design/controls/navigationview#adaptive-behavior
|
||||
///
|
||||
/// DD/MM/YYYY
|
||||
/// (06/04/2022)
|
||||
///
|
||||
/// When PaneDisplayMode is set to its default value of Auto, the
|
||||
/// adaptive behavior is to show:
|
||||
/// - An expanded left pane on large window widths (1008px or greater).
|
||||
/// - A left, icon-only, nav pane (compact) on medium window widths
|
||||
/// (641px to 1007px).
|
||||
/// - Only a menu button (minimal) on small window widths (640px or less).
|
||||
double width = consts.biggest.width;
|
||||
if (width.isInfinite) width = MediaQuery.of(context).size.width;
|
||||
|
||||
late PaneDisplayMode autoDisplayMode;
|
||||
if (width <= 640) {
|
||||
autoDisplayMode = PaneDisplayMode.minimal;
|
||||
} else if (width >= 1008) {
|
||||
autoDisplayMode = PaneDisplayMode.open;
|
||||
} else if (width > 640) {
|
||||
autoDisplayMode = PaneDisplayMode.compact;
|
||||
}
|
||||
|
||||
displayMode = autoDisplayMode;
|
||||
}
|
||||
|
||||
assert(displayMode != PaneDisplayMode.auto);
|
||||
|
||||
late Widget paneResult;
|
||||
if (widget.pane != null) {
|
||||
final pane = widget.pane!;
|
||||
if (pane.customPane != null) {
|
||||
paneResult = Builder(builder: (context) {
|
||||
return pane.customPane!.build(
|
||||
context,
|
||||
NavigationPaneWidgetData(
|
||||
appBar: appBar,
|
||||
content: ClipRect(child: widget.content),
|
||||
listKey: _listKey,
|
||||
paneKey: _panelKey,
|
||||
scrollController: scrollController,
|
||||
pane: pane,
|
||||
),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
final contentShape = widget.contentShape ??
|
||||
RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
width: 0.3,
|
||||
color: FluentTheme.of(context).brightness.isDark
|
||||
? Colors.black
|
||||
: const Color(0xffBCBCBC),
|
||||
),
|
||||
borderRadius: displayMode == PaneDisplayMode.top
|
||||
? BorderRadius.zero
|
||||
: const BorderRadiusDirectional.only(
|
||||
topStart: Radius.circular(8.0),
|
||||
).resolve(direction),
|
||||
);
|
||||
final Widget content = ClipRect(
|
||||
key: _contentKey,
|
||||
child: displayMode == PaneDisplayMode.minimal
|
||||
? widget.content
|
||||
: DecoratedBox(
|
||||
position: DecorationPosition.foreground,
|
||||
decoration: ShapeDecoration(shape: contentShape),
|
||||
child: ClipPath(
|
||||
clipBehavior: widget.clipBehavior,
|
||||
clipper: ShapeBorderClipper(shape: contentShape),
|
||||
child: widget.content,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (displayMode != PaneDisplayMode.compact) {
|
||||
_compactOverlayOpen = false;
|
||||
}
|
||||
switch (displayMode) {
|
||||
case PaneDisplayMode.top:
|
||||
paneResult = Column(children: [
|
||||
appBar,
|
||||
PaneScrollConfiguration(
|
||||
child: _TopNavigationPane(
|
||||
pane: pane,
|
||||
listKey: _listKey,
|
||||
appBar: widget.appBar,
|
||||
),
|
||||
),
|
||||
Expanded(child: content),
|
||||
]);
|
||||
break;
|
||||
case PaneDisplayMode.compact:
|
||||
void toggleCompactOpenMode() {
|
||||
setState(() => _compactOverlayOpen = !_compactOverlayOpen);
|
||||
}
|
||||
|
||||
final openSize =
|
||||
pane.size?.openWidth ?? _kOpenNavigationPanelWidth;
|
||||
|
||||
final bool openedWithoutOverlay =
|
||||
_compactOverlayOpen && consts.maxWidth / 2.5 > openSize;
|
||||
|
||||
paneResult = Stack(children: [
|
||||
AnimatedPositionedDirectional(
|
||||
duration: theme.animationDuration ?? Duration.zero,
|
||||
curve: theme.animationCurve ?? Curves.linear,
|
||||
top: widget.appBar?.height ?? 0.0,
|
||||
start: openedWithoutOverlay
|
||||
? openSize
|
||||
: pane.size?.compactWidth ??
|
||||
_kCompactNavigationPanelWidth,
|
||||
end: 0,
|
||||
bottom: 0,
|
||||
child: content,
|
||||
),
|
||||
// If the overlay is open, add a gesture detector above the
|
||||
// content to close if the user click outside the overlay
|
||||
if (_compactOverlayOpen)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: toggleCompactOpenMode,
|
||||
child: AbsorbPointer(
|
||||
child: Semantics(
|
||||
label: localizations.modalBarrierDismissLabel,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PaneScrollConfiguration(
|
||||
child: () {
|
||||
if (openedWithoutOverlay) {
|
||||
return Mica(
|
||||
key: _overlayKey,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
child: Container(
|
||||
child: _OpenNavigationPane(
|
||||
theme: theme,
|
||||
pane: pane,
|
||||
paneKey: _panelKey,
|
||||
listKey: _listKey,
|
||||
onToggle: toggleCompactOpenMode,
|
||||
onItemSelected: toggleCompactOpenMode,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (_compactOverlayOpen) {
|
||||
return Mica(
|
||||
key: _overlayKey,
|
||||
backgroundColor: _overlayBackgroundColor(),
|
||||
elevation: 10.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFF6c6c6c),
|
||||
width: 0.15,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: _OpenNavigationPane(
|
||||
theme: theme,
|
||||
pane: pane,
|
||||
paneKey: _panelKey,
|
||||
listKey: _listKey,
|
||||
onToggle: toggleCompactOpenMode,
|
||||
onItemSelected: toggleCompactOpenMode,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Mica(
|
||||
key: _overlayKey,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
child: _CompactNavigationPane(
|
||||
pane: pane,
|
||||
paneKey: _panelKey,
|
||||
listKey: _listKey,
|
||||
onToggle: toggleCompactOpenMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
}(),
|
||||
),
|
||||
appBar,
|
||||
]);
|
||||
break;
|
||||
case PaneDisplayMode.open:
|
||||
paneResult = Column(children: [
|
||||
appBar,
|
||||
Expanded(
|
||||
child: Row(children: [
|
||||
PaneScrollConfiguration(
|
||||
child: _OpenNavigationPane(
|
||||
theme: theme,
|
||||
pane: pane,
|
||||
paneKey: _panelKey,
|
||||
listKey: _listKey,
|
||||
),
|
||||
),
|
||||
Expanded(child: content),
|
||||
]),
|
||||
),
|
||||
]);
|
||||
break;
|
||||
case PaneDisplayMode.minimal:
|
||||
paneResult = Stack(children: [
|
||||
Positioned(
|
||||
top: widget.appBar?.height ?? 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
bottom: 0.0,
|
||||
child: content,
|
||||
),
|
||||
if (_minimalPaneOpen)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() => _minimalPaneOpen = false);
|
||||
},
|
||||
child: AbsorbPointer(
|
||||
child: Semantics(
|
||||
label: localizations.modalBarrierDismissLabel,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedPositionedDirectional(
|
||||
key: _overlayKey,
|
||||
duration: theme.animationDuration ?? Duration.zero,
|
||||
curve: theme.animationCurve ?? Curves.linear,
|
||||
start: _minimalPaneOpen ? 0.0 : -_kOpenNavigationPanelWidth,
|
||||
width: _kOpenNavigationPanelWidth,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: PaneScrollConfiguration(
|
||||
child: Mica(
|
||||
backgroundColor: _overlayBackgroundColor(),
|
||||
elevation: 10.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: const Color(0xFF6c6c6c),
|
||||
width: 0.15,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
child: _OpenNavigationPane(
|
||||
theme: theme,
|
||||
pane: pane,
|
||||
paneKey: _panelKey,
|
||||
listKey: _listKey,
|
||||
onItemSelected: () {
|
||||
setState(() => _minimalPaneOpen = false);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
appBar,
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
paneResult = content;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
paneResult = Column(children: [
|
||||
appBar,
|
||||
Expanded(child: widget.content),
|
||||
]);
|
||||
}
|
||||
return Mica(
|
||||
backgroundColor: theme.backgroundColor,
|
||||
child: InheritedNavigationView(
|
||||
displayMode: _compactOverlayOpen ? PaneDisplayMode.open : displayMode,
|
||||
minimalPaneOpen: _minimalPaneOpen,
|
||||
pane: widget.pane,
|
||||
oldIndex: oldIndex,
|
||||
child: _PaneItemKeys(
|
||||
keys: _itemKeys,
|
||||
child: paneResult,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
Widget PaneScrollConfiguration({required Widget child}) {
|
||||
return PrimaryScrollController(
|
||||
controller: scrollController,
|
||||
child: ScrollConfiguration(
|
||||
behavior: const _NavigationViewScrollBehavior(),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The bar displayed at the top of the app. It can adapt itself to
|
||||
/// all the display modes.
|
||||
///
|
||||
/// See also:
|
||||
/// - [NavigationView]
|
||||
/// - [NavigationPane]
|
||||
/// - [PaneDisplayMode]
|
||||
class NavigationAppBar with Diagnosticable {
|
||||
final Key? key;
|
||||
|
||||
/// The widget at the beggining of the app bar, before [title].
|
||||
///
|
||||
/// Typically the [leading] widget is an [Icon] or an [IconButton].
|
||||
///
|
||||
/// If this is null and [automaticallyImplyLeading] is set to true, the
|
||||
/// view will imply an appropriate widget. If the parent [Navigator] can
|
||||
/// go back, the app bar will use an [IconButton] that calls [Navigator.maybePop].
|
||||
///
|
||||
/// See also:
|
||||
/// * [automaticallyImplyLeading], that controls whether we should try to
|
||||
/// imply the leading widget, if [leading] is null
|
||||
final Widget? leading;
|
||||
|
||||
/// {@macro flutter.material.appbar.automaticallyImplyLeading}
|
||||
final bool automaticallyImplyLeading;
|
||||
|
||||
/// Typically a [Text] widget that contains the app name.
|
||||
final Widget? title;
|
||||
|
||||
/// A list of Widgets to display in a row after the [title] widget.
|
||||
///
|
||||
/// Typically these widgets are [IconButton]s representing common
|
||||
/// operations.
|
||||
final Widget? actions;
|
||||
|
||||
/// The height of the app bar. [_kDefaultAppBarHeight] is used by default
|
||||
final double height;
|
||||
|
||||
/// The background color of this app bar.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Creates an app bar
|
||||
const NavigationAppBar({
|
||||
this.key,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.actions,
|
||||
this.automaticallyImplyLeading = true,
|
||||
this.height = _kDefaultAppBarHeight,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(FlagProperty(
|
||||
'automatically imply leading',
|
||||
value: automaticallyImplyLeading,
|
||||
ifFalse: 'do not imply leading',
|
||||
defaultValue: true,
|
||||
));
|
||||
properties.add(ColorProperty('backgroundColor', backgroundColor));
|
||||
properties.add(DoubleProperty(
|
||||
'height',
|
||||
height,
|
||||
defaultValue: _kDefaultAppBarHeight,
|
||||
));
|
||||
}
|
||||
|
||||
static Widget buildLeading(
|
||||
BuildContext context,
|
||||
NavigationAppBar appBar, [
|
||||
bool imply = true,
|
||||
]) {
|
||||
final ModalRoute<dynamic>? parentRoute = ModalRoute.of(context);
|
||||
final bool canPop = parentRoute?.canPop ?? false;
|
||||
late Widget widget;
|
||||
if (appBar.leading != null) {
|
||||
widget = appBar.leading!;
|
||||
} else if (appBar.automaticallyImplyLeading && imply) {
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final localizations = FluentLocalizations.of(context);
|
||||
final onPressed = canPop ? () => Navigator.maybePop(context) : null;
|
||||
widget = NavigationPaneTheme(
|
||||
data: NavigationPaneTheme.of(context).merge(NavigationPaneThemeData(
|
||||
unselectedIconColor: ButtonState.resolveWith((states) {
|
||||
if (states.isDisabled) {
|
||||
return ButtonThemeData.buttonColor(
|
||||
FluentTheme.of(context).brightness,
|
||||
states,
|
||||
);
|
||||
}
|
||||
return ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context),
|
||||
states,
|
||||
).basedOnLuminance();
|
||||
}),
|
||||
)),
|
||||
child: Builder(
|
||||
builder: (context) => PaneItem(
|
||||
icon: const Icon(FluentIcons.back, size: 14.0),
|
||||
title: Text(localizations.backButtonTooltip),
|
||||
).build(
|
||||
context,
|
||||
false,
|
||||
onPressed,
|
||||
displayMode: PaneDisplayMode.compact,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
widget = SizedBox(width: _kCompactNavigationPanelWidth, child: widget);
|
||||
return widget;
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationAppBar extends StatelessWidget {
|
||||
const _NavigationAppBar({
|
||||
Key? key,
|
||||
required this.appBar,
|
||||
this.additionalLeading,
|
||||
}) : super(key: key);
|
||||
|
||||
final NavigationAppBar appBar;
|
||||
final Widget? additionalLeading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final direction = Directionality.of(context);
|
||||
final PaneDisplayMode displayMode =
|
||||
InheritedNavigationView.maybeOf(context)?.displayMode ??
|
||||
PaneDisplayMode.top;
|
||||
final leading = NavigationAppBar.buildLeading(
|
||||
context,
|
||||
appBar,
|
||||
displayMode != PaneDisplayMode.top,
|
||||
);
|
||||
final title = () {
|
||||
if (appBar.title != null) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = NavigationPaneTheme.of(context);
|
||||
return AnimatedPadding(
|
||||
duration: theme.animationDuration ?? Duration.zero,
|
||||
curve: theme.animationCurve ?? Curves.linear,
|
||||
padding: [PaneDisplayMode.minimal, PaneDisplayMode.open]
|
||||
.contains(displayMode)
|
||||
? EdgeInsets.zero
|
||||
: const EdgeInsets.only(left: 24.0),
|
||||
child: DefaultTextStyle(
|
||||
style:
|
||||
FluentTheme.of(context).typography.caption ?? const TextStyle(),
|
||||
overflow: TextOverflow.clip,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
child: appBar.title!,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}();
|
||||
late Widget result;
|
||||
switch (displayMode) {
|
||||
case PaneDisplayMode.top:
|
||||
result = Row(children: [
|
||||
leading,
|
||||
if (additionalLeading != null) additionalLeading!,
|
||||
title,
|
||||
if (appBar.actions != null) Expanded(child: appBar.actions!),
|
||||
]);
|
||||
break;
|
||||
case PaneDisplayMode.minimal:
|
||||
case PaneDisplayMode.open:
|
||||
case PaneDisplayMode.compact:
|
||||
final isMinimalPaneOpen =
|
||||
InheritedNavigationView.maybeOf(context)?.minimalPaneOpen ?? false;
|
||||
final double width =
|
||||
displayMode == PaneDisplayMode.minimal && !isMinimalPaneOpen
|
||||
? 0.0
|
||||
: displayMode == PaneDisplayMode.compact
|
||||
? _kCompactNavigationPanelWidth
|
||||
: _kOpenNavigationPanelWidth;
|
||||
result = Stack(children: [
|
||||
Row(children: [
|
||||
leading,
|
||||
if (additionalLeading != null) additionalLeading!,
|
||||
Expanded(child: title),
|
||||
]),
|
||||
if (appBar.actions != null)
|
||||
Positioned.directional(
|
||||
textDirection: direction,
|
||||
start: width,
|
||||
end: 0.0,
|
||||
top: 0.0,
|
||||
bottom: 0.0,
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: appBar.actions!,
|
||||
),
|
||||
),
|
||||
]);
|
||||
break;
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Container(
|
||||
color: appBar.backgroundColor,
|
||||
height: appBar.height,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationViewScrollBehavior extends ScrollBehavior {
|
||||
const _NavigationViewScrollBehavior();
|
||||
@override
|
||||
Widget buildScrollbar(context, child, details) {
|
||||
return Scrollbar(
|
||||
controller: PrimaryScrollController.of(context),
|
||||
thumbVisibility: false,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
810
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/tab_view.dart
vendored
Normal file
810
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/tab_view.dart
vendored
Normal file
@@ -0,0 +1,810 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const double _kMinTileWidth = 80.0;
|
||||
const double _kMaxTileWidth = 240.0;
|
||||
const double _kTileHeight = 34.0;
|
||||
const double _kButtonWidth = 40.0;
|
||||
|
||||
enum CloseButtonVisibilityMode {
|
||||
/// The close button will never be visible
|
||||
never,
|
||||
|
||||
/// The close button will always be visible
|
||||
always,
|
||||
|
||||
/// The close button will only be shown on hover
|
||||
onHover,
|
||||
}
|
||||
|
||||
/// Determines how the tab sizes itself
|
||||
enum TabWidthBehavior {
|
||||
/// The tab will fit its content
|
||||
sizeToContent,
|
||||
|
||||
/// If not scrollable, the tabs will have the same size
|
||||
equal,
|
||||
|
||||
/// If not selected, the [Tab]'s text is hidden. The tab will fit its content
|
||||
compact,
|
||||
}
|
||||
|
||||
/// The TabView control is a way to display a set of tabs
|
||||
/// and their respective content. TabViews are useful for
|
||||
/// displaying several pages (or documents) of content while
|
||||
/// giving a user the capability to rearrange, open, or close
|
||||
/// new tabs.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// There must be enough space to render the tabview.
|
||||
///
|
||||
/// See also:
|
||||
/// - [NavigationPanel]
|
||||
class TabView extends StatefulWidget {
|
||||
/// Creates a tab view.
|
||||
///
|
||||
/// [tabs] must have the same length as [bodies]
|
||||
///
|
||||
/// [maxTabWidth] must be non-negative
|
||||
const TabView({
|
||||
Key? key,
|
||||
required this.currentIndex,
|
||||
this.onChanged,
|
||||
required this.tabs,
|
||||
required this.bodies,
|
||||
this.onNewPressed,
|
||||
this.addIconData = FluentIcons.add,
|
||||
this.shortcutsEnabled = true,
|
||||
this.onReorder,
|
||||
this.showScrollButtons = true,
|
||||
this.wheelScroll = false,
|
||||
this.scrollController,
|
||||
this.minTabWidth = _kMinTileWidth,
|
||||
this.maxTabWidth = _kMaxTileWidth,
|
||||
this.closeButtonVisibility = CloseButtonVisibilityMode.always,
|
||||
this.tabWidthBehavior = TabWidthBehavior.equal,
|
||||
this.header,
|
||||
this.footer,
|
||||
}) : assert(tabs.length == bodies.length),
|
||||
super(key: key);
|
||||
|
||||
/// The index of the tab to be displayed
|
||||
final int currentIndex;
|
||||
|
||||
/// Whether another tab was requested to be displayed
|
||||
final ValueChanged<int>? onChanged;
|
||||
|
||||
/// The tabs to be displayed. This must have the same
|
||||
/// length of [bodies]
|
||||
final List<Tab> tabs;
|
||||
|
||||
/// The bodies of the tabs. This must have the same
|
||||
/// length of [tabs]
|
||||
final List<Widget> bodies;
|
||||
|
||||
/// Called when the new button is pressed or when the
|
||||
/// shortcut `Ctrl + T` is executed.
|
||||
///
|
||||
/// If null, the new button won't be displayed
|
||||
final VoidCallback? onNewPressed;
|
||||
|
||||
/// The icon of the new button
|
||||
final IconData addIconData;
|
||||
|
||||
/// Whether the following shortcuts are enabled:
|
||||
///
|
||||
/// - Ctrl + T to create a new tab
|
||||
/// - Ctrl + F4 or Ctrl + W to close the current tab
|
||||
/// - `Ctrl+1` to `Ctrl+8` to navigate through tabs
|
||||
/// - `Ctrl+9` to navigate to the last tab
|
||||
final bool shortcutsEnabled;
|
||||
|
||||
/// Called when the tabs are reordered. If null,
|
||||
/// reordering is disabled. It's disabled by default.
|
||||
final ReorderCallback? onReorder;
|
||||
|
||||
/// The min width a tab can have. Must not be negative.
|
||||
///
|
||||
/// Default to 80 logical pixels
|
||||
final double minTabWidth;
|
||||
|
||||
/// The max width a tab can have. Must not be negative.
|
||||
///
|
||||
/// Defaults to 240 logical pixels
|
||||
final double maxTabWidth;
|
||||
|
||||
/// Whether the buttons that scroll forward or backward
|
||||
/// should be displayed, if necessary. Defaults to true
|
||||
final bool showScrollButtons;
|
||||
|
||||
/// The [ScrollPosController] used to move tabview to right and left when the
|
||||
/// tabs don't fit the available horizontal space.
|
||||
///
|
||||
/// If null, a [ScrollPosController] is created internally.
|
||||
final ScrollPosController? scrollController;
|
||||
|
||||
/// Indicate if the wheel scroll changes the tabs positions.
|
||||
///
|
||||
/// Defaults to `false`
|
||||
final bool wheelScroll;
|
||||
|
||||
/// Indicates the close button visibility mode
|
||||
final CloseButtonVisibilityMode closeButtonVisibility;
|
||||
|
||||
/// Indicates how a tab will size itself
|
||||
final TabWidthBehavior tabWidthBehavior;
|
||||
|
||||
/// Displayed before all the tabs and buttons.
|
||||
///
|
||||
/// Usually a [Text]
|
||||
final Widget? header;
|
||||
|
||||
/// Displayed after all the tabs and buttons.
|
||||
///
|
||||
/// Usually a [Text]
|
||||
final Widget? footer;
|
||||
|
||||
/// Whenever the new button should be displayed.
|
||||
bool get showNewButton => onNewPressed != null;
|
||||
|
||||
/// Whether reordering is enabled or not. To enable it,
|
||||
/// make sure [widget.onReorder] is not null.
|
||||
bool get isReorderEnabled => onReorder != null;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TabViewState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(IntProperty('currentIndex', currentIndex));
|
||||
properties.add(FlagProperty(
|
||||
'showNewButton',
|
||||
value: showNewButton,
|
||||
ifFalse: 'no new button',
|
||||
));
|
||||
properties.add(IconDataProperty('addIconData', addIconData));
|
||||
properties.add(ObjectFlagProperty(
|
||||
'onChanged',
|
||||
onChanged,
|
||||
ifNull: 'disabled',
|
||||
));
|
||||
properties.add(ObjectFlagProperty(
|
||||
'onNewPressed',
|
||||
onNewPressed,
|
||||
ifNull: 'no new button',
|
||||
));
|
||||
properties.add(IntProperty('tabs', tabs.length));
|
||||
properties.add(FlagProperty(
|
||||
'reorderEnabled',
|
||||
value: isReorderEnabled,
|
||||
ifFalse: 'reorder disabled',
|
||||
));
|
||||
properties.add(FlagProperty(
|
||||
'showScrollButtons',
|
||||
value: showScrollButtons,
|
||||
ifFalse: 'hide scroll buttons',
|
||||
));
|
||||
properties.add(EnumProperty('closeButtonVisibility', closeButtonVisibility,
|
||||
defaultValue: CloseButtonVisibilityMode.always));
|
||||
}
|
||||
}
|
||||
|
||||
class _TabViewState extends State<TabView> {
|
||||
late ScrollPosController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
scrollController = widget.scrollController ??
|
||||
ScrollPosController(
|
||||
itemCount: widget.tabs.length,
|
||||
animationDuration: const Duration(milliseconds: 100),
|
||||
);
|
||||
scrollController.itemCount = widget.tabs.length;
|
||||
scrollController.addListener(_handleScrollUpdate);
|
||||
}
|
||||
|
||||
void _handleScrollUpdate() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TabView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.tabs.length != oldWidget.tabs.length) {
|
||||
scrollController.itemCount = widget.tabs.length;
|
||||
}
|
||||
if (widget.currentIndex != oldWidget.currentIndex) {
|
||||
scrollController.scrollToItem(widget.currentIndex, center: false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.scrollController == null) {
|
||||
// only dispose the local controller
|
||||
scrollController.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _tabBuilder(
|
||||
BuildContext context,
|
||||
int index,
|
||||
double preferredTabWidth,
|
||||
) {
|
||||
final Tab tab = widget.tabs[index];
|
||||
final tabWidget = _Tab(
|
||||
tab,
|
||||
key: ValueKey<int>(index),
|
||||
reorderIndex: widget.isReorderEnabled ? index : null,
|
||||
selected: index == widget.currentIndex,
|
||||
onPressed:
|
||||
widget.onChanged == null ? null : () => widget.onChanged!(index),
|
||||
animationDuration: FluentTheme.of(context).fastAnimationDuration,
|
||||
animationCurve: FluentTheme.of(context).animationCurve,
|
||||
visibilityMode: widget.closeButtonVisibility,
|
||||
tabWidthBehavior: widget.tabWidthBehavior,
|
||||
);
|
||||
final Widget child = GestureDetector(
|
||||
onTertiaryTapUp: (_) => tab.onClosed?.call(),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (widget.tabWidthBehavior == TabWidthBehavior.equal)
|
||||
Expanded(child: tabWidget)
|
||||
else
|
||||
Flexible(child: tabWidget),
|
||||
divider(index),
|
||||
]),
|
||||
);
|
||||
final minWidth = () {
|
||||
switch (widget.tabWidthBehavior) {
|
||||
case TabWidthBehavior.sizeToContent:
|
||||
return null;
|
||||
default:
|
||||
return preferredTabWidth;
|
||||
}
|
||||
}();
|
||||
return AnimatedContainer(
|
||||
key: ValueKey<Tab>(tab),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: minWidth ?? double.infinity,
|
||||
minWidth: minWidth ?? 0.0,
|
||||
),
|
||||
duration: FluentTheme.of(context).fastAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buttonTabBuilder(
|
||||
BuildContext context,
|
||||
Widget icon,
|
||||
VoidCallback? onPressed,
|
||||
String tooltip,
|
||||
) {
|
||||
final item = SizedBox(
|
||||
width: _kButtonWidth,
|
||||
height: _kTileHeight - 10,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2),
|
||||
child: IconButton(
|
||||
icon: icon,
|
||||
onPressed: onPressed,
|
||||
style: ButtonStyle(
|
||||
foregroundColor: ButtonState.resolveWith((states) {
|
||||
if (states.isDisabled || states.isNone) {
|
||||
return FluentTheme.of(context).disabledColor;
|
||||
} else {
|
||||
return FluentTheme.of(context).inactiveColor;
|
||||
}
|
||||
}),
|
||||
backgroundColor: ButtonState.resolveWith((states) {
|
||||
if (states.isDisabled) return Colors.transparent;
|
||||
return ButtonThemeData.uncheckedInputColor(
|
||||
FluentTheme.of(context), states);
|
||||
}),
|
||||
padding: ButtonState.all(
|
||||
const EdgeInsets.symmetric(horizontal: 10),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (onPressed == null) return item;
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: item,
|
||||
);
|
||||
}
|
||||
|
||||
Widget divider(int index) {
|
||||
return SizedBox(
|
||||
height: _kTileHeight,
|
||||
child: Divider(
|
||||
direction: Axis.vertical,
|
||||
style: DividerThemeData(
|
||||
verticalMargin: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration:
|
||||
![widget.currentIndex - 1, widget.currentIndex].contains(index)
|
||||
? null
|
||||
: const BoxDecoration(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
final TextDirection direction = Directionality.of(context);
|
||||
final ThemeData theme = FluentTheme.of(context);
|
||||
final localizations = FluentLocalizations.of(context);
|
||||
|
||||
final headerFooterTextStyle =
|
||||
(theme.typography.bodyLarge ?? const TextStyle());
|
||||
|
||||
Widget tabBar = Column(children: [
|
||||
ScrollConfiguration(
|
||||
behavior: const _TabViewScrollBehavior(),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 4.5),
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
height: _kTileHeight,
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.header != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12.0),
|
||||
child: DefaultTextStyle(
|
||||
style: headerFooterTextStyle,
|
||||
child: widget.header!,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(builder: (context, consts) {
|
||||
final width = consts.biggest.width;
|
||||
assert(
|
||||
width.isFinite,
|
||||
'You can only create a TabView in a box with defined width',
|
||||
);
|
||||
|
||||
final double preferredTabWidth =
|
||||
((width - (widget.showNewButton ? _kButtonWidth : 0)) /
|
||||
widget.tabs.length)
|
||||
.clamp(widget.minTabWidth, widget.maxTabWidth);
|
||||
|
||||
final Widget listView = Listener(
|
||||
onPointerSignal: widget.wheelScroll
|
||||
? (PointerSignalEvent e) {
|
||||
if (e is PointerScrollEvent) {
|
||||
if (e.scrollDelta.dy > 0) {
|
||||
scrollController.forward(
|
||||
align: false,
|
||||
animate: false,
|
||||
);
|
||||
} else {
|
||||
scrollController.backward(
|
||||
align: false,
|
||||
animate: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: ReorderableListView.builder(
|
||||
buildDefaultDragHandles: false,
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
scrollController: scrollController,
|
||||
onReorder: (i, ii) {
|
||||
widget.onReorder?.call(i, ii);
|
||||
},
|
||||
itemCount: widget.tabs.length,
|
||||
proxyDecorator: (child, index, animation) {
|
||||
return child;
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return _tabBuilder(
|
||||
context,
|
||||
index,
|
||||
preferredTabWidth,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
bool scrollable = preferredTabWidth * widget.tabs.length >
|
||||
width - (widget.showNewButton ? _kButtonWidth : 0);
|
||||
|
||||
final bool showScrollButtons =
|
||||
widget.showScrollButtons && scrollable;
|
||||
final backwardButton = _buttonTabBuilder(
|
||||
context,
|
||||
const Icon(FluentIcons.caret_left_solid8, size: 10),
|
||||
!scrollController.canBackward
|
||||
? () {
|
||||
if (direction == TextDirection.ltr) {
|
||||
scrollController.backward();
|
||||
} else {
|
||||
scrollController.forward();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
localizations.scrollTabBackwardLabel,
|
||||
);
|
||||
|
||||
final forwardButton = _buttonTabBuilder(
|
||||
context,
|
||||
const Icon(FluentIcons.caret_right_solid8, size: 10),
|
||||
!scrollController.canForward
|
||||
? () {
|
||||
if (direction == TextDirection.ltr) {
|
||||
scrollController.forward();
|
||||
} else {
|
||||
scrollController.backward();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
localizations.scrollTabForwardLabel,
|
||||
);
|
||||
|
||||
return Row(children: [
|
||||
if (showScrollButtons)
|
||||
direction == TextDirection.ltr
|
||||
? backwardButton
|
||||
: forwardButton,
|
||||
if (scrollable)
|
||||
Expanded(child: listView)
|
||||
else
|
||||
Flexible(child: listView),
|
||||
if (showScrollButtons)
|
||||
direction == TextDirection.ltr
|
||||
? forwardButton
|
||||
: backwardButton,
|
||||
if (widget.showNewButton)
|
||||
_buttonTabBuilder(
|
||||
context,
|
||||
Icon(widget.addIconData, size: 16.0),
|
||||
widget.onNewPressed!,
|
||||
localizations.newTabLabel,
|
||||
),
|
||||
]);
|
||||
}),
|
||||
),
|
||||
if (widget.footer != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0),
|
||||
child: DefaultTextStyle(
|
||||
style: headerFooterTextStyle,
|
||||
child: widget.footer!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.bodies.isNotEmpty)
|
||||
Expanded(child: widget.bodies[widget.currentIndex]),
|
||||
]);
|
||||
if (widget.shortcutsEnabled) {
|
||||
void _onClosePressed() {
|
||||
widget.tabs[widget.currentIndex].onClosed?.call();
|
||||
}
|
||||
|
||||
return FocusScope(
|
||||
autofocus: true,
|
||||
child: CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.f4, control: true):
|
||||
_onClosePressed,
|
||||
const SingleActivator(LogicalKeyboardKey.keyW, control: true):
|
||||
_onClosePressed,
|
||||
const SingleActivator(LogicalKeyboardKey.keyT, control: true): () {
|
||||
widget.onNewPressed?.call();
|
||||
},
|
||||
...Map.fromIterable(
|
||||
List<int>.generate(9, (index) => index),
|
||||
key: (i) {
|
||||
final digits = [
|
||||
LogicalKeyboardKey.digit1,
|
||||
LogicalKeyboardKey.digit2,
|
||||
LogicalKeyboardKey.digit3,
|
||||
LogicalKeyboardKey.digit4,
|
||||
LogicalKeyboardKey.digit5,
|
||||
LogicalKeyboardKey.digit6,
|
||||
LogicalKeyboardKey.digit7,
|
||||
LogicalKeyboardKey.digit8,
|
||||
LogicalKeyboardKey.digit9,
|
||||
];
|
||||
return SingleActivator(digits[i], control: true);
|
||||
},
|
||||
value: (index) {
|
||||
return () {
|
||||
// If it's the last, move to the last tab
|
||||
if (index == 8) {
|
||||
widget.onChanged?.call(widget.tabs.length - 1);
|
||||
} else {
|
||||
if (widget.tabs.length - 1 >= index) {
|
||||
widget.onChanged?.call(index);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
),
|
||||
},
|
||||
child: tabBar,
|
||||
),
|
||||
);
|
||||
}
|
||||
return tabBar;
|
||||
}
|
||||
}
|
||||
|
||||
class Tab {
|
||||
/// Creates a tab.
|
||||
const Tab({
|
||||
this.key,
|
||||
this.icon = const FlutterLogo(),
|
||||
required this.text,
|
||||
this.closeIcon = FluentIcons.chrome_close,
|
||||
this.onClosed,
|
||||
this.semanticLabel,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
/// The leading icon of the tab. [FlutterLogo] is used by default
|
||||
final Widget? icon;
|
||||
|
||||
/// The text of the tab. Usually a [Text] widget
|
||||
final Widget text;
|
||||
|
||||
/// The close icon of the tab. Usually an [IconButton] widget
|
||||
final IconData? closeIcon;
|
||||
|
||||
/// Called when the close button is called or when the
|
||||
/// shortcut `Ctrl + T` or `Ctrl + F4` is executed
|
||||
final VoidCallback? onClosed;
|
||||
|
||||
/// {@macro fluent_ui.controls.inputs.HoverButton.semanticLabel}
|
||||
final String? semanticLabel;
|
||||
}
|
||||
|
||||
class _Tab extends StatefulWidget {
|
||||
const _Tab(
|
||||
this.tab, {
|
||||
Key? key,
|
||||
this.onPressed,
|
||||
required this.selected,
|
||||
this.reorderIndex,
|
||||
this.animationDuration = Duration.zero,
|
||||
this.animationCurve = Curves.linear,
|
||||
required this.visibilityMode,
|
||||
required this.tabWidthBehavior,
|
||||
}) : super(key: key);
|
||||
|
||||
final Tab tab;
|
||||
final bool selected;
|
||||
final VoidCallback? onPressed;
|
||||
final int? reorderIndex;
|
||||
final Duration animationDuration;
|
||||
final Curve animationCurve;
|
||||
final CloseButtonVisibilityMode visibilityMode;
|
||||
final TabWidthBehavior tabWidthBehavior;
|
||||
|
||||
@override
|
||||
__TabState createState() => __TabState();
|
||||
}
|
||||
|
||||
class __TabState extends State<_Tab>
|
||||
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.animationDuration,
|
||||
);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_Tab oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_controller.duration = oldWidget.animationDuration;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final ThemeData theme = FluentTheme.of(context);
|
||||
final localizations = FluentLocalizations.of(context);
|
||||
|
||||
// The text of the tab, if a [Text] widget is used
|
||||
final String? text = () {
|
||||
if (widget.tab.text is Text) {
|
||||
return (widget.tab.text as Text).data ??
|
||||
(widget.tab.text as Text).textSpan?.toPlainText();
|
||||
}
|
||||
}();
|
||||
return HoverButton(
|
||||
key: widget.tab.key,
|
||||
semanticLabel: widget.tab.semanticLabel ?? text,
|
||||
onPressed: widget.onPressed,
|
||||
builder: (context, states) {
|
||||
final primaryBorder = FluentTheme.of(context).focusTheme.primaryBorder;
|
||||
Widget child = Container(
|
||||
height: _kTileHeight,
|
||||
constraints: widget.tabWidthBehavior == TabWidthBehavior.sizeToContent
|
||||
? null
|
||||
: const BoxConstraints(maxWidth: _kMaxTileWidth),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
/// Using a [FocusBorder] here would be more adequate, but it
|
||||
/// seems it disabled the reordering effect. Using this boder
|
||||
/// have the same effect, but make sure to update the code here
|
||||
/// whenever [FocusBorder] code is altered
|
||||
border: Border.all(
|
||||
style: states.isFocused ? BorderStyle.solid : BorderStyle.none,
|
||||
color: primaryBorder?.color ?? Colors.transparent,
|
||||
width: primaryBorder?.width ?? 0.0,
|
||||
),
|
||||
borderRadius: states.isFocused
|
||||
? BorderRadius.zero
|
||||
: const BorderRadius.vertical(top: Radius.circular(4)),
|
||||
color: widget.selected
|
||||
? null
|
||||
: ButtonThemeData.uncheckedInputColor(theme, states),
|
||||
),
|
||||
child: () {
|
||||
final result = ClipRect(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (widget.tab.icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: widget.tab.icon!,
|
||||
),
|
||||
if (widget.tabWidthBehavior != TabWidthBehavior.compact ||
|
||||
(widget.tabWidthBehavior == TabWidthBehavior.compact &&
|
||||
widget.selected))
|
||||
Flexible(
|
||||
fit: widget.tabWidthBehavior == TabWidthBehavior.equal
|
||||
? FlexFit.tight
|
||||
: FlexFit.loose,
|
||||
child: DefaultTextStyle(
|
||||
style: (theme.typography.body ?? const TextStyle())
|
||||
.copyWith(
|
||||
fontSize: 12.0,
|
||||
fontWeight: widget.selected ? FontWeight.w600 : null,
|
||||
),
|
||||
softWrap: false,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.clip,
|
||||
child: widget.tab.text,
|
||||
),
|
||||
),
|
||||
if (widget.tab.closeIcon != null &&
|
||||
(widget.visibilityMode ==
|
||||
CloseButtonVisibilityMode.always ||
|
||||
(widget.visibilityMode ==
|
||||
CloseButtonVisibilityMode.onHover &&
|
||||
states.isHovering)))
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 2.0),
|
||||
child: FocusTheme(
|
||||
data: const FocusThemeData(
|
||||
primaryBorder: BorderSide.none,
|
||||
secondaryBorder: BorderSide.none,
|
||||
),
|
||||
child: Tooltip(
|
||||
message: localizations.closeTabLabel,
|
||||
child: IconButton(
|
||||
icon: Icon(widget.tab.closeIcon),
|
||||
onPressed: widget.tab.onClosed,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (widget.reorderIndex != null) {
|
||||
return ReorderableDragStartListener(
|
||||
index: widget.reorderIndex!,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}(),
|
||||
);
|
||||
if (text != null) {
|
||||
child = Tooltip(
|
||||
message: text,
|
||||
style: const TooltipThemeData(preferBelow: true),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
if (widget.selected) {
|
||||
child = CustomPaint(
|
||||
willChange: false,
|
||||
painter: _TabPainter(theme.brightness.isDark
|
||||
? const Color(0xFF282828)
|
||||
: const Color(0xFFf9f9f9)),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return Semantics(
|
||||
selected: widget.selected,
|
||||
focusable: true,
|
||||
focused: states.isFocused,
|
||||
child: SmallIconButton(child: child),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
class _TabPainter extends CustomPainter {
|
||||
final Color color;
|
||||
|
||||
const _TabPainter(this.color);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final path = Path();
|
||||
const radius = 6.0;
|
||||
path
|
||||
..moveTo(-radius, size.height)
|
||||
..quadraticBezierTo(0, size.height, 0, size.height - radius)
|
||||
..lineTo(0, radius)
|
||||
..quadraticBezierTo(0, 0, radius, 0)
|
||||
..lineTo(size.width - radius, 0)
|
||||
..quadraticBezierTo(size.width, 0, size.width, radius)
|
||||
..lineTo(size.width, size.height - radius)
|
||||
..quadraticBezierTo(
|
||||
size.width,
|
||||
size.height,
|
||||
size.width + radius,
|
||||
size.height,
|
||||
);
|
||||
canvas.drawPath(path, Paint()..color = color);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_TabPainter oldDelegate) => color != oldDelegate.color;
|
||||
|
||||
@override
|
||||
bool shouldRebuildSemantics(_TabPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
class _TabViewScrollBehavior extends ScrollBehavior {
|
||||
const _TabViewScrollBehavior();
|
||||
|
||||
@override
|
||||
Widget buildScrollbar(context, child, details) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
717
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/tree_view.dart
vendored
Normal file
717
dependencies/fluent_ui-3.12.0/lib/src/controls/navigation/tree_view.dart
vendored
Normal file
@@ -0,0 +1,717 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
const double _whiteSpace = 28.0;
|
||||
|
||||
/// Default loading indicator used by [TreeView]
|
||||
const Widget kTreeViewLoadingIndicator = SizedBox(
|
||||
height: 12.0,
|
||||
width: 12.0,
|
||||
child: ProgressRing(
|
||||
strokeWidth: 3.0,
|
||||
),
|
||||
);
|
||||
|
||||
enum TreeViewSelectionMode {
|
||||
/// Selection is disabled
|
||||
none,
|
||||
|
||||
/// When single selection is enabled, only a single item can be selected by
|
||||
/// once.
|
||||
single,
|
||||
|
||||
/// When multiple selection is enabled, a checkbox is shown next to each tree
|
||||
/// view item, and selected items are highlighted. A user can select or
|
||||
/// de-select an item by using the checkbox; clicking the item still causes
|
||||
/// it to be invoked.
|
||||
///
|
||||
/// Selecting or de-selecting a parent item will select or de-select all
|
||||
/// children under that item. If some, but not all, of the children under a
|
||||
/// parent item are selected, the checkbox for the parent node is shown in
|
||||
/// the indeterminate state
|
||||
///
|
||||
/// 
|
||||
multiple,
|
||||
}
|
||||
|
||||
/// The item used by [TreeView] to render tiles
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/tree-view>
|
||||
/// * [TreeView], which render [TreeViewItem]s as tiles
|
||||
/// * [Checkbox], used on multiple selection mode
|
||||
class TreeViewItem with Diagnosticable {
|
||||
final Key? key;
|
||||
|
||||
/// The item leading
|
||||
///
|
||||
/// Usually an [Icon]
|
||||
final Widget? leading;
|
||||
|
||||
/// The item content
|
||||
///
|
||||
/// Usually a [Text]
|
||||
final Widget content;
|
||||
|
||||
/// An optional/arbitrary value associated with the item.
|
||||
///
|
||||
/// For example, a primary key of the row of data that this
|
||||
/// item is associated with.
|
||||
final dynamic value;
|
||||
|
||||
/// The children of this item.
|
||||
final List<TreeViewItem> children;
|
||||
|
||||
/// Whether the item can be collapsable by user-input or not.
|
||||
///
|
||||
/// Defaults to `true`
|
||||
final bool collapsable;
|
||||
|
||||
TreeViewItem? _parent;
|
||||
|
||||
/// Whether the item has any siblings (including itself) that are expandable
|
||||
bool _anyExpandableSiblings;
|
||||
|
||||
/// [TreeViewItem] that owns the [children] collection that this node is part
|
||||
/// of.
|
||||
///
|
||||
/// If null, this is the root node
|
||||
TreeViewItem? get parent => _parent;
|
||||
|
||||
/// Whether the current item is expanded.
|
||||
///
|
||||
/// It has no effect if [children] is empty.
|
||||
bool expanded;
|
||||
|
||||
/// Whether the current item is selected.
|
||||
///
|
||||
/// If [TreeView.selectionMode] is [TreeViewSelectionMode.none], this has no
|
||||
/// effect. If it's [TreeViewSelectionMode.single], this item is going to be
|
||||
/// the only selected item. If it's [TreeViewSelectionMode.multiple], this
|
||||
/// item is going to be one of the selected items
|
||||
bool? selected;
|
||||
|
||||
/// Called when this item is invoked
|
||||
final Future<void> Function(TreeViewItem item)? onInvoked;
|
||||
|
||||
/// The background color of this item.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ButtonThemeData.uncheckedInputColor], which is used by default
|
||||
final ButtonState<Color>? backgroundColor;
|
||||
|
||||
/// Whether this item is visible or not. Used to not lose the item state while
|
||||
/// it's not on the screen
|
||||
bool _visible = true;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro fluent_ui.controls.inputs.HoverButton.semanticLabel}
|
||||
final String? semanticLabel;
|
||||
|
||||
/// Whether the tree view item is loading
|
||||
bool loading = false;
|
||||
|
||||
/// Widget to show when [loading]
|
||||
///
|
||||
/// If null, [TreeView.loadingWidget] is used instead
|
||||
final Widget? loadingWidget;
|
||||
|
||||
/// Whether this item children is loaded lazily
|
||||
final bool lazy;
|
||||
|
||||
/// Creates a tab view item
|
||||
TreeViewItem({
|
||||
this.key,
|
||||
this.leading,
|
||||
required this.content,
|
||||
this.value,
|
||||
this.children = const [],
|
||||
this.collapsable = true,
|
||||
bool? expanded,
|
||||
this.selected = false,
|
||||
this.onInvoked,
|
||||
this.backgroundColor,
|
||||
this.autofocus = false,
|
||||
this.focusNode,
|
||||
this.semanticLabel,
|
||||
this.loadingWidget,
|
||||
this.lazy = false,
|
||||
}) : expanded = expanded ?? children.isNotEmpty,
|
||||
_anyExpandableSiblings = false;
|
||||
|
||||
/// Deep copy constructor that can be used to copy an item and all of
|
||||
/// its child items. Useful if you want to have multiple trees with the
|
||||
/// same items, but with different UX states (e.g., selection, visibility,
|
||||
/// etc.).
|
||||
TreeViewItem.from(TreeViewItem source)
|
||||
: this(
|
||||
key: source.key,
|
||||
leading: source.leading,
|
||||
content: source.content,
|
||||
value: source.value,
|
||||
children: source.children.map((i) => TreeViewItem.from(i)).toList(),
|
||||
collapsable: source.collapsable,
|
||||
expanded: source.expanded,
|
||||
selected: source.selected,
|
||||
onInvoked: source.onInvoked,
|
||||
backgroundColor: source.backgroundColor,
|
||||
autofocus: source.autofocus,
|
||||
focusNode: source.focusNode,
|
||||
semanticLabel: source.semanticLabel,
|
||||
loadingWidget: source.loadingWidget,
|
||||
lazy: source.lazy,
|
||||
);
|
||||
|
||||
/// Whether this node is expandable
|
||||
bool get isExpandable {
|
||||
return lazy || children.isNotEmpty;
|
||||
}
|
||||
|
||||
/// Indicates how far from the root node this child node is.
|
||||
///
|
||||
/// If this is the root node, the depth is 0
|
||||
int get depth {
|
||||
if (parent != null) {
|
||||
int count = 1;
|
||||
TreeViewItem? currentParent = parent!;
|
||||
while (currentParent?.parent != null) {
|
||||
count++;
|
||||
currentParent = currentParent?.parent;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Gets the last parent in the tree, in decrescent order.
|
||||
///
|
||||
/// If this is the root parent, [this] is returned
|
||||
TreeViewItem get lastParent {
|
||||
if (parent != null) {
|
||||
TreeViewItem currentParent = parent!;
|
||||
while (currentParent.parent != null) {
|
||||
if (currentParent.parent != null) currentParent = currentParent.parent!;
|
||||
}
|
||||
return currentParent;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// Executes [callback] for every parent found in the tree
|
||||
void executeForAllParents(ValueChanged<TreeViewItem?> callback) {
|
||||
if (parent == null) return;
|
||||
TreeViewItem? currentParent = parent!;
|
||||
callback(currentParent);
|
||||
while (currentParent?.parent != null) {
|
||||
currentParent = currentParent?.parent;
|
||||
callback(currentParent);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates [selected] based on the [children]s' state
|
||||
void updateSelected() {
|
||||
bool hasNull = false;
|
||||
bool hasFalse = false;
|
||||
bool hasTrue = false;
|
||||
|
||||
for (final child in children.build(assignInternalProperties: false)) {
|
||||
if (child.selected == null) {
|
||||
hasNull = true;
|
||||
} else if (child.selected == false) {
|
||||
hasFalse = true;
|
||||
} else if (child.selected == true) {
|
||||
hasTrue = true;
|
||||
}
|
||||
}
|
||||
|
||||
selected = hasNull || (hasTrue && hasFalse) ? null : hasTrue;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(FlagProperty('hasLeading',
|
||||
value: leading != null, ifFalse: 'no leading'))
|
||||
..add(FlagProperty('hasChildren',
|
||||
value: children.isNotEmpty, ifFalse: 'has children'))
|
||||
..add(FlagProperty('collapsable',
|
||||
value: collapsable, defaultValue: true, ifFalse: 'uncollapsable'))
|
||||
..add(FlagProperty('isRootNode',
|
||||
value: parent == null, ifFalse: 'has parent'))
|
||||
..add(FlagProperty('expanded',
|
||||
value: expanded, defaultValue: true, ifFalse: 'collapsed'))
|
||||
..add(FlagProperty('selected',
|
||||
value: selected, defaultValue: false, ifFalse: 'unselected'))
|
||||
..add(FlagProperty('loading',
|
||||
value: loading, defaultValue: false, ifFalse: 'not loading'));
|
||||
}
|
||||
}
|
||||
|
||||
extension TreeViewItemCollection on List<TreeViewItem> {
|
||||
/// Adds the [TreeViewItem.parent] property to the [TreeViewItem]s
|
||||
/// and calculates other internal properties.
|
||||
List<TreeViewItem> build({
|
||||
TreeViewItem? parent,
|
||||
bool assignInternalProperties = true,
|
||||
}) {
|
||||
if (isNotEmpty) {
|
||||
final List<TreeViewItem> list = [];
|
||||
final anyExpandableSiblings =
|
||||
assignInternalProperties ? any((i) => i.isExpandable) : null;
|
||||
for (final item in [...this]) {
|
||||
if (assignInternalProperties) {
|
||||
item._parent = parent;
|
||||
item._anyExpandableSiblings = anyExpandableSiblings!;
|
||||
}
|
||||
if (parent != null) {
|
||||
item._visible = parent._visible;
|
||||
}
|
||||
if (item._visible) {
|
||||
list.add(item);
|
||||
}
|
||||
final itemAnyExpandableSiblings = assignInternalProperties
|
||||
? item.children.any((i) => i.isExpandable)
|
||||
: null;
|
||||
for (final child in item.children) {
|
||||
// only add the children when it's expanded and visible
|
||||
child._visible = item.expanded && item._visible;
|
||||
if (assignInternalProperties) {
|
||||
child._parent = item;
|
||||
child._anyExpandableSiblings = itemAnyExpandableSiblings!;
|
||||
}
|
||||
if (child._visible) {
|
||||
list.add(child);
|
||||
}
|
||||
if (child.expanded) {
|
||||
list.addAll(child.children.build(parent: child));
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
void executeForAll(ValueChanged<TreeViewItem> callback) {
|
||||
for (final child in this) {
|
||||
callback(child);
|
||||
child.children.executeForAll(callback);
|
||||
}
|
||||
}
|
||||
|
||||
Iterable<TreeViewItem> whereForAll(bool Function(TreeViewItem element) t) {
|
||||
var result = where(t);
|
||||
for (final child in this) {
|
||||
result = result.followedBy(child.children.whereForAll(t));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// A callback that receives a notification that the selection state of
|
||||
/// a TreeView has changed.
|
||||
///
|
||||
/// Used by [TreeView.onSelectionChanged]
|
||||
typedef TreeViewSelectionChangedCallback = Future<void> Function(
|
||||
Iterable<TreeViewItem> selectedItems)?;
|
||||
|
||||
/// The `TreeView` control enables a hierarchical list with expanding and
|
||||
/// collapsing nodes that contain nested items. It can be used to illustrate a
|
||||
/// folder structure or nested relationships in your UI.
|
||||
///
|
||||
/// The tree view uses a combination of indentation and icons to represent the
|
||||
/// nested relationship between parent nodes and child nodes. Collapsed items
|
||||
/// use a chevron pointing to the right, and expanded nodes use a chevron
|
||||
/// pointing down.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// You can include an icon in the [TreeViewItem] template to represent items.
|
||||
/// For example, if you show a file system hierarchy, you could use folder
|
||||
/// icons for the parent items and file icons for the leaf items.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/tree-view>
|
||||
/// * [TreeViewItem], used to render the tiles
|
||||
/// * [Checkbox], used on multiple selection mode
|
||||
class TreeView extends StatefulWidget {
|
||||
/// Creates a tree view.
|
||||
///
|
||||
/// [items] must not be empty
|
||||
const TreeView({
|
||||
Key? key,
|
||||
required this.items,
|
||||
this.selectionMode = TreeViewSelectionMode.none,
|
||||
this.onSelectionChanged,
|
||||
this.onItemInvoked,
|
||||
this.loadingWidget = kTreeViewLoadingIndicator,
|
||||
this.shrinkWrap = true,
|
||||
this.scrollPrimary,
|
||||
this.scrollController,
|
||||
this.cacheExtent,
|
||||
this.itemExtent,
|
||||
this.addRepaintBoundaries = true,
|
||||
this.usePrototypeItem = false,
|
||||
}) : assert(items.length > 0, 'There must be at least one item'),
|
||||
super(key: key);
|
||||
|
||||
/// The items of the tree view.
|
||||
///
|
||||
/// Must not be empty
|
||||
final List<TreeViewItem> items;
|
||||
|
||||
/// The current selection mode.
|
||||
///
|
||||
/// [TreeViewSelectionMode.none] is used by default
|
||||
final TreeViewSelectionMode selectionMode;
|
||||
|
||||
/// Called when an item is invoked
|
||||
final Future<void> Function(TreeViewItem item)? onItemInvoked;
|
||||
|
||||
/// Called when the selection changes. The items that are currently
|
||||
/// selected will be passed to the callback. This could be empty
|
||||
/// if nothing is now selected. If [TreeView.selectionMode] is
|
||||
/// [TreeViewSelectionMode.single] then it will contain exactly
|
||||
/// zero or one items.
|
||||
final TreeViewSelectionChangedCallback onSelectionChanged;
|
||||
|
||||
/// A widget to be shown when a node is loading. Only used if
|
||||
/// [TreeViewItem.loadingWidget] is null.
|
||||
///
|
||||
/// [kTreeViewLoadingIndicator] is used by default
|
||||
final Widget loadingWidget;
|
||||
|
||||
/// {@macro flutter.widgets.scroll_view.shrinkWrap}
|
||||
final bool shrinkWrap;
|
||||
|
||||
/// {@macro flutter.widgets.scroll_view.primary}
|
||||
final bool? scrollPrimary;
|
||||
|
||||
/// {@macro flutter.widgets.scroll_view.controller}
|
||||
final ScrollController? scrollController;
|
||||
|
||||
/// {@macro flutter.rendering.RenderViewportBase.cacheExtent}
|
||||
final double? cacheExtent;
|
||||
|
||||
/// {@macro flutter.widgets.list_view.itemExtent}
|
||||
final double? itemExtent;
|
||||
|
||||
/// Whether to wrap each child in a [RepaintBoundary].
|
||||
///
|
||||
/// Typically, children in a scrolling container are wrapped in repaint
|
||||
/// boundaries so that they do not need to be repainted as the list scrolls.
|
||||
/// If the children are easy to repaint (e.g., solid color blocks or a short
|
||||
/// snippet of text), it might be more efficient to not add a repaint boundary
|
||||
/// and simply repaint the children during scrolling.
|
||||
///
|
||||
/// Defaults to true.
|
||||
final bool addRepaintBoundaries;
|
||||
|
||||
/// Whether or not to give the internal [ListView] a prototypeItem
|
||||
/// based on the first item in the tree view. Set this to true
|
||||
/// to allow the ListView to more efficiently calculate the maximum
|
||||
/// scrolling extent, and it will force the vertical size of each
|
||||
/// item to be the same size as the first item in the tree view.
|
||||
final bool usePrototypeItem;
|
||||
|
||||
@override
|
||||
_TreeViewState createState() => _TreeViewState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(EnumProperty('selectionMode', selectionMode,
|
||||
defaultValue: TreeViewSelectionMode.none))
|
||||
..add(IterableProperty<TreeViewItem>('items', items));
|
||||
}
|
||||
}
|
||||
|
||||
class _TreeViewState extends State<TreeView> {
|
||||
late List<TreeViewItem> items;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
items = widget.items.build();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(TreeView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.items != oldWidget.items) {
|
||||
items = widget.items.build();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
items.clear();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 28.0),
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.vertical,
|
||||
// If shrinkWrap is true, then we default to not using the primary
|
||||
// scroll controller (should not normally need any controller in
|
||||
// this case).
|
||||
primary: widget.scrollPrimary ?? (widget.shrinkWrap ? false : null),
|
||||
controller: widget.scrollController,
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
cacheExtent: widget.cacheExtent,
|
||||
itemExtent: widget.itemExtent,
|
||||
addRepaintBoundaries: widget.addRepaintBoundaries,
|
||||
prototypeItem: widget.usePrototypeItem && items.isNotEmpty
|
||||
? _TreeViewItem(
|
||||
item: items.first,
|
||||
selectionMode: widget.selectionMode,
|
||||
onInvoked: () {},
|
||||
onSelect: () {},
|
||||
onExpandToggle: () {},
|
||||
loadingWidgetFallback: widget.loadingWidget,
|
||||
)
|
||||
: null,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
|
||||
return _TreeViewItem(
|
||||
key: item.key ?? ValueKey<TreeViewItem>(item),
|
||||
item: item,
|
||||
selectionMode: widget.selectionMode,
|
||||
onSelect: () async {
|
||||
final onSelectionChanged = widget.onSelectionChanged;
|
||||
switch (widget.selectionMode) {
|
||||
case TreeViewSelectionMode.single:
|
||||
setState(() {
|
||||
for (final item in items) {
|
||||
item.selected = false;
|
||||
}
|
||||
item.selected = true;
|
||||
});
|
||||
if (onSelectionChanged != null) {
|
||||
await onSelectionChanged([item]);
|
||||
}
|
||||
break;
|
||||
case TreeViewSelectionMode.multiple:
|
||||
setState(() {
|
||||
// if it's root
|
||||
if (item.selected == null || item.selected == false) {
|
||||
item
|
||||
..selected = true
|
||||
..children.executeForAll((item) {
|
||||
item.selected = true;
|
||||
})
|
||||
..executeForAllParents((p) => p?.updateSelected());
|
||||
} else {
|
||||
item
|
||||
..selected = false
|
||||
..children.executeForAll((item) {
|
||||
item.selected = false;
|
||||
})
|
||||
..executeForAllParents((p) => p?.updateSelected());
|
||||
}
|
||||
});
|
||||
if (onSelectionChanged != null) {
|
||||
final selectedItems = widget.items
|
||||
.whereForAll((item) => item.selected ?? false);
|
||||
await onSelectionChanged(selectedItems);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
onExpandToggle: () async {
|
||||
await invokeItem(item);
|
||||
if (item.collapsable) {
|
||||
setState(() {
|
||||
item.expanded = !item.expanded;
|
||||
items = widget.items.build();
|
||||
});
|
||||
}
|
||||
},
|
||||
onInvoked: () => invokeItem(item),
|
||||
loadingWidgetFallback: widget.loadingWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> invokeItem(TreeViewItem item) async {
|
||||
await Future.wait([
|
||||
if (widget.onItemInvoked != null) widget.onItemInvoked!(item),
|
||||
if (item.onInvoked != null) item.onInvoked!(item),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class _TreeViewItem extends StatelessWidget {
|
||||
const _TreeViewItem({
|
||||
Key? key,
|
||||
required this.item,
|
||||
required this.selectionMode,
|
||||
required this.onSelect,
|
||||
required this.onExpandToggle,
|
||||
required this.onInvoked,
|
||||
required this.loadingWidgetFallback,
|
||||
}) : super(key: key);
|
||||
|
||||
final TreeViewItem item;
|
||||
final TreeViewSelectionMode selectionMode;
|
||||
final VoidCallback onSelect;
|
||||
final VoidCallback onExpandToggle;
|
||||
final VoidCallback onInvoked;
|
||||
final Widget loadingWidgetFallback;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!item._visible) return const SizedBox.shrink();
|
||||
final theme = FluentTheme.of(context);
|
||||
final selected = item.selected ?? false;
|
||||
final direction = Directionality.of(context);
|
||||
return HoverButton(
|
||||
onPressed: selectionMode == TreeViewSelectionMode.none
|
||||
? onInvoked
|
||||
: selectionMode == TreeViewSelectionMode.single
|
||||
? () {
|
||||
onSelect();
|
||||
onInvoked();
|
||||
}
|
||||
: onInvoked,
|
||||
autofocus: item.autofocus,
|
||||
focusNode: item.focusNode,
|
||||
semanticLabel: item.semanticLabel,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 2.0,
|
||||
horizontal: 4.0,
|
||||
),
|
||||
builder: (context, states) {
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
height:
|
||||
selectionMode == TreeViewSelectionMode.multiple ? 28.0 : 26.0,
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
start: 12.0 + item.depth * _whiteSpace,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: item.backgroundColor?.resolve(states) ??
|
||||
ButtonThemeData.uncheckedInputColor(
|
||||
theme,
|
||||
[
|
||||
TreeViewSelectionMode.multiple,
|
||||
TreeViewSelectionMode.none
|
||||
].contains(selectionMode)
|
||||
? states
|
||||
: selected && (states.isPressing || states.isNone)
|
||||
? {ButtonStates.hovering}
|
||||
: selected && states.isHovering
|
||||
? {ButtonStates.pressing}
|
||||
: states,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (selectionMode == TreeViewSelectionMode.multiple)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 20.0),
|
||||
child: Checkbox(
|
||||
checked: item.selected,
|
||||
onChanged: (value) {
|
||||
onSelect();
|
||||
onInvoked();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (item.isExpandable)
|
||||
if (item.loading)
|
||||
item.loadingWidget ?? loadingWidgetFallback
|
||||
else
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.deferToChild,
|
||||
onTap: onExpandToggle,
|
||||
child: Icon(
|
||||
item.expanded
|
||||
? FluentIcons.chevron_down
|
||||
: direction == TextDirection.ltr
|
||||
? FluentIcons.chevron_right
|
||||
: FluentIcons.chevron_left,
|
||||
size: 8.0,
|
||||
color: Colors.grey[80],
|
||||
),
|
||||
)
|
||||
else if (item._anyExpandableSiblings)
|
||||
// if some child items are expandable and others are not,
|
||||
// make sure that they line up vertically the same for the
|
||||
// same depth
|
||||
const Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 8.0)),
|
||||
if (item.leading != null)
|
||||
Container(
|
||||
margin: const EdgeInsetsDirectional.only(start: 18.0),
|
||||
width: 20.0,
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(size: 20.0),
|
||||
child: item.leading!,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 18.0),
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: theme.typography.body!.color!.withOpacity(
|
||||
states.isPressing ? 0.7 : 1.0,
|
||||
),
|
||||
),
|
||||
child: item.content,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (selected && selectionMode == TreeViewSelectionMode.single)
|
||||
Positioned(
|
||||
top: 6.0,
|
||||
bottom: 6.0,
|
||||
left: 0.0,
|
||||
child: Container(
|
||||
width: 3.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.accentColor,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
871
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/bottom_sheet.dart
vendored
Normal file
871
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/bottom_sheet.dart
vendored
Normal file
@@ -0,0 +1,871 @@
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
const Duration _bottomSheetEnterDuration = Duration(milliseconds: 250);
|
||||
const Duration _bottomSheetExitDuration = Duration(milliseconds: 200);
|
||||
const Curve _modalBottomSheetCurve = Curves.decelerate;
|
||||
const double _minFlingVelocity = 700.0;
|
||||
const double _closeProgressThreshold = 0.5;
|
||||
|
||||
/// A callback for when the user begins dragging the bottom sheet.
|
||||
///
|
||||
/// Used by [_BottomSheet.onDragStart].
|
||||
typedef _BottomSheetDragStartHandler = void Function(DragStartDetails details);
|
||||
|
||||
/// A callback for when the user stops dragging the bottom sheet.
|
||||
///
|
||||
/// Used by [_BottomSheet.onDragEnd].
|
||||
typedef _BottomSheetDragEndHandler = void Function(
|
||||
DragEndDetails details, {
|
||||
required bool isClosing,
|
||||
});
|
||||
|
||||
/// A fluent design bottom sheet.
|
||||
///
|
||||
/// A bottom sheet is an alternative to a menu or a dialog and
|
||||
/// prevents the user from interacting with the rest of the app. Modal bottom
|
||||
/// sheets can be created and displayed with the [showBottomSheet]
|
||||
/// .
|
||||
///
|
||||
/// The [_BottomSheet] widget itself is rarely used directly. Instead, prefer to
|
||||
/// create a bottom sheet with [showBottomSheet].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [showBottomSheet], which can be used to display a modal bottom
|
||||
/// sheet.
|
||||
class _BottomSheet extends StatefulWidget {
|
||||
/// Creates a bottom sheet.
|
||||
///
|
||||
/// Typically, bottom sheets are created implicitly by [showBottomSheet],
|
||||
/// for modal bottom sheets.
|
||||
const _BottomSheet({
|
||||
Key? key,
|
||||
this.animationController,
|
||||
this.enableDrag = true,
|
||||
this.onDragStart,
|
||||
this.onDragEnd,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.shape,
|
||||
required this.onClosing,
|
||||
required this.builder,
|
||||
}) : assert(elevation == null || elevation >= 0.0),
|
||||
super(key: key);
|
||||
|
||||
/// The animation controller that controls the bottom sheet's entrance and
|
||||
/// exit animations.
|
||||
///
|
||||
/// The BottomSheet widget will manipulate the position of this animation, it
|
||||
/// is not just a passive observer.
|
||||
final AnimationController? animationController;
|
||||
|
||||
/// Called when the bottom sheet begins to close.
|
||||
///
|
||||
/// A bottom sheet might be prevented from closing (e.g., by user
|
||||
/// interaction) even after this callback is called. For this reason, this
|
||||
/// callback might be call multiple times for a given bottom sheet.
|
||||
final VoidCallback onClosing;
|
||||
|
||||
/// A builder for the contents of the sheet.
|
||||
final WidgetBuilder builder;
|
||||
|
||||
/// If true, the bottom sheet can be dragged up and down and dismissed by
|
||||
/// swiping downwards.
|
||||
///
|
||||
/// Default is true.
|
||||
final bool enableDrag;
|
||||
|
||||
/// Called when the user begins dragging the bottom sheet vertically, if
|
||||
/// [enableDrag] is true.
|
||||
///
|
||||
/// Would typically be used to change the bottom sheet animation curve so
|
||||
/// that it tracks the user's finger accurately.
|
||||
final _BottomSheetDragStartHandler? onDragStart;
|
||||
|
||||
/// Called when the user stops dragging the bottom sheet, if [enableDrag]
|
||||
/// is true.
|
||||
///
|
||||
/// Would typically be used to reset the bottom sheet animation curve, so
|
||||
/// that it animates non-linearly. Called before [onClosing] if the bottom
|
||||
/// sheet is closing.
|
||||
final _BottomSheetDragEndHandler? onDragEnd;
|
||||
|
||||
/// The bottom sheet's background color.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// This controls the size of the shadow.
|
||||
///
|
||||
/// Defaults to 0. The value is non-negative.
|
||||
final double? elevation;
|
||||
|
||||
/// The shape of the bottom sheet.
|
||||
final ShapeBorder? shape;
|
||||
|
||||
@override
|
||||
_BottomSheetState createState() => _BottomSheetState();
|
||||
|
||||
/// Creates an [AnimationController] suitable for a
|
||||
/// [_BottomSheet.animationController].
|
||||
///
|
||||
/// This API available as a convenience for a Material compliant bottom sheet
|
||||
/// animation. If alternative animation durations are required, a different
|
||||
/// animation controller could be provided.
|
||||
static AnimationController createAnimationController(TickerProvider vsync) {
|
||||
return AnimationController(
|
||||
duration: _bottomSheetEnterDuration,
|
||||
reverseDuration: _bottomSheetExitDuration,
|
||||
debugLabel: '_BottomSheet',
|
||||
vsync: vsync,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomSheetState extends State<_BottomSheet> {
|
||||
final GlobalKey _childKey = GlobalKey(debugLabel: '_BottomSheet child');
|
||||
|
||||
double get _childHeight {
|
||||
final RenderBox renderBox =
|
||||
_childKey.currentContext!.findRenderObject()! as RenderBox;
|
||||
return renderBox.size.height;
|
||||
}
|
||||
|
||||
bool get _dismissUnderway =>
|
||||
widget.animationController!.status == AnimationStatus.reverse;
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
widget.onDragStart?.call(details);
|
||||
}
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
assert(widget.enableDrag);
|
||||
if (_dismissUnderway) return;
|
||||
widget.animationController!.value -= details.primaryDelta! / _childHeight;
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
assert(widget.enableDrag);
|
||||
if (_dismissUnderway) return;
|
||||
bool isClosing = false;
|
||||
if (details.velocity.pixelsPerSecond.dy > _minFlingVelocity) {
|
||||
final double flingVelocity =
|
||||
-details.velocity.pixelsPerSecond.dy / _childHeight;
|
||||
if (widget.animationController!.value > 0.0) {
|
||||
widget.animationController!.fling(velocity: flingVelocity);
|
||||
}
|
||||
if (flingVelocity < 0.0) {
|
||||
isClosing = true;
|
||||
}
|
||||
} else if (widget.animationController!.value < _closeProgressThreshold) {
|
||||
if (widget.animationController!.value > 0.0) {
|
||||
widget.animationController!.fling(velocity: -1.0);
|
||||
}
|
||||
isClosing = true;
|
||||
} else {
|
||||
widget.animationController!.forward();
|
||||
}
|
||||
|
||||
widget.onDragEnd?.call(
|
||||
details,
|
||||
isClosing: isClosing,
|
||||
);
|
||||
|
||||
if (isClosing) {
|
||||
widget.onClosing();
|
||||
}
|
||||
}
|
||||
|
||||
bool extentChanged(DraggableScrollableNotification notification) {
|
||||
if (notification.extent == notification.minExtent) {
|
||||
widget.onClosing();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final BottomSheetThemeData bottomSheetTheme = BottomSheetTheme.of(context);
|
||||
final Color? color =
|
||||
widget.backgroundColor ?? bottomSheetTheme.backgroundColor;
|
||||
final double elevation =
|
||||
widget.elevation ?? bottomSheetTheme.elevation ?? 0;
|
||||
final ShapeBorder? shape = widget.shape ?? bottomSheetTheme.shape;
|
||||
|
||||
final Widget bottomSheet = PhysicalModel(
|
||||
color: Colors.black,
|
||||
elevation: elevation,
|
||||
borderRadius: shape is RoundedRectangleBorder
|
||||
? shape.borderRadius is BorderRadius
|
||||
? shape.borderRadius as BorderRadius
|
||||
: BorderRadius.zero
|
||||
: BorderRadius.zero,
|
||||
child: Container(
|
||||
key: _childKey,
|
||||
decoration: ShapeDecoration(
|
||||
shape: shape ?? const RoundedRectangleBorder(),
|
||||
color: color,
|
||||
),
|
||||
child: NotificationListener<DraggableScrollableNotification>(
|
||||
onNotification: extentChanged,
|
||||
child: widget.builder(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
return !widget.enableDrag
|
||||
? bottomSheet
|
||||
: GestureDetector(
|
||||
onVerticalDragStart: _handleDragStart,
|
||||
onVerticalDragUpdate: _handleDragUpdate,
|
||||
onVerticalDragEnd: _handleDragEnd,
|
||||
excludeFromSemantics: true,
|
||||
child: bottomSheet,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
|
||||
_ModalBottomSheetLayout(this.progress, this.isScrollControlled);
|
||||
|
||||
final double progress;
|
||||
final bool isScrollControlled;
|
||||
|
||||
@override
|
||||
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||
return BoxConstraints(
|
||||
minWidth: constraints.maxWidth,
|
||||
maxWidth: constraints.maxWidth,
|
||||
minHeight: 0.0,
|
||||
maxHeight: isScrollControlled
|
||||
? constraints.maxHeight
|
||||
: constraints.maxHeight * 9.0 / 16.0,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Offset getPositionForChild(Size size, Size childSize) {
|
||||
return Offset(0.0, size.height - childSize.height * progress);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
|
||||
return progress != oldDelegate.progress;
|
||||
}
|
||||
}
|
||||
|
||||
class _ModalBottomSheet<T> extends StatefulWidget {
|
||||
const _ModalBottomSheet({
|
||||
Key? key,
|
||||
this.route,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.shape,
|
||||
this.isScrollControlled = false,
|
||||
this.enableDrag = true,
|
||||
}) : super(key: key);
|
||||
|
||||
final _ModalBottomSheetRoute<T>? route;
|
||||
final bool isScrollControlled;
|
||||
final Color? backgroundColor;
|
||||
final double? elevation;
|
||||
final ShapeBorder? shape;
|
||||
final bool enableDrag;
|
||||
|
||||
@override
|
||||
_ModalBottomSheetState<T> createState() => _ModalBottomSheetState<T>();
|
||||
}
|
||||
|
||||
class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
|
||||
ParametricCurve<double> animationCurve = _modalBottomSheetCurve;
|
||||
|
||||
String _getRouteLabel(FluentLocalizations localizations) {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return '';
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return localizations.dialogLabel;
|
||||
}
|
||||
}
|
||||
|
||||
void handleDragStart(DragStartDetails details) {
|
||||
// Allow the bottom sheet to track the user's finger accurately.
|
||||
animationCurve = Curves.linear;
|
||||
}
|
||||
|
||||
void handleDragEnd(DragEndDetails details, {bool? isClosing}) {
|
||||
// Allow the bottom sheet to animate smoothly from its current position.
|
||||
animationCurve = _BottomSheetSuspendedCurve(
|
||||
widget.route!.animation!.value,
|
||||
curve: _modalBottomSheetCurve,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
final MediaQueryData mediaQuery = MediaQuery.of(context);
|
||||
final FluentLocalizations localizations = FluentLocalizations.of(context);
|
||||
final String routeLabel = _getRouteLabel(localizations);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: widget.route!.animation!,
|
||||
child: _BottomSheet(
|
||||
animationController: widget.route!._animationController,
|
||||
onClosing: () {
|
||||
if (widget.route!.isCurrent) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
builder: widget.route!.builder!,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
elevation: widget.elevation,
|
||||
shape: widget.shape,
|
||||
enableDrag: widget.enableDrag,
|
||||
onDragStart: handleDragStart,
|
||||
onDragEnd: handleDragEnd,
|
||||
),
|
||||
builder: (BuildContext context, Widget? child) {
|
||||
// Disable the initial animation when accessible navigation is on so
|
||||
// that the semantics are added to the tree at the correct time.
|
||||
final double animationValue = animationCurve.transform(
|
||||
mediaQuery.accessibleNavigation
|
||||
? 1.0
|
||||
: widget.route!.animation!.value,
|
||||
);
|
||||
return Semantics(
|
||||
scopesRoute: true,
|
||||
namesRoute: true,
|
||||
label: routeLabel,
|
||||
explicitChildNodes: true,
|
||||
child: ClipRect(
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: _ModalBottomSheetLayout(
|
||||
animationValue,
|
||||
widget.isScrollControlled,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
|
||||
_ModalBottomSheetRoute({
|
||||
this.builder,
|
||||
required this.capturedThemes,
|
||||
this.barrierLabel,
|
||||
this.backgroundColor,
|
||||
this.elevation,
|
||||
this.shape,
|
||||
this.modalBarrierColor,
|
||||
this.isDismissible = true,
|
||||
this.enableDrag = true,
|
||||
required this.isScrollControlled,
|
||||
RouteSettings? settings,
|
||||
this.transitionAnimationController,
|
||||
}) : super(settings: settings);
|
||||
|
||||
final WidgetBuilder? builder;
|
||||
final CapturedThemes capturedThemes;
|
||||
final bool isScrollControlled;
|
||||
final Color? backgroundColor;
|
||||
final double? elevation;
|
||||
final ShapeBorder? shape;
|
||||
final Color? modalBarrierColor;
|
||||
final bool isDismissible;
|
||||
final bool enableDrag;
|
||||
final AnimationController? transitionAnimationController;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => _bottomSheetEnterDuration;
|
||||
|
||||
@override
|
||||
Duration get reverseTransitionDuration => _bottomSheetExitDuration;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => isDismissible;
|
||||
|
||||
@override
|
||||
final String? barrierLabel;
|
||||
|
||||
@override
|
||||
Color get barrierColor => modalBarrierColor ?? Colors.black.withOpacity(0.54);
|
||||
|
||||
AnimationController? _animationController;
|
||||
|
||||
@override
|
||||
AnimationController createAnimationController() {
|
||||
assert(_animationController == null);
|
||||
_animationController = transitionAnimationController ??
|
||||
_BottomSheet.createAnimationController(navigator!.overlay!);
|
||||
return _animationController!;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||
Animation<double> secondaryAnimation) {
|
||||
// By definition, the bottom sheet is aligned to the bottom of the page
|
||||
// and isn't exposed to the top padding of the MediaQuery.
|
||||
final Widget bottomSheet = MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final BottomSheetThemeData sheetTheme = BottomSheetTheme.of(context);
|
||||
return _ModalBottomSheet<T>(
|
||||
route: this,
|
||||
backgroundColor: backgroundColor ?? sheetTheme.backgroundColor,
|
||||
elevation: elevation ?? sheetTheme.elevation,
|
||||
shape: shape,
|
||||
isScrollControlled: isScrollControlled,
|
||||
enableDrag: enableDrag,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
return capturedThemes.wrap(bottomSheet);
|
||||
}
|
||||
}
|
||||
|
||||
/// A curve that progresses linearly until a specified [startingPoint], at which
|
||||
/// point [curve] will begin. Unlike [Interval], [curve] will not start at zero,
|
||||
/// but will use [startingPoint] as the Y position.
|
||||
///
|
||||
/// For example, if [startingPoint] is set to `0.5`, and [curve] is set to
|
||||
/// [Curves.easeOut], then the bottom-left quarter of the curve will be a
|
||||
/// straight line, and the top-right quarter will contain the entire contents of
|
||||
/// [Curves.easeOut].
|
||||
///
|
||||
/// This is useful in situations where a widget must track the user's finger
|
||||
/// (which requires a linear animation), and afterwards can be flung using a
|
||||
/// curve specified with the [curve] argument, after the finger is released. In
|
||||
/// such a case, the value of [startingPoint] would be the progress of the
|
||||
/// animation at the time when the finger was released.
|
||||
///
|
||||
/// The [startingPoint] and [curve] arguments must not be null.
|
||||
class _BottomSheetSuspendedCurve extends ParametricCurve<double> {
|
||||
/// Creates a suspended curve.
|
||||
const _BottomSheetSuspendedCurve(
|
||||
this.startingPoint, {
|
||||
this.curve = Curves.easeOutCubic,
|
||||
});
|
||||
|
||||
/// The progress value at which [curve] should begin.
|
||||
///
|
||||
/// This defaults to [Curves.easeOutCubic].
|
||||
final double startingPoint;
|
||||
|
||||
/// The curve to use when [startingPoint] is reached.
|
||||
final Curve curve;
|
||||
|
||||
@override
|
||||
double transform(double t) {
|
||||
assert(t >= 0.0 && t <= 1.0);
|
||||
assert(startingPoint >= 0.0 && startingPoint <= 1.0);
|
||||
|
||||
if (t < startingPoint) {
|
||||
return t;
|
||||
}
|
||||
|
||||
if (t == 1.0) {
|
||||
return t;
|
||||
}
|
||||
|
||||
final double curveProgress = (t - startingPoint) / (1 - startingPoint);
|
||||
final double transformed = curve.transform(curveProgress);
|
||||
return lerpDouble(startingPoint, 1, transformed)!;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${describeIdentity(this)}($startingPoint, $curve)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a bottom sheet.
|
||||
///
|
||||
/// A bottom sheet is an alternative to a menu or a dialog and prevents
|
||||
/// the user from interacting with the rest of the app.
|
||||
///
|
||||
/// The `context` argument is used to look up the [Navigator] and [Theme] for
|
||||
/// the bottom sheet. It is only used when the method is called. Its
|
||||
/// corresponding widget can be safely removed from the tree before the bottom
|
||||
/// sheet is closed.
|
||||
///
|
||||
/// The `isScrollControlled` parameter specifies whether this is a route for
|
||||
/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish
|
||||
/// to have a bottom sheet that has a scrollable child such as a [ListView] or
|
||||
/// a [GridView] and have the bottom sheet be draggable, you should set this
|
||||
/// parameter to true.
|
||||
///
|
||||
/// The `useRootNavigator` parameter ensures that the root navigator is used to
|
||||
/// display the [BottomSheet] when set to `true`. This is useful in the case
|
||||
/// that a modal [BottomSheet] needs to be displayed above all other content
|
||||
/// but the caller is inside another [Navigator].
|
||||
///
|
||||
/// The [isDismissible] parameter specifies whether the bottom sheet will be
|
||||
/// dismissed when user taps on the scrim.
|
||||
///
|
||||
/// The [enableDrag] parameter specifies whether the bottom sheet can be
|
||||
/// dragged up and down and dismissed by swiping downwards.
|
||||
///
|
||||
/// The optional [backgroundColor], [elevation], [shape], [clipBehavior] and
|
||||
/// [transitionAnimationController] parameters can be passed in to customize the
|
||||
/// appearance and behavior of modal bottom sheets.
|
||||
///
|
||||
/// The [transitionAnimationController] controls the bottom sheet's entrance and
|
||||
/// exit animations if provided.
|
||||
///
|
||||
/// The optional `routeSettings` parameter sets the [RouteSettings] of the modal bottom sheet
|
||||
/// sheet. This is particularly useful in the case that a user wants to observe
|
||||
/// [PopupRoute]s within a [NavigatorObserver].
|
||||
///
|
||||
/// Returns a `Future` that resolves to the value (if any) that was passed to
|
||||
/// [Navigator.pop] when the modal bottom sheet was closed.
|
||||
///
|
||||
/// {@tool dartpad --template=stateless_widget_scaffold}
|
||||
///
|
||||
/// This example demonstrates how to use `showBottomSheet` to display a
|
||||
/// bottom sheet that obscures the content behind it when a user taps a button.
|
||||
/// It also demonstrates how to close the bottom sheet using the [Navigator]
|
||||
/// when a user taps on a button inside the bottom sheet.
|
||||
///
|
||||
/// ```dart
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Center(
|
||||
/// child: ElevatedButton(
|
||||
/// child: const Text('showBottomSheet'),
|
||||
/// onPressed: () {
|
||||
/// showBottomSheet<void>(
|
||||
/// context: context,
|
||||
/// builder: (BuildContext context) {
|
||||
/// return Container(
|
||||
/// height: 200,
|
||||
/// color: Colors.amber,
|
||||
/// child: Center(
|
||||
/// child: Column(
|
||||
/// mainAxisAlignment: MainAxisAlignment.center,
|
||||
/// mainAxisSize: MainAxisSize.min,
|
||||
/// children: <Widget>[
|
||||
/// const Text('Modal BottomSheet'),
|
||||
/// ElevatedButton(
|
||||
/// child: const Text('Close BottomSheet'),
|
||||
/// onPressed: () => Navigator.pop(context),
|
||||
/// )
|
||||
/// ],
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// },
|
||||
/// );
|
||||
/// },
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
/// See also:
|
||||
///
|
||||
/// * [BottomSheet], a helper widget that implements the fluent ui bottom and top
|
||||
/// sheet
|
||||
/// * [DraggableScrollableSheet], which allows you to create a bottom sheet
|
||||
/// that grows and then becomes scrollable once it reaches its maximum size.
|
||||
Future<T?> showBottomSheet<T>({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
Color? backgroundColor,
|
||||
double? elevation,
|
||||
ShapeBorder? shape,
|
||||
Color? barrierColor,
|
||||
bool isScrollControlled = true,
|
||||
bool useRootNavigator = false,
|
||||
bool isDismissible = true,
|
||||
bool enableDrag = true,
|
||||
RouteSettings? routeSettings,
|
||||
AnimationController? transitionAnimationController,
|
||||
}) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
|
||||
final NavigatorState navigator =
|
||||
Navigator.of(context, rootNavigator: useRootNavigator);
|
||||
return navigator.push(_ModalBottomSheetRoute<T>(
|
||||
builder: builder,
|
||||
capturedThemes:
|
||||
InheritedTheme.capture(from: context, to: navigator.context),
|
||||
isScrollControlled: isScrollControlled,
|
||||
barrierLabel: FluentLocalizations.of(context).modalBarrierDismissLabel,
|
||||
backgroundColor: backgroundColor,
|
||||
elevation: elevation,
|
||||
shape: shape,
|
||||
isDismissible: isDismissible,
|
||||
modalBarrierColor: barrierColor,
|
||||
enableDrag: enableDrag,
|
||||
settings: routeSettings,
|
||||
transitionAnimationController: transitionAnimationController,
|
||||
));
|
||||
}
|
||||
|
||||
class _BottomSheetScrollBehavior extends ScrollBehavior {
|
||||
@override
|
||||
Widget buildScrollbar(context, child, details) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
class BottomSheet extends StatelessWidget {
|
||||
/// Creates a bottom sheet.
|
||||
const BottomSheet({
|
||||
Key? key,
|
||||
this.header,
|
||||
this.showHandle = true,
|
||||
this.showDivider,
|
||||
this.description,
|
||||
this.initialChildSize = 0.5,
|
||||
this.minChildSize = 0.25,
|
||||
this.maxChildSize = 0.85,
|
||||
this.children,
|
||||
}) : assert(
|
||||
header == null || description == null,
|
||||
'You can NOT provide both header and description',
|
||||
),
|
||||
assert(minChildSize >= 0.0),
|
||||
assert(maxChildSize <= 1.0),
|
||||
assert(minChildSize <= initialChildSize),
|
||||
assert(initialChildSize <= maxChildSize),
|
||||
super(key: key);
|
||||
|
||||
/// Whether the handle should be displayed by the bottom sheet.
|
||||
/// Defaults to true
|
||||
final bool showHandle;
|
||||
|
||||
/// Whether the divider should be displayed to divide the [header]
|
||||
/// or [description] from [children].
|
||||
///
|
||||
/// If null, the divider is automatically inserted if [header] or
|
||||
/// [description] are non-null.
|
||||
final bool? showDivider;
|
||||
|
||||
/// The header of the bottom sheet. May be null.
|
||||
final Widget? header;
|
||||
|
||||
/// The description of the bottom sheet. May be null.
|
||||
///
|
||||
/// Typically a [Text]
|
||||
final Widget? description;
|
||||
|
||||
/// The content of the bottom sheet
|
||||
final List<Widget>? children;
|
||||
|
||||
/// The initial fractional value of the parent container's height to use when
|
||||
/// displaying the widget.
|
||||
///
|
||||
/// The default value is `0.5`.
|
||||
final double initialChildSize;
|
||||
|
||||
/// The minimum fractional value of the parent container's height to use when
|
||||
/// displaying the widget.
|
||||
///
|
||||
/// The default value is `0.25`.
|
||||
final double minChildSize;
|
||||
|
||||
/// The maximum fractional value of the parent container's height to use when
|
||||
/// displaying the widget.
|
||||
///
|
||||
/// The default value is `1.0`.
|
||||
final double maxChildSize;
|
||||
|
||||
static Widget buildHandle(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = BottomSheetTheme.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4.0,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.handleColor ?? Colors.grey[80],
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = BottomSheetTheme.of(context);
|
||||
return DraggableScrollableSheet(
|
||||
expand: false,
|
||||
initialChildSize: initialChildSize,
|
||||
minChildSize: minChildSize,
|
||||
maxChildSize: maxChildSize,
|
||||
builder: (context, controller) {
|
||||
return IconTheme.merge(
|
||||
data: IconThemeData(color: theme.handleColor),
|
||||
child: ScrollConfiguration(
|
||||
behavior: _BottomSheetScrollBehavior(),
|
||||
child: ListView(controller: controller, children: [
|
||||
if (showHandle) buildHandle(context),
|
||||
if (header != null) header!,
|
||||
if (description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 12.0,
|
||||
),
|
||||
child: DefaultTextStyle(
|
||||
style: FluentTheme.of(context).typography.caption!,
|
||||
textAlign: TextAlign.center,
|
||||
child: description!,
|
||||
),
|
||||
),
|
||||
if (showDivider != false &&
|
||||
(header != null || description != null))
|
||||
const Divider(
|
||||
style: DividerThemeData(
|
||||
horizontalMargin: EdgeInsets.zero,
|
||||
),
|
||||
),
|
||||
if (children != null) ...children!,
|
||||
]),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [BottomSheet]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [BottomSheet] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class BottomSheetTheme extends InheritedTheme {
|
||||
/// Creates a info bar theme that controls the configurations for
|
||||
/// [BottomSheet].
|
||||
const BottomSheetTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [BottomSheet] widgets.
|
||||
final BottomSheetThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [BottomSheet]s should
|
||||
/// look like, and merges in the current toggle button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required BottomSheetThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return BottomSheetTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static BottomSheetThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme =
|
||||
context.dependOnInheritedWidgetOfExactType<BottomSheetTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).bottomSheetTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [BottomSheetTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.bottomSheetTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// BottomSheetThemeData theme = BottomSheetTheme.of(context);
|
||||
/// ```
|
||||
static BottomSheetThemeData of(BuildContext context) {
|
||||
return BottomSheetThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return BottomSheetTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(BottomSheetTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
||||
|
||||
class BottomSheetThemeData with Diagnosticable {
|
||||
final Color? backgroundColor;
|
||||
final Color? handleColor;
|
||||
final ShapeBorder? shape;
|
||||
final double? elevation;
|
||||
|
||||
const BottomSheetThemeData({
|
||||
this.handleColor,
|
||||
this.backgroundColor,
|
||||
this.shape,
|
||||
this.elevation,
|
||||
});
|
||||
|
||||
factory BottomSheetThemeData.standard(ThemeData style) {
|
||||
final bool isLight = style.brightness.isLight;
|
||||
return BottomSheetThemeData(
|
||||
backgroundColor:
|
||||
isLight ? style.scaffoldBackgroundColor : const Color(0xFF212121),
|
||||
handleColor: isLight ? const Color(0xFF919191) : const Color(0xFF6e6e6e),
|
||||
elevation: 8.0,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static BottomSheetThemeData lerp(
|
||||
BottomSheetThemeData? a,
|
||||
BottomSheetThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return BottomSheetThemeData(
|
||||
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
|
||||
handleColor: Color.lerp(a?.handleColor, b?.handleColor, t),
|
||||
elevation: lerpDouble(a?.elevation, b?.elevation, t),
|
||||
shape: ShapeBorder.lerp(a?.shape, b?.shape, t),
|
||||
);
|
||||
}
|
||||
|
||||
BottomSheetThemeData merge(BottomSheetThemeData? style) {
|
||||
if (style == null) return this;
|
||||
return BottomSheetThemeData(
|
||||
backgroundColor: style.backgroundColor ?? backgroundColor,
|
||||
handleColor: style.handleColor ?? handleColor,
|
||||
shape: style.shape ?? shape,
|
||||
elevation: style.elevation ?? elevation,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ColorProperty('backgroundColor', backgroundColor));
|
||||
properties.add(ColorProperty('handleColor', handleColor));
|
||||
properties.add(DiagnosticsProperty('shape', shape));
|
||||
properties.add(DoubleProperty('elevation', elevation));
|
||||
}
|
||||
}
|
||||
24
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/calendar/calendar_view.dart
vendored
Normal file
24
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/calendar/calendar_view.dart
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
// TODO: Implement calendar view (https://github.com/bdlukaa/fluent_ui/issues/18)
|
||||
|
||||
/// A calendar view lets a user view and interact with a
|
||||
/// calendar that they can navigate by month, year, or
|
||||
/// decade. A user can select a single date or a range of
|
||||
/// dates. It doesn't have a picker surface and the calendar
|
||||
/// is always visible.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
/// - [DatePicker]
|
||||
/// - [TimePicker]
|
||||
/// - [CalendarDatePicker]
|
||||
class CalendarView extends StatelessWidget {
|
||||
const CalendarView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
51
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/card.dart
vendored
Normal file
51
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/card.dart
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class Card extends StatelessWidget {
|
||||
const Card({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.padding = const EdgeInsets.all(12.0),
|
||||
this.backgroundColor,
|
||||
this.elevation = 4.0,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(6.0)),
|
||||
}) : super(key: key);
|
||||
|
||||
/// The card content
|
||||
final Widget child;
|
||||
|
||||
/// The padding around content
|
||||
final EdgeInsets padding;
|
||||
|
||||
/// The background color.
|
||||
///
|
||||
/// If null, [ThemeData.cardColor] is used
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The z-coordinate relative to the parent at which to place this card
|
||||
///
|
||||
/// The valus is non-negative
|
||||
final double elevation;
|
||||
|
||||
/// The rounded corners of this card
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
return PhysicalModel(
|
||||
elevation: elevation,
|
||||
color: Colors.transparent,
|
||||
borderRadius: borderRadius.resolve(Directionality.of(context)),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor ?? theme.cardColor,
|
||||
borderRadius: borderRadius,
|
||||
),
|
||||
padding: padding,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
562
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/commandbar.dart
vendored
Normal file
562
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/commandbar.dart
vendored
Normal file
@@ -0,0 +1,562 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// A card with appropriate margins, padding, and elevation for it to
|
||||
/// contain one or more [CommandBar]s.
|
||||
class CommandBarCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double elevation;
|
||||
final EdgeInsetsGeometry margin;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const CommandBarCard({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.margin = const EdgeInsets.all(0),
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 6.0, vertical: 4.0),
|
||||
this.elevation = 2.0,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: margin,
|
||||
child: Card(
|
||||
padding: padding,
|
||||
elevation: elevation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// How horizontal overflow is handled for the items on the primary area
|
||||
/// of a CommandBar.
|
||||
enum CommandBarOverflowBehavior {
|
||||
/// Will cause items to scroll horizontally.
|
||||
scrolling,
|
||||
|
||||
/// Will expand the size of the CommandBar based on the size of the contained items.
|
||||
noWrap,
|
||||
|
||||
/// Will wrap items onto additional lines as needed.
|
||||
wrap,
|
||||
|
||||
/// Will keep items on one line and clip as needed.
|
||||
clip,
|
||||
|
||||
/// Will dynamically move overflowing items into the "secondary area"
|
||||
/// (shown as a flyout menu when the overflow item is activated).
|
||||
dynamicOverflow,
|
||||
}
|
||||
|
||||
/// Signature of function that will build a [CommandBarItem] with some
|
||||
/// functionality to trigger an action (e.g., a clickable button), and
|
||||
/// it will call the given callback when the action is triggered.
|
||||
typedef CommandBarActionItemBuilder = CommandBarItem Function(
|
||||
VoidCallback onPressed);
|
||||
|
||||
/// Command bars provide quick access to common tasks. This could be
|
||||
/// application-level or page-level commands.
|
||||
///
|
||||
/// A command bar is composed of a series of [CommandBarItem]s, which each could
|
||||
/// be a [CommandBarButton] or a custom [CommandBarItem].
|
||||
///
|
||||
/// If there is not enough horizontal space to display all items, the overflow
|
||||
/// behavior is determined by [overflowBehavior].
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/command-bar>
|
||||
class CommandBar extends StatefulWidget {
|
||||
/// The [CommandBarItem]s that should appear on the primary area.
|
||||
final List<CommandBarItem> primaryItems;
|
||||
|
||||
/// If non-empty, a "overflow item" will appear on the primary area
|
||||
/// (as built by [overflowItemBuilder], or it will be a "more" button
|
||||
/// if [overflowItemBuilder] is null), and when activated, will show a
|
||||
/// flyout containing this list of secondary items.
|
||||
final List<CommandBarItem> secondaryItems;
|
||||
|
||||
/// Allows customization of the "overflow item" that will appear on the
|
||||
/// primary area of the command bar if there are any items in the
|
||||
/// [secondaryItems] (including any items that are dynamically considered
|
||||
/// to be there if [overflowBehavior] is
|
||||
/// [CommandBarOverflowBehavior.dynamicOverflow].)
|
||||
final CommandBarActionItemBuilder? overflowItemBuilder;
|
||||
|
||||
/// Determines what should happen when the items are too wide for the
|
||||
/// primary command bar area. See [CommandBarOverflowBehavior].
|
||||
final CommandBarOverflowBehavior overflowBehavior;
|
||||
|
||||
/// If the width of this widget is less then the indicated amount,
|
||||
/// items in the primary area will be rendered using
|
||||
/// [CommandBarItemDisplayMode.inPrimaryCompact]. If this is `null`
|
||||
/// or the width of this widget is wider, then the items will be rendered
|
||||
/// using [CommandBarItemDisplayMode.inPrimary].
|
||||
final double? compactBreakpointWidth;
|
||||
|
||||
/// If [compactBreakpointWidth] is `null`, then specifies whether or not
|
||||
/// primary items should be displayed in compact mode
|
||||
/// ([CommandBarItemDisplayMode.inPrimaryCompact]) or normal mode
|
||||
/// [CommandBarItemDisplayMode.inPrimary].
|
||||
///
|
||||
/// This can be useful if the CommandBar is used in a setting where
|
||||
/// [compactBreakpointWidth] cannot be used (i.e. because using
|
||||
/// [LayoutBuilder] cannot be used in a context where the intrinsic
|
||||
/// height is also calculated), and you want to specify whether or not
|
||||
/// the primary items should be compact or not.
|
||||
///
|
||||
/// If [compactBreakpointWidth] is not `null` this field is ignored.
|
||||
final bool? isCompact;
|
||||
|
||||
/// The alignment of the items within the command bar across the main axis
|
||||
final MainAxisAlignment mainAxisAlignment;
|
||||
|
||||
/// The alignment of the items within the command bar across the cross axis
|
||||
final CrossAxisAlignment crossAxisAlignment;
|
||||
|
||||
/// The alignment of the overflow item (if displayed) between the end of
|
||||
/// the visible primary items and the end of the boundaries of this widget.
|
||||
/// Only relevant if [overflowBehavior] is
|
||||
/// [CommandBarOverflowBehavior.dynamicOverflow].
|
||||
final MainAxisAlignment overflowItemAlignment;
|
||||
|
||||
final bool _isExpanded;
|
||||
|
||||
const CommandBar({
|
||||
Key? key,
|
||||
required this.primaryItems,
|
||||
this.secondaryItems = const [],
|
||||
this.overflowItemBuilder,
|
||||
this.overflowBehavior = CommandBarOverflowBehavior.dynamicOverflow,
|
||||
this.compactBreakpointWidth,
|
||||
this.isCompact,
|
||||
this.mainAxisAlignment = MainAxisAlignment.start,
|
||||
this.crossAxisAlignment = CrossAxisAlignment.center,
|
||||
this.overflowItemAlignment = MainAxisAlignment.end,
|
||||
}) : _isExpanded = overflowBehavior != CommandBarOverflowBehavior.noWrap,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_CommandBarState createState() => _CommandBarState();
|
||||
}
|
||||
|
||||
class _CommandBarState extends State<CommandBar> {
|
||||
final FlyoutController secondaryFlyoutController = FlyoutController();
|
||||
List<int> dynamicallyHiddenPrimaryItems = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
secondaryFlyoutController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
WrapAlignment _getWrapAlignment() {
|
||||
switch (widget.mainAxisAlignment) {
|
||||
case MainAxisAlignment.start:
|
||||
return WrapAlignment.start;
|
||||
case MainAxisAlignment.end:
|
||||
return WrapAlignment.end;
|
||||
case MainAxisAlignment.center:
|
||||
return WrapAlignment.center;
|
||||
case MainAxisAlignment.spaceBetween:
|
||||
return WrapAlignment.spaceBetween;
|
||||
case MainAxisAlignment.spaceAround:
|
||||
return WrapAlignment.spaceAround;
|
||||
case MainAxisAlignment.spaceEvenly:
|
||||
return WrapAlignment.spaceEvenly;
|
||||
}
|
||||
}
|
||||
|
||||
WrapCrossAlignment _getWrapCrossAlignment() {
|
||||
switch (widget.crossAxisAlignment) {
|
||||
case CrossAxisAlignment.start:
|
||||
return WrapCrossAlignment.start;
|
||||
case CrossAxisAlignment.end:
|
||||
return WrapCrossAlignment.end;
|
||||
case CrossAxisAlignment.center:
|
||||
return WrapCrossAlignment.center;
|
||||
case CrossAxisAlignment.stretch:
|
||||
case CrossAxisAlignment.baseline:
|
||||
throw UnsupportedError(
|
||||
'CommandBar does not support ${widget.crossAxisAlignment}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildForPrimaryMode(
|
||||
BuildContext context, CommandBarItemDisplayMode primaryMode) {
|
||||
final builtItems =
|
||||
widget.primaryItems.map((item) => item.build(context, primaryMode));
|
||||
Widget? overflowWidget;
|
||||
if (widget.secondaryItems.isNotEmpty ||
|
||||
widget.overflowBehavior == CommandBarOverflowBehavior.dynamicOverflow) {
|
||||
void showSecondaryMenu() {
|
||||
secondaryFlyoutController.open();
|
||||
}
|
||||
|
||||
late CommandBarItem overflowItem;
|
||||
if (widget.overflowItemBuilder != null) {
|
||||
overflowItem = widget.overflowItemBuilder!(showSecondaryMenu);
|
||||
} else {
|
||||
overflowItem = CommandBarButton(
|
||||
onPressed: showSecondaryMenu,
|
||||
icon: const Icon(FluentIcons.more),
|
||||
);
|
||||
}
|
||||
|
||||
var allSecondaryItems = [
|
||||
...dynamicallyHiddenPrimaryItems
|
||||
.map((index) => widget.primaryItems[index]),
|
||||
...widget.secondaryItems,
|
||||
];
|
||||
// It's useless if the first item is a separator
|
||||
if (allSecondaryItems.isNotEmpty &&
|
||||
allSecondaryItems.first is CommandBarSeparator) {
|
||||
allSecondaryItems.removeAt(0);
|
||||
}
|
||||
overflowWidget = Flyout(
|
||||
content: (context) => FlyoutContent(
|
||||
constraints: const BoxConstraints(maxWidth: 250.0),
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: allSecondaryItems
|
||||
.map((item) => item.build(
|
||||
context,
|
||||
CommandBarItemDisplayMode.inSecondary,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
controller: secondaryFlyoutController,
|
||||
child: overflowItem.build(context, primaryMode),
|
||||
);
|
||||
}
|
||||
|
||||
late Widget w;
|
||||
switch (widget.overflowBehavior) {
|
||||
case CommandBarOverflowBehavior.scrolling:
|
||||
w = HorizontalScrollView(
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.mainAxisAlignment,
|
||||
crossAxisAlignment: widget.crossAxisAlignment,
|
||||
children: [
|
||||
...builtItems,
|
||||
if (overflowWidget != null) overflowWidget,
|
||||
],
|
||||
),
|
||||
);
|
||||
break;
|
||||
case CommandBarOverflowBehavior.noWrap:
|
||||
w = Row(
|
||||
mainAxisAlignment: widget.mainAxisAlignment,
|
||||
crossAxisAlignment: widget.crossAxisAlignment,
|
||||
children: [
|
||||
...builtItems,
|
||||
if (overflowWidget != null) overflowWidget,
|
||||
],
|
||||
);
|
||||
break;
|
||||
case CommandBarOverflowBehavior.wrap:
|
||||
w = Wrap(
|
||||
alignment: _getWrapAlignment(),
|
||||
crossAxisAlignment: _getWrapCrossAlignment(),
|
||||
children: [
|
||||
...builtItems,
|
||||
if (overflowWidget != null) overflowWidget,
|
||||
],
|
||||
);
|
||||
break;
|
||||
case CommandBarOverflowBehavior.dynamicOverflow:
|
||||
assert(overflowWidget != null);
|
||||
w = DynamicOverflow(
|
||||
alignment: widget.mainAxisAlignment,
|
||||
crossAxisAlignment: widget.crossAxisAlignment,
|
||||
alwaysDisplayOverflowWidget: widget.secondaryItems.isNotEmpty,
|
||||
overflowWidget: overflowWidget!,
|
||||
overflowWidgetAlignment: widget.overflowItemAlignment,
|
||||
overflowChangedCallback: (hiddenItems) {
|
||||
setState(() {
|
||||
// indexes should always be valid
|
||||
assert(() {
|
||||
for (var i = 0; i < hiddenItems.length; i++) {
|
||||
if (hiddenItems[i] < 0 ||
|
||||
hiddenItems[i] >= widget.primaryItems.length) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
dynamicallyHiddenPrimaryItems = hiddenItems;
|
||||
});
|
||||
},
|
||||
children: builtItems.toList(),
|
||||
);
|
||||
break;
|
||||
case CommandBarOverflowBehavior.clip:
|
||||
w = SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: Row(
|
||||
mainAxisAlignment: widget.mainAxisAlignment,
|
||||
crossAxisAlignment: widget.crossAxisAlignment,
|
||||
children: [
|
||||
...builtItems,
|
||||
if (overflowWidget != null) overflowWidget,
|
||||
],
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (widget._isExpanded) {
|
||||
w = Row(children: [Expanded(child: w)]);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.compactBreakpointWidth == null) {
|
||||
final displayMode = (widget.isCompact ?? false)
|
||||
? CommandBarItemDisplayMode.inPrimaryCompact
|
||||
: CommandBarItemDisplayMode.inPrimary;
|
||||
return _buildForPrimaryMode(context, displayMode);
|
||||
} else {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth > widget.compactBreakpointWidth!) {
|
||||
return _buildForPrimaryMode(
|
||||
context, CommandBarItemDisplayMode.inPrimary);
|
||||
} else {
|
||||
return _buildForPrimaryMode(
|
||||
context, CommandBarItemDisplayMode.inPrimaryCompact);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When a [CommandBarItem] is being built, indicates the visual context
|
||||
/// in which the item is being built.
|
||||
enum CommandBarItemDisplayMode {
|
||||
/// The item is displayed in the horizontal area (primary command area)
|
||||
/// of the command bar.
|
||||
///
|
||||
/// The item should be rendered by wrapping content in a
|
||||
/// [CommandBarItemInPrimary] widget.
|
||||
inPrimary,
|
||||
|
||||
/// The item is displayed in the horizontal area (primary command area)
|
||||
/// of the command bar, but it is requested that the item take up less
|
||||
/// horizontal space so that more items may fit without overflow.
|
||||
///
|
||||
/// The item should be rendered by wrapping content in a
|
||||
/// [CommandBarItemInPrimary] widget.
|
||||
inPrimaryCompact,
|
||||
|
||||
/// The item is displayed within the secondary command area (within a
|
||||
/// Flyout as a drop down of the "more" button).
|
||||
///
|
||||
/// Normally you would want to render an item in this visual context as a
|
||||
/// [TappableListTile].
|
||||
inSecondary,
|
||||
}
|
||||
|
||||
/// An individual control displayed within a [CommandBar]. Sub-class this
|
||||
/// to build a new type of widget that appears inside of a command bar.
|
||||
/// It knows how to build an appropriate widget for the given
|
||||
/// [CommandBarItemDisplayMode] during build time.
|
||||
abstract class CommandBarItem with Diagnosticable {
|
||||
final Key? key;
|
||||
|
||||
const CommandBarItem({required this.key});
|
||||
|
||||
/// Builds the final widget for this display mode for this item.
|
||||
/// Sub-classes implement this to build the widget that is appropriate
|
||||
/// for the given display mode.
|
||||
Widget build(BuildContext context, CommandBarItemDisplayMode displayMode);
|
||||
}
|
||||
|
||||
/// Signature of function that can customize the widget returned by
|
||||
/// a CommandBarItem built in the given display mode. Can be useful to
|
||||
/// wrap the widget in a [Tooltip] etc.
|
||||
typedef CommandBarItemWidgetBuilder = Widget Function(
|
||||
BuildContext context, CommandBarItemDisplayMode displayMode, Widget w);
|
||||
|
||||
class CommandBarBuilderItem extends CommandBarItem {
|
||||
/// Function that is called with the built widget of the wrappedItem for
|
||||
/// a given display mode before it is returned. For example, to wrap a
|
||||
/// widget in a [Tooltip].
|
||||
final CommandBarItemWidgetBuilder builder;
|
||||
final CommandBarItem wrappedItem;
|
||||
|
||||
CommandBarBuilderItem({
|
||||
Key? key,
|
||||
required this.builder,
|
||||
required this.wrappedItem,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, CommandBarItemDisplayMode displayMode) {
|
||||
// First, build the widget for the wrappedItem in the given displayMode,
|
||||
// as it is always passed to the callback
|
||||
Widget w = wrappedItem.build(context, displayMode);
|
||||
return builder(context, displayMode, w);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget to help render items that will appear on the primary
|
||||
/// (horizontal) area of a command bar. This widget ensures that
|
||||
/// the child widget has the proper margin so the item has the proper
|
||||
/// minimum height and width expected of a control within the
|
||||
/// primary command area of a [CommandBar].
|
||||
class CommandBarItemInPrimary extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const CommandBarItemInPrimary({Key? key, required this.child})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 3.0),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Buttons are the most common control to put within a [CommandBar].
|
||||
/// They are composed of an (optional) icon and an (optional) label.
|
||||
class CommandBarButton extends CommandBarItem {
|
||||
/// The icon to show in the button (primary area) or menu (secondary area)
|
||||
final Widget? icon;
|
||||
|
||||
/// The label to show in the button (primary area) or menu (secondary area)
|
||||
final Widget? label;
|
||||
|
||||
/// The sub-title to use if this item is shown in the secondary menu
|
||||
final Widget? subtitle;
|
||||
|
||||
/// The trailing widget to use if this item is shown in the secondary menu
|
||||
final Widget? trailing;
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onLongPress;
|
||||
final FocusNode? focusNode;
|
||||
final bool autofocus;
|
||||
|
||||
const CommandBarButton({
|
||||
Key? key,
|
||||
this.icon,
|
||||
this.label,
|
||||
this.subtitle,
|
||||
this.trailing,
|
||||
required this.onPressed,
|
||||
this.onLongPress,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, CommandBarItemDisplayMode displayMode) {
|
||||
switch (displayMode) {
|
||||
case CommandBarItemDisplayMode.inPrimary:
|
||||
case CommandBarItemDisplayMode.inPrimaryCompact:
|
||||
final showIcon = (icon != null);
|
||||
final showLabel = (label != null &&
|
||||
(displayMode == CommandBarItemDisplayMode.inPrimary || !showIcon));
|
||||
return IconButton(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
onLongPress: onLongPress,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
icon: CommandBarItemInPrimary(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showIcon)
|
||||
IconTheme(
|
||||
data: IconTheme.of(context).copyWith(size: 16),
|
||||
child: icon!,
|
||||
),
|
||||
if (showIcon && showLabel) const SizedBox(width: 10),
|
||||
if (showLabel) label!,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
case CommandBarItemDisplayMode.inSecondary:
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0, left: 8.0),
|
||||
child: FlyoutListTile(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
icon: icon,
|
||||
text: label ?? const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Separators for grouping command bar items. Set the color property to
|
||||
/// [Colors.transparent] to render the separator as space. Uses a [Divider]
|
||||
/// under the hood, consequently uses the closest [DividerThemeData].
|
||||
///
|
||||
/// See also:
|
||||
/// * [CommandBar], which is a collection of [CommandBarItem]s.
|
||||
/// * [CommandBarButton], an item for a button with an icon and/or label.
|
||||
class CommandBarSeparator extends CommandBarItem {
|
||||
/// Creates a command bar item separator.
|
||||
const CommandBarSeparator({
|
||||
Key? key,
|
||||
this.color,
|
||||
this.thickness,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Override the color used by the [Divider].
|
||||
final Color? color;
|
||||
|
||||
/// Override the separator thickness.
|
||||
final double? thickness;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, CommandBarItemDisplayMode displayMode) {
|
||||
switch (displayMode) {
|
||||
case CommandBarItemDisplayMode.inPrimary:
|
||||
case CommandBarItemDisplayMode.inPrimaryCompact:
|
||||
return CommandBarItemInPrimary(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minHeight: 28),
|
||||
child: Divider(
|
||||
direction: Axis.vertical,
|
||||
style: DividerThemeData(
|
||||
thickness: thickness,
|
||||
decoration: color != null ? BoxDecoration(color: color) : null,
|
||||
verticalMargin: const EdgeInsets.symmetric(
|
||||
vertical: 0.0,
|
||||
horizontal: 0.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
case CommandBarItemDisplayMode.inSecondary:
|
||||
return Divider(
|
||||
direction: Axis.horizontal,
|
||||
style: DividerThemeData(
|
||||
thickness: thickness,
|
||||
decoration: color != null ? BoxDecoration(color: color) : null,
|
||||
horizontalMargin: const EdgeInsets.only(bottom: 5.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
459
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/dialog.dart
vendored
Normal file
459
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/dialog.dart
vendored
Normal file
@@ -0,0 +1,459 @@
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Dialog controls are modal UI overlays that provide contextual
|
||||
/// app information. They block interactions with the app window
|
||||
/// until being explicitly dismissed. They often request some kind
|
||||
/// of action from the user.
|
||||
///
|
||||
/// To display a dialog, use the function `showDialog`:
|
||||
///
|
||||
/// ```dart
|
||||
/// showDialog(
|
||||
/// context: context,
|
||||
/// builder: (context) {
|
||||
/// return ContentDialog(
|
||||
/// title: Text('Delete file permanently?'),
|
||||
/// content: Text(
|
||||
/// 'If you delete this file, you won\'t be able to recover it. Do you want to delete it?',
|
||||
/// ),
|
||||
/// actions: [
|
||||
/// Button(
|
||||
/// child: Text('Delete'),
|
||||
/// autofocus: true,
|
||||
/// onPressed: () {
|
||||
/// // Delete file here
|
||||
/// },
|
||||
/// ),
|
||||
/// Button(
|
||||
/// child: Text('Cancel'),
|
||||
/// onPressed: () => Navigator.pop(context),
|
||||
/// ),
|
||||
/// ],
|
||||
/// );
|
||||
/// }
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <showDialog>, used to display dialogs on top of the app content
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/dialogs-and-flyouts/dialogs>
|
||||
class ContentDialog extends StatelessWidget {
|
||||
/// Creates a content dialog.
|
||||
const ContentDialog({
|
||||
Key? key,
|
||||
this.title,
|
||||
this.content,
|
||||
this.actions,
|
||||
this.style,
|
||||
this.backgroundDismiss = true,
|
||||
this.constraints = const BoxConstraints(maxWidth: 368),
|
||||
}) : super(key: key);
|
||||
|
||||
/// The title of the dialog. Usually, a [Text] widget
|
||||
final Widget? title;
|
||||
|
||||
/// The content of the dialog. Usually, a [Text] widget
|
||||
final Widget? content;
|
||||
|
||||
/// The actions of the dialog. Usually, a List of [Button]s
|
||||
final List<Widget>? actions;
|
||||
|
||||
/// The style used by this dialog. If non-null, it's mescled with
|
||||
/// [ThemeData.dialogThemeData]
|
||||
final ContentDialogThemeData? style;
|
||||
|
||||
/// Whether the background is dismissible or not.
|
||||
final bool backgroundDismiss;
|
||||
|
||||
/// The constraints of the dialog. It defaults to `BoxConstraints(maxWidth: 368)`
|
||||
final BoxConstraints constraints;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final style = ContentDialogThemeData.standard(FluentTheme.of(
|
||||
context,
|
||||
)).merge(
|
||||
FluentTheme.of(context).dialogTheme.merge(this.style),
|
||||
);
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
decoration: style.decoration,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: style.padding ?? EdgeInsets.zero,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null)
|
||||
Padding(
|
||||
padding: style.titlePadding ?? EdgeInsets.zero,
|
||||
child: DefaultTextStyle(
|
||||
style: style.titleStyle ?? const TextStyle(),
|
||||
child: title!,
|
||||
),
|
||||
),
|
||||
if (content != null)
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: style.bodyPadding ?? EdgeInsets.zero,
|
||||
child: DefaultTextStyle(
|
||||
style: style.bodyStyle ?? const TextStyle(),
|
||||
child: content!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (actions != null)
|
||||
Container(
|
||||
decoration: style.actionsDecoration,
|
||||
padding: style.actionsPadding,
|
||||
child: ButtonTheme.merge(
|
||||
data: style.actionThemeData ?? const ButtonThemeData(),
|
||||
child: () {
|
||||
if (actions!.length == 1) {
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: actions!.first,
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: actions!.map((e) {
|
||||
final index = actions!.indexOf(e);
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsetsDirectional.only(
|
||||
end: index != (actions!.length - 1)
|
||||
? style.actionsSpacing ?? 3
|
||||
: 0,
|
||||
),
|
||||
child: e,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays a Material dialog above the current contents of the app, with
|
||||
/// Material entrance and exit animations, modal barrier color, and modal
|
||||
/// barrier behavior (dialog is dismissible with a tap on the barrier).
|
||||
///
|
||||
/// This function takes a `builder` which typically builds a [Dialog] widget.
|
||||
/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
|
||||
/// returned by the `builder` does not share a context with the location that
|
||||
/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
|
||||
/// custom [StatefulWidget] if the dialog needs to update dynamically.
|
||||
///
|
||||
/// The `context` argument is used to look up the [Navigator] and [Theme] for
|
||||
/// the dialog. It is only used when the method is called. Its corresponding
|
||||
/// widget can be safely removed from the tree before the dialog is closed.
|
||||
///
|
||||
/// The `barrierDismissible` argument is used to indicate whether tapping on the
|
||||
/// barrier will dismiss the dialog. It is `true` by default and can not be `null`.
|
||||
///
|
||||
/// The `barrierColor` argument is used to specify the color of the modal
|
||||
/// barrier that darkens everything below the dialog. If `null` the default color
|
||||
/// `Colors.black54` is used.
|
||||
///
|
||||
/// The `useSafeArea` argument is used to indicate if the dialog should only
|
||||
/// display in 'safe' areas of the screen not used by the operating system
|
||||
/// (see [SafeArea] for more details). It is `true` by default, which means
|
||||
/// the dialog will not overlap operating system areas. If it is set to `false`
|
||||
/// the dialog will only be constrained by the screen size. It can not be `null`.
|
||||
///
|
||||
/// The `useRootNavigator` argument is used to determine whether to push the
|
||||
/// dialog to the [Navigator] furthest from or nearest to the given `context`.
|
||||
/// By default, `useRootNavigator` is `true` and the dialog route created by
|
||||
/// this method is pushed to the root navigator. It can not be `null`.
|
||||
///
|
||||
/// The `routeSettings` argument is passed to [showGeneralDialog],
|
||||
/// see [RouteSettings] for details.
|
||||
///
|
||||
/// If the application has multiple [Navigator] objects, it may be necessary to
|
||||
/// call `Navigator.of(context, rootNavigator: true).pop(result)` to close the
|
||||
/// dialog rather than just `Navigator.pop(context, result)`.
|
||||
///
|
||||
/// Returns a [Future] that resolves to the value (if any) that was passed to
|
||||
/// [Navigator.pop] when the dialog was closed.
|
||||
///
|
||||
/// ### State Restoration in Dialogs
|
||||
///
|
||||
/// Using this method will not enable state restoration for the dialog. In order
|
||||
/// to enable state restoration for a dialog, use [Navigator.restorablePush]
|
||||
/// or [Navigator.restorablePushNamed] with [FluentDialogRoute].
|
||||
///
|
||||
/// For more information about state restoration, see [RestorationManager].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ContentDialog], for dialogs that have a row of buttons below a body.
|
||||
/// * [showGeneralDialog], which allows for customization of the dialog popup.
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/dialogs-and-flyouts/dialogs>
|
||||
Future<T?> showDialog<T extends Object?>({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
RouteTransitionsBuilder transitionBuilder =
|
||||
FluentDialogRoute._defaultTransitionBuilder,
|
||||
Duration? transitionDuration,
|
||||
bool useRootNavigator = true,
|
||||
RouteSettings? routeSettings,
|
||||
String? barrierLabel,
|
||||
Color? barrierColor = const Color(0x8A000000),
|
||||
bool barrierDismissible = false,
|
||||
}) {
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
|
||||
final CapturedThemes themes = InheritedTheme.capture(
|
||||
from: context,
|
||||
to: Navigator.of(
|
||||
context,
|
||||
rootNavigator: useRootNavigator,
|
||||
).context,
|
||||
);
|
||||
|
||||
return Navigator.of(
|
||||
context,
|
||||
rootNavigator: useRootNavigator,
|
||||
).push<T>(FluentDialogRoute<T>(
|
||||
context: context,
|
||||
builder: builder,
|
||||
barrierColor: barrierColor,
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierLabel: FluentLocalizations.of(context).modalBarrierDismissLabel,
|
||||
settings: routeSettings,
|
||||
transitionBuilder: transitionBuilder,
|
||||
transitionDuration: transitionDuration ??
|
||||
FluentTheme.maybeOf(context)?.fastAnimationDuration ??
|
||||
const Duration(milliseconds: 300),
|
||||
themes: themes,
|
||||
));
|
||||
}
|
||||
|
||||
/// A dialog route with Fluent entrance and exit animations.
|
||||
///
|
||||
/// It is used internally by [showDialog] or can be directly pushed
|
||||
/// onto the [Navigator] stack to enable state restoration. See
|
||||
/// [showDialog] for a state restoration app example.
|
||||
///
|
||||
/// This function takes a `builder` which typically builds a [Dialog] widget.
|
||||
/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
|
||||
/// returned by the `builder` does not share a context with the location that
|
||||
/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
|
||||
/// custom [StatefulWidget] if the dialog needs to update dynamically.
|
||||
///
|
||||
/// The `context` argument is used to look up
|
||||
/// [FluentLocalizations.modalBarrierDismissLabel], which provides the
|
||||
/// modal with a localized accessibility label that will be used for the
|
||||
/// modal's barrier. However, a custom `barrierLabel` can be passed in as well.
|
||||
///
|
||||
/// The `barrierDismissible` argument is used to indicate whether tapping on the
|
||||
/// barrier will dismiss the dialog. It is `true` by default and cannot be `null`.
|
||||
///
|
||||
/// The `barrierColor` argument is used to specify the color of the modal
|
||||
/// barrier that darkens everything below the dialog. If `null`, the default
|
||||
/// color `Colors.black54` is used.
|
||||
///
|
||||
/// The `useSafeArea` argument is used to indicate if the dialog should only
|
||||
/// display in 'safe' areas of the screen not used by the operating system
|
||||
/// (see [SafeArea] for more details). It is `true` by default, which means
|
||||
/// the dialog will not overlap operating system areas. If it is set to `false`
|
||||
/// the dialog will only be constrained by the screen size. It can not be `null`.
|
||||
///
|
||||
/// The `settings` argument define the settings for this route. See
|
||||
/// [RouteSettings] for details.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [showDialog], which is a way to display a DialogRoute.
|
||||
/// * [showGeneralDialog], which allows for customization of the dialog popup.
|
||||
class FluentDialogRoute<T> extends RawDialogRoute<T> {
|
||||
/// A dialog route with Material entrance and exit animations,
|
||||
/// modal barrier color
|
||||
FluentDialogRoute({
|
||||
required WidgetBuilder builder,
|
||||
required BuildContext context,
|
||||
CapturedThemes? themes,
|
||||
bool barrierDismissible = false,
|
||||
Color? barrierColor = const Color(0x8A000000),
|
||||
String? barrierLabel,
|
||||
Duration transitionDuration = const Duration(milliseconds: 250),
|
||||
RouteTransitionsBuilder? transitionBuilder = _defaultTransitionBuilder,
|
||||
RouteSettings? settings,
|
||||
}) : super(
|
||||
pageBuilder: (BuildContext context, animation, secondaryAnimation) {
|
||||
final pageChild = Builder(builder: builder);
|
||||
final dialog = themes?.wrap(pageChild) ?? pageChild;
|
||||
return SafeArea(
|
||||
child: Actions(
|
||||
actions: {DismissIntent: _DismissAction(context)},
|
||||
child: FocusScope(
|
||||
autofocus: true,
|
||||
child: dialog,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
barrierDismissible: barrierDismissible,
|
||||
barrierLabel: barrierLabel ??
|
||||
FluentLocalizations.of(context).modalBarrierDismissLabel,
|
||||
barrierColor: barrierColor,
|
||||
transitionDuration: transitionDuration,
|
||||
transitionBuilder: transitionBuilder,
|
||||
settings: settings,
|
||||
);
|
||||
|
||||
static Widget _defaultTransitionBuilder(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return FadeTransition(
|
||||
opacity: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
child: ScaleTransition(
|
||||
scale: CurvedAnimation(
|
||||
parent: Tween<double>(
|
||||
begin: 1,
|
||||
end: 0.85,
|
||||
).animate(animation),
|
||||
curve: Curves.easeOut,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DismissAction extends DismissAction {
|
||||
_DismissAction(this.context);
|
||||
|
||||
final BuildContext context;
|
||||
|
||||
@override
|
||||
void invoke(covariant DismissIntent intent) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ContentDialogThemeData {
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? titlePadding;
|
||||
final EdgeInsetsGeometry? bodyPadding;
|
||||
|
||||
final Decoration? decoration;
|
||||
final Color? barrierColor;
|
||||
|
||||
final ButtonThemeData? actionThemeData;
|
||||
final double? actionsSpacing;
|
||||
final Decoration? actionsDecoration;
|
||||
final EdgeInsetsGeometry? actionsPadding;
|
||||
|
||||
final TextStyle? titleStyle;
|
||||
final TextStyle? bodyStyle;
|
||||
|
||||
const ContentDialogThemeData({
|
||||
this.decoration,
|
||||
this.barrierColor,
|
||||
this.titlePadding,
|
||||
this.bodyPadding,
|
||||
this.padding,
|
||||
this.actionsSpacing,
|
||||
this.actionThemeData,
|
||||
this.actionsDecoration,
|
||||
this.actionsPadding,
|
||||
this.titleStyle,
|
||||
this.bodyStyle,
|
||||
});
|
||||
|
||||
factory ContentDialogThemeData.standard(ThemeData style) {
|
||||
return ContentDialogThemeData(
|
||||
decoration: BoxDecoration(
|
||||
color: style.menuColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: kElevationToShadow[6],
|
||||
),
|
||||
padding: const EdgeInsets.all(20),
|
||||
titlePadding: const EdgeInsets.only(bottom: 12),
|
||||
actionsSpacing: 10,
|
||||
actionsDecoration: BoxDecoration(
|
||||
color: style.micaBackgroundColor,
|
||||
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)),
|
||||
boxShadow: kElevationToShadow[1],
|
||||
),
|
||||
actionsPadding: const EdgeInsets.all(20),
|
||||
barrierColor: Colors.grey[200].withOpacity(0.8),
|
||||
titleStyle: style.typography.title,
|
||||
bodyStyle: style.typography.body,
|
||||
);
|
||||
}
|
||||
|
||||
static ContentDialogThemeData lerp(
|
||||
ContentDialogThemeData? a,
|
||||
ContentDialogThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return ContentDialogThemeData(
|
||||
decoration: Decoration.lerp(a?.decoration, b?.decoration, t),
|
||||
barrierColor: Color.lerp(a?.barrierColor, b?.barrierColor, t),
|
||||
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
|
||||
bodyPadding: EdgeInsetsGeometry.lerp(a?.bodyPadding, b?.bodyPadding, t),
|
||||
titlePadding:
|
||||
EdgeInsetsGeometry.lerp(a?.titlePadding, b?.titlePadding, t),
|
||||
actionsSpacing: lerpDouble(a?.actionsSpacing, b?.actionsSpacing, t),
|
||||
actionThemeData:
|
||||
ButtonThemeData.lerp(a?.actionThemeData, b?.actionThemeData, t),
|
||||
actionsDecoration:
|
||||
Decoration.lerp(a?.actionsDecoration, b?.actionsDecoration, t),
|
||||
actionsPadding:
|
||||
EdgeInsetsGeometry.lerp(a?.actionsPadding, b?.actionsPadding, t),
|
||||
titleStyle: TextStyle.lerp(a?.titleStyle, b?.titleStyle, t),
|
||||
bodyStyle: TextStyle.lerp(a?.bodyStyle, b?.bodyStyle, t),
|
||||
);
|
||||
}
|
||||
|
||||
ContentDialogThemeData merge(ContentDialogThemeData? style) {
|
||||
if (style == null) return this;
|
||||
return ContentDialogThemeData(
|
||||
decoration: style.decoration ?? decoration,
|
||||
barrierColor: style.barrierColor ?? barrierColor,
|
||||
padding: style.padding ?? padding,
|
||||
bodyPadding: style.bodyPadding ?? bodyPadding,
|
||||
titlePadding: style.titlePadding ?? titlePadding,
|
||||
actionsSpacing: style.actionsSpacing ?? actionsSpacing,
|
||||
actionThemeData: style.actionThemeData ?? actionThemeData,
|
||||
actionsDecoration: style.actionsDecoration ?? actionsDecoration,
|
||||
actionsPadding: style.actionsPadding ?? actionsPadding,
|
||||
titleStyle: style.titleStyle ?? titleStyle,
|
||||
bodyStyle: style.bodyStyle ?? bodyStyle,
|
||||
);
|
||||
}
|
||||
}
|
||||
295
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/expander.dart
vendored
Normal file
295
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/expander.dart
vendored
Normal file
@@ -0,0 +1,295 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// The expander direction
|
||||
enum ExpanderDirection {
|
||||
/// Whether the [Expander] expands down
|
||||
down,
|
||||
|
||||
/// Whether the [Expander] expands up
|
||||
up,
|
||||
}
|
||||
|
||||
/// The [Expander] control lets you show or hide less important content
|
||||
/// that's related to a piece of primary content that's always visible.
|
||||
/// Items contained in the Header are always visible. The user can expand
|
||||
/// and collapse the Content area, where secondary content is displayed,
|
||||
/// by interacting with the header. When the content area is expanded,
|
||||
/// it pushes other UI elements out of the way; it does not overlay other
|
||||
/// UI. The Expander can expand upwards or downwards.
|
||||
///
|
||||
/// Both the Header and Content areas can contain any content, from simple
|
||||
/// text to complex UI layouts. For example, you can use the control to show
|
||||
/// additional options for an item.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/expander>
|
||||
class Expander extends StatefulWidget {
|
||||
/// Creates an expander
|
||||
const Expander({
|
||||
Key? key,
|
||||
this.leading,
|
||||
required this.header,
|
||||
required this.content,
|
||||
this.icon,
|
||||
this.trailing,
|
||||
this.animationCurve,
|
||||
this.animationDuration,
|
||||
this.direction = ExpanderDirection.down,
|
||||
this.initiallyExpanded = false,
|
||||
this.onStateChanged,
|
||||
this.headerHeight = 48.0,
|
||||
this.headerBackgroundColor,
|
||||
this.contentBackgroundColor,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The leading widget.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Icon]
|
||||
/// * [RadioButton]
|
||||
/// * [Checkbox]
|
||||
final Widget? leading;
|
||||
|
||||
/// The expander header
|
||||
///
|
||||
/// Usually a [Text]
|
||||
final Widget header;
|
||||
|
||||
/// The expander content
|
||||
///
|
||||
/// You can use complex, interactive UI as the content of the
|
||||
/// Expander, including nested Expander controls in the content
|
||||
/// of a parent Expander as shown here.
|
||||
///
|
||||
/// 
|
||||
final Widget content;
|
||||
|
||||
/// The icon of the toggle button.
|
||||
final Widget? icon;
|
||||
|
||||
/// The trailing widget. It's positioned at the right of [header]
|
||||
/// and at the left of [icon].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ToggleSwitch]
|
||||
final Widget? trailing;
|
||||
|
||||
/// The expand-collapse animation duration. If null, defaults to
|
||||
/// [FluentTheme.fastAnimationDuration]
|
||||
final Duration? animationDuration;
|
||||
|
||||
/// The expand-collapse animation curve. If null, defaults to
|
||||
/// [FluentTheme.animationCurve]
|
||||
final Curve? animationCurve;
|
||||
|
||||
/// The expand direction. Defaults to [ExpanderDirection.down]
|
||||
final ExpanderDirection direction;
|
||||
|
||||
/// Whether the [Expander] is initially expanded. Defaults to `false`
|
||||
final bool initiallyExpanded;
|
||||
|
||||
/// A callback called when the current state is changed. `true` when
|
||||
/// open and `false` when closed.
|
||||
final ValueChanged<bool>? onStateChanged;
|
||||
|
||||
/// The height of the header.
|
||||
///
|
||||
/// Defaults to 48.0
|
||||
final double headerHeight;
|
||||
|
||||
/// The background color of the header.
|
||||
final ButtonState<Color>? headerBackgroundColor;
|
||||
|
||||
/// The content color of the header
|
||||
final Color? contentBackgroundColor;
|
||||
|
||||
@override
|
||||
ExpanderState createState() => ExpanderState();
|
||||
}
|
||||
|
||||
class ExpanderState extends State<Expander>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late ThemeData _theme;
|
||||
|
||||
bool? _open;
|
||||
bool get open => _open ?? false;
|
||||
set open(bool value) {
|
||||
if (_open != value) _handlePressed();
|
||||
}
|
||||
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: widget.animationDuration ?? const Duration(milliseconds: 150),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_theme = FluentTheme.of(context);
|
||||
if (_open == null) {
|
||||
_open = !widget.initiallyExpanded;
|
||||
open = widget.initiallyExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePressed() {
|
||||
if (open) {
|
||||
_controller.animateTo(
|
||||
0.0,
|
||||
duration: widget.animationDuration ?? _theme.fastAnimationDuration,
|
||||
curve: widget.animationCurve ?? _theme.animationCurve,
|
||||
);
|
||||
_open = false;
|
||||
} else {
|
||||
_controller.animateTo(
|
||||
1.0,
|
||||
duration: widget.animationDuration ?? _theme.fastAnimationDuration,
|
||||
curve: widget.animationCurve ?? _theme.animationCurve,
|
||||
);
|
||||
_open = true;
|
||||
}
|
||||
widget.onStateChanged?.call(open);
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
bool get _isDown => widget.direction == ExpanderDirection.down;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static Color backgroundColor(ThemeData style, Set<ButtonStates> states) {
|
||||
if (style.brightness == Brightness.light) {
|
||||
if (states.isDisabled) return style.disabledColor;
|
||||
if (states.isPressing) return const Color(0xFFf9f9f9).withOpacity(0.2);
|
||||
if (states.isHovering) return const Color(0xFFf9f9f9).withOpacity(0.4);
|
||||
return Colors.white.withOpacity(0.7);
|
||||
} else {
|
||||
if (states.isDisabled) return style.disabledColor;
|
||||
if (states.isPressing) return Colors.white.withOpacity(0.03);
|
||||
if (states.isHovering) return Colors.white.withOpacity(0.082);
|
||||
return Colors.white.withOpacity(0.05);
|
||||
}
|
||||
}
|
||||
|
||||
static Color borderColor(ThemeData style, Set<ButtonStates> states) {
|
||||
if (style.brightness == Brightness.light) {
|
||||
if (states.isHovering && !states.isPressing) {
|
||||
return const Color(0xFF212121).withOpacity(0.22);
|
||||
}
|
||||
return const Color(0xFF212121).withOpacity(0.17);
|
||||
} else {
|
||||
if (states.isPressing) return Colors.white.withOpacity(0.062);
|
||||
if (states.isHovering) return Colors.white.withOpacity(0.02);
|
||||
return Colors.black.withOpacity(0.52);
|
||||
}
|
||||
}
|
||||
|
||||
static const double borderSize = 0.5;
|
||||
static final Color darkBorderColor = Colors.black.withOpacity(0.8);
|
||||
|
||||
static const Duration expanderAnimationDuration = Duration(milliseconds: 70);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final children = [
|
||||
HoverButton(
|
||||
onPressed: _handlePressed,
|
||||
builder: (context, states) {
|
||||
return AnimatedContainer(
|
||||
duration: expanderAnimationDuration,
|
||||
height: widget.headerHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: widget.headerBackgroundColor?.resolve(states) ??
|
||||
backgroundColor(_theme, states),
|
||||
border: Border.all(
|
||||
width: borderSize,
|
||||
color: borderColor(_theme, states),
|
||||
),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: const Radius.circular(4.0),
|
||||
bottom: Radius.circular(open ? 0.0 : 4.0),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.only(start: 16.0),
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (widget.leading != null)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 10.0),
|
||||
child: widget.leading!,
|
||||
),
|
||||
Expanded(child: widget.header),
|
||||
if (widget.trailing != null)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 20.0),
|
||||
child: widget.trailing!,
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsetsDirectional.only(
|
||||
start: widget.trailing != null ? 8.0 : 20.0,
|
||||
end: 8.0,
|
||||
top: 8.0,
|
||||
bottom: 8.0,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(_theme, states),
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: widget.icon ??
|
||||
RotationTransition(
|
||||
turns: Tween<double>(begin: 0, end: 0.5)
|
||||
.animate(_controller),
|
||||
child: Icon(
|
||||
_isDown
|
||||
? FluentIcons.chevron_down
|
||||
: FluentIcons.chevron_up,
|
||||
size: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizeTransition(
|
||||
sizeFactor: _controller,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: borderSize,
|
||||
color: borderColor(_theme, {ButtonStates.none}),
|
||||
),
|
||||
color: widget.contentBackgroundColor ??
|
||||
backgroundColor(_theme, {ButtonStates.none}),
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(bottom: Radius.circular(4.0)),
|
||||
),
|
||||
child: widget.content,
|
||||
),
|
||||
),
|
||||
];
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: _isDown ? children : children.reversed.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
208
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/flyout/content.dart
vendored
Normal file
208
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/flyout/content.dart
vendored
Normal file
@@ -0,0 +1,208 @@
|
||||
part of 'flyout.dart';
|
||||
|
||||
/// The content of the flyout.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Flyout], which is a light dismiss container that can show arbitrary UI
|
||||
/// as its content
|
||||
/// * [FlyoutListTile],
|
||||
class FlyoutContent extends StatelessWidget {
|
||||
/// Creates a flyout content
|
||||
const FlyoutContent({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.color,
|
||||
this.shape,
|
||||
this.padding = const EdgeInsets.all(8.0),
|
||||
this.shadowColor = Colors.black,
|
||||
this.elevation = 8,
|
||||
this.constraints,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
|
||||
/// The background color of the box.
|
||||
final Color? color;
|
||||
|
||||
/// The shape to fill the [color] of the box.
|
||||
final ShapeBorder? shape;
|
||||
|
||||
/// Empty space to inscribe around the [child]
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
/// The shadow color.
|
||||
final Color shadowColor;
|
||||
|
||||
/// The z-coordinate relative to the box at which to place this physical
|
||||
/// object.
|
||||
final double elevation;
|
||||
|
||||
/// Additional constraints to apply to the child.
|
||||
final BoxConstraints? constraints;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final ThemeData theme = FluentTheme.of(context);
|
||||
return PhysicalModel(
|
||||
elevation: elevation,
|
||||
color: Colors.transparent,
|
||||
shadowColor: shadowColor,
|
||||
child: Container(
|
||||
constraints: constraints,
|
||||
decoration: ShapeDecoration(
|
||||
color: color ?? theme.menuColor,
|
||||
shape: shape ??
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
side: BorderSide(
|
||||
width: 0.25,
|
||||
color: theme.inactiveBackgroundColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: padding,
|
||||
child: DefaultTextStyle(
|
||||
style: theme.typography.body ?? const TextStyle(),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A tile that is used inside of [FlyoutContent]
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Flyout]
|
||||
/// * [FlyoutContent]
|
||||
class FlyoutListTile extends StatelessWidget {
|
||||
/// Creates a flyout list tile.
|
||||
const FlyoutListTile({
|
||||
Key? key,
|
||||
this.onPressed,
|
||||
this.tooltip,
|
||||
this.icon,
|
||||
required this.text,
|
||||
this.trailing,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.semanticLabel,
|
||||
this.margin = const EdgeInsets.only(bottom: 5.0),
|
||||
this.selected = false,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// The tile tooltip text
|
||||
final String? tooltip;
|
||||
|
||||
/// The leading widget.
|
||||
///
|
||||
/// Usually an [Icon]
|
||||
final Widget? icon;
|
||||
|
||||
/// The title widget.
|
||||
///
|
||||
/// Usually a [Text]
|
||||
final Widget text;
|
||||
|
||||
/// The leading widget.
|
||||
final Widget? trailing;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
/// {@macro fluent_ui.controls.inputs.HoverButton.semanticLabel}
|
||||
final String? semanticLabel;
|
||||
|
||||
final EdgeInsetsGeometry margin;
|
||||
|
||||
final bool selected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
return HoverButton(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
semanticLabel: semanticLabel,
|
||||
builder: (context, states) {
|
||||
final theme = FluentTheme.of(context);
|
||||
final radius = BorderRadius.circular(4.0);
|
||||
|
||||
if (selected) {
|
||||
states = {ButtonStates.hovering};
|
||||
}
|
||||
|
||||
Widget content = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ButtonThemeData.uncheckedInputColor(theme, states),
|
||||
borderRadius: radius,
|
||||
),
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
top: 4.0,
|
||||
bottom: 4.0,
|
||||
start: 10.0,
|
||||
end: 8.0,
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
if (icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 10.0),
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(size: 16.0),
|
||||
child: icon!,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 10.0),
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
inherit: false,
|
||||
fontSize: 14.0,
|
||||
letterSpacing: -0.15,
|
||||
color: theme.inactiveColor,
|
||||
),
|
||||
child: text,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (trailing != null)
|
||||
DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
inherit: false,
|
||||
fontSize: 12.0,
|
||||
color: theme.borderInputColor,
|
||||
height: 0.7,
|
||||
),
|
||||
child: trailing!,
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
if (tooltip != null) {
|
||||
content = Tooltip(message: tooltip, child: content);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: margin,
|
||||
child: FocusBorder(
|
||||
focused: states.isFocused,
|
||||
renderOutside: true,
|
||||
style: FocusThemeData(borderRadius: radius),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
44
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/flyout/controller.dart
vendored
Normal file
44
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/flyout/controller.dart
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class FlyoutController extends ChangeNotifier with Diagnosticable {
|
||||
bool _open = false;
|
||||
|
||||
/// Whether the flyout is open
|
||||
bool get isOpen => _open;
|
||||
|
||||
/// Whether the flyout is closed
|
||||
bool get isClosed => !_open;
|
||||
|
||||
/// Opens the flyout. Has no effect if it's already open
|
||||
void open() {
|
||||
_open = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Closes the flyout. Has no effect if it's already closed
|
||||
void close() {
|
||||
_open = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Toggles the flyout. If it's opened, it'll be closed. Otherwise, it'll be
|
||||
/// opened.
|
||||
void toggle() {
|
||||
if (isOpen) {
|
||||
close();
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(FlagProperty(
|
||||
'open',
|
||||
value: isOpen,
|
||||
ifFalse: 'closed',
|
||||
defaultValue: false,
|
||||
));
|
||||
}
|
||||
}
|
||||
291
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/flyout/flyout.dart
vendored
Normal file
291
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/flyout/flyout.dart
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
import '../../../utils/popup.dart';
|
||||
|
||||
export 'controller.dart';
|
||||
|
||||
part 'content.dart';
|
||||
part 'menu.dart';
|
||||
|
||||
const kDefaultLongHoverDuration = Duration(milliseconds: 400);
|
||||
|
||||
/// Where the flyout will be placed vertically relativelly the child
|
||||
enum FlyoutPosition {
|
||||
/// The flyout will be above the child, if there is enough space available
|
||||
above,
|
||||
|
||||
/// The flyout will be below the child, if there is enough space available
|
||||
below,
|
||||
|
||||
/// The flyout will be by the side of the child, if there is enough space
|
||||
/// available
|
||||
side,
|
||||
}
|
||||
|
||||
/// How the flyout will be placed relatively to the child
|
||||
enum FlyoutPlacement {
|
||||
/// The flyout will be placed on the start point of the child.
|
||||
///
|
||||
/// If the current directionality it's left-to-right, it's left. Otherwise,
|
||||
/// it's right
|
||||
start,
|
||||
|
||||
/// The flyout will be placed on the center of the child.
|
||||
center,
|
||||
|
||||
/// The flyout will be placed on the end point of the child.
|
||||
///
|
||||
/// If the current directionality it's left-to-right, it's right. Otherwise,
|
||||
/// it's left
|
||||
end,
|
||||
|
||||
/// The flyout will be streched and positioned on the whole app window. A
|
||||
/// [Align] can be used to align the flyout to a certain place of the
|
||||
/// window.
|
||||
full,
|
||||
}
|
||||
|
||||
/// How the flyout will be opened by the end-user
|
||||
enum FlyoutOpenMode {
|
||||
/// The flyout will not be opened automatically
|
||||
none,
|
||||
|
||||
/// The flyout will opened when the user hover the child
|
||||
hover,
|
||||
|
||||
/// The flyout will be opened when the user long hover the child
|
||||
longHover,
|
||||
|
||||
/// The flyout will opened when the user press the child
|
||||
press,
|
||||
|
||||
/// The flyout will be opened when the user long-press the child
|
||||
longPress,
|
||||
}
|
||||
|
||||
/// A flyout is a light dismiss container that can show arbitrary UI as its
|
||||
/// content. Flyouts can contain other flyouts or context menus to create a
|
||||
/// nested experience.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/dialogs-and-flyouts/flyouts>
|
||||
/// * [FlyoutContent]
|
||||
/// * [PopUp], which is used by this under the hood to perform the flyout
|
||||
/// positioning
|
||||
/// * [Tooltip], which is a short description linked to a widget in form of an
|
||||
/// overlay
|
||||
class Flyout extends StatefulWidget {
|
||||
/// Creates a flyout.
|
||||
const Flyout({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.content,
|
||||
this.controller,
|
||||
this.verticalOffset = 24,
|
||||
this.horizontalOffset = 10.0,
|
||||
this.placement = FlyoutPlacement.center,
|
||||
this.openMode = FlyoutOpenMode.none,
|
||||
this.position = FlyoutPosition.above,
|
||||
this.longHoverDuration = kDefaultLongHoverDuration,
|
||||
this.onOpen,
|
||||
this.onClose,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The child that will be attached to the flyout.
|
||||
final Widget child;
|
||||
|
||||
/// The content that will be displayed on the flyout.
|
||||
///
|
||||
/// Usually a [FlyoutContent] is used
|
||||
final WidgetBuilder content;
|
||||
|
||||
/// Holds the state of the flyout. Can be useful to open or close the flyout
|
||||
/// programatically.
|
||||
///
|
||||
/// Call `controller.dispose()` to clean up resources when no longer necessary
|
||||
///
|
||||
/// See also:
|
||||
/// * [openMode], which can open the flyout on hover, press and long press
|
||||
final FlyoutController? controller;
|
||||
|
||||
/// The vertical gap between the [child] and the displayed flyout.
|
||||
final double verticalOffset;
|
||||
|
||||
/// The horizontal gap between the [child] and the displayed flyout.
|
||||
final double horizontalOffset;
|
||||
|
||||
/// How the flyout will be placed horizontally relatively to the [child].
|
||||
///
|
||||
/// Defaults to [FlyoutPlacement.center]
|
||||
final FlyoutPlacement placement;
|
||||
|
||||
/// How the flyout will be opened by the end-user without needing to use a
|
||||
/// controller.
|
||||
///
|
||||
/// Defaults to none
|
||||
final FlyoutOpenMode openMode;
|
||||
|
||||
/// The duration of the hover if [openMode] is [FlyoutOpenMode.longHover].
|
||||
///
|
||||
/// 800 milliseconds are used by default
|
||||
final Duration longHoverDuration;
|
||||
|
||||
/// Where the flyout will be placed vertically relatively to the child
|
||||
///
|
||||
/// Defaults to [FlyoutPosition.above]
|
||||
final FlyoutPosition position;
|
||||
|
||||
/// Called when the flyout is opened, either by [controller] or [openMode]
|
||||
final VoidCallback? onOpen;
|
||||
|
||||
/// Called when the flyout is closed, either by [controller] or by the user
|
||||
final VoidCallback? onClose;
|
||||
|
||||
@override
|
||||
_FlyoutState createState() => _FlyoutState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty<FlyoutController>('controller', controller))
|
||||
..add(DoubleProperty(
|
||||
'vertical offset',
|
||||
verticalOffset,
|
||||
defaultValue: 24.0,
|
||||
))
|
||||
..add(DoubleProperty(
|
||||
'horizontal offset',
|
||||
horizontalOffset,
|
||||
defaultValue: 10.0,
|
||||
))
|
||||
..add(EnumProperty<FlyoutPlacement>(
|
||||
'placement',
|
||||
placement,
|
||||
defaultValue: FlyoutPlacement.center,
|
||||
))
|
||||
..add(EnumProperty<FlyoutOpenMode>(
|
||||
'open mode',
|
||||
openMode,
|
||||
defaultValue: FlyoutOpenMode.none,
|
||||
))
|
||||
..add(EnumProperty<FlyoutPosition>(
|
||||
'position',
|
||||
position,
|
||||
defaultValue: FlyoutPosition.above,
|
||||
))
|
||||
..add(DiagnosticsProperty<Duration>(
|
||||
'long hover duration',
|
||||
longHoverDuration,
|
||||
defaultValue: kDefaultLongHoverDuration,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _FlyoutState extends State<Flyout> {
|
||||
final popupKey = GlobalKey<PopUpState>();
|
||||
|
||||
late FlyoutController controller;
|
||||
Timer? longHoverTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = widget.controller ?? FlyoutController();
|
||||
controller.addListener(_handleStateChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant Flyout oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.controller == null && widget.controller != null) {
|
||||
// Dispose the current controller, which was created locally
|
||||
controller.dispose();
|
||||
|
||||
// Assign to the new controller
|
||||
controller = widget.controller!;
|
||||
controller.addListener(_handleStateChanged);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleStateChanged() {
|
||||
if (!mounted) return;
|
||||
final isOpen = popupKey.currentState?.isOpen ?? false;
|
||||
if (!isOpen && controller.isOpen) {
|
||||
popupKey.currentState?.openPopup().then((value) {
|
||||
widget.onClose?.call();
|
||||
});
|
||||
widget.onOpen?.call();
|
||||
} else if (isOpen && controller.isClosed) {
|
||||
Navigator.pop(context);
|
||||
widget.onClose?.call();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.removeListener(_handleStateChanged);
|
||||
// Dispose the controller if null
|
||||
if (widget.controller == null) {
|
||||
controller.dispose();
|
||||
}
|
||||
longHoverTimer?.cancel();
|
||||
longHoverTimer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final popup = PopUp(
|
||||
key: popupKey,
|
||||
content: widget.content,
|
||||
verticalOffset: widget.verticalOffset,
|
||||
horizontalOffset: widget.horizontalOffset,
|
||||
placement: widget.placement,
|
||||
position: widget.position,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
switch (widget.openMode) {
|
||||
case FlyoutOpenMode.none:
|
||||
return popup;
|
||||
case FlyoutOpenMode.hover:
|
||||
return MouseRegion(
|
||||
opaque: false,
|
||||
onEnter: (event) => controller.open(),
|
||||
child: popup,
|
||||
);
|
||||
case FlyoutOpenMode.longHover:
|
||||
return MouseRegion(
|
||||
opaque: true,
|
||||
onEnter: (event) {
|
||||
longHoverTimer = Timer(widget.longHoverDuration, controller.open);
|
||||
},
|
||||
onExit: (event) {
|
||||
if (longHoverTimer?.isActive ?? false) longHoverTimer?.cancel();
|
||||
},
|
||||
child: popup,
|
||||
);
|
||||
case FlyoutOpenMode.press:
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: controller.open,
|
||||
child: popup,
|
||||
);
|
||||
case FlyoutOpenMode.longPress:
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onLongPress: controller.open,
|
||||
child: popup,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
237
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/flyout/menu.dart
vendored
Normal file
237
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/flyout/menu.dart
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
part of 'flyout.dart';
|
||||
|
||||
/// Menu flyouts are used in menu and context menu scenarios to display a list
|
||||
/// of commands or options when requested by the user. A menu flyout shows a
|
||||
/// single, inline, top-level menu that can have menu items and sub-menus.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Flyout]
|
||||
/// * [FlyoutContent]
|
||||
class MenuFlyout extends StatelessWidget {
|
||||
/// Creates a menu flyout.
|
||||
const MenuFlyout({
|
||||
Key? key,
|
||||
this.items = const [],
|
||||
this.color,
|
||||
this.shape,
|
||||
this.shadowColor = Colors.black,
|
||||
this.elevation = 8.0,
|
||||
this.constraints,
|
||||
this.padding = const EdgeInsets.symmetric(vertical: 8.0),
|
||||
}) : super(key: key);
|
||||
|
||||
final List<MenuFlyoutItemInterface> items;
|
||||
|
||||
/// The background color of the box.
|
||||
final Color? color;
|
||||
|
||||
/// The shape to fill the [color] of the box.
|
||||
final ShapeBorder? shape;
|
||||
|
||||
/// The shadow color.
|
||||
final Color shadowColor;
|
||||
|
||||
/// The z-coordinate relative to the box at which to place this physical
|
||||
/// object.
|
||||
final double elevation;
|
||||
|
||||
/// Additional constraints to apply to the child.
|
||||
final BoxConstraints? constraints;
|
||||
|
||||
/// The padding applied the [items], with correct handling when scrollable
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
static const EdgeInsetsGeometry itemsPadding = EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool hasLeading = () {
|
||||
try {
|
||||
items.whereType<MenuFlyoutItem>().firstWhere((i) => i.leading != null);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}();
|
||||
return FlyoutContent(
|
||||
color: color,
|
||||
constraints: constraints,
|
||||
elevation: elevation,
|
||||
shadowColor: shadowColor,
|
||||
shape: shape,
|
||||
padding: EdgeInsets.zero,
|
||||
child: ScrollConfiguration(
|
||||
behavior: const _MenuScrollBehavior(),
|
||||
child: SingleChildScrollView(
|
||||
padding: padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: items.map<Widget>((item) {
|
||||
if (item is MenuFlyoutItem) item._useIconPlaceholder = hasLeading;
|
||||
return KeyedSubtree(
|
||||
key: item.key,
|
||||
child: item.build(context),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Do not use the platform-specific default scroll configuration.
|
||||
// Menus should never overscroll or display an overscroll indicator.
|
||||
class _MenuScrollBehavior extends FluentScrollBehavior {
|
||||
const _MenuScrollBehavior();
|
||||
|
||||
@override
|
||||
TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;
|
||||
|
||||
@override
|
||||
Widget buildViewportChrome(
|
||||
BuildContext context, Widget child, AxisDirection axisDirection) =>
|
||||
child;
|
||||
|
||||
@override
|
||||
ScrollPhysics getScrollPhysics(BuildContext context) =>
|
||||
const ClampingScrollPhysics();
|
||||
}
|
||||
|
||||
abstract class MenuFlyoutItemInterface {
|
||||
final Key? key;
|
||||
|
||||
const MenuFlyoutItemInterface({this.key});
|
||||
|
||||
Widget build(BuildContext context);
|
||||
}
|
||||
|
||||
class MenuFlyoutItem extends MenuFlyoutItemInterface {
|
||||
MenuFlyoutItem({
|
||||
Key? key,
|
||||
this.leading,
|
||||
required this.text,
|
||||
this.trailing,
|
||||
required this.onPressed,
|
||||
this.onRightPressed,
|
||||
this.selected = false,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget? leading;
|
||||
final Widget text;
|
||||
final Widget? trailing;
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onRightPressed;
|
||||
final bool selected;
|
||||
|
||||
bool _useIconPlaceholder = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = PopupContentSizeInfo.of(context).size;
|
||||
return Listener(
|
||||
child: Container(
|
||||
width: size.isEmpty ? null : size.width,
|
||||
padding: MenuFlyout.itemsPadding,
|
||||
child: Listener(
|
||||
child: FlyoutListTile(
|
||||
selected: selected,
|
||||
icon: leading ??
|
||||
() {
|
||||
if (_useIconPlaceholder) return const Icon(null);
|
||||
return null;
|
||||
}(),
|
||||
text: text,
|
||||
trailing: IconTheme.merge(
|
||||
data: const IconThemeData(size: 12.0),
|
||||
child: trailing ?? const SizedBox.shrink(),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
),
|
||||
onPointerDown: (event) {
|
||||
if(event.kind != PointerDeviceKind.mouse
|
||||
|| event.buttons != kSecondaryMouseButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
onRightPressed?.call();
|
||||
},
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuFlyoutSeparator extends MenuFlyoutItemInterface {
|
||||
const MenuFlyoutSeparator({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = PopupContentSizeInfo.of(context).size;
|
||||
return SizedBox(
|
||||
width: size.width,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.only(bottom: 5.0),
|
||||
child: Divider(
|
||||
style: DividerThemeData(horizontalMargin: EdgeInsets.zero),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuFlyoutSubItem extends MenuFlyoutItem {
|
||||
MenuFlyoutSubItem({
|
||||
Key? key,
|
||||
Widget? leading,
|
||||
required Widget text,
|
||||
Widget? trailing = const Icon(FluentIcons.chevron_right),
|
||||
required this.items,
|
||||
this.openMode = FlyoutOpenMode.longHover,
|
||||
}) : super(
|
||||
key: key,
|
||||
leading: leading,
|
||||
text: text,
|
||||
trailing: trailing,
|
||||
onPressed: () {},
|
||||
);
|
||||
|
||||
final List<MenuFlyoutItemInterface> items;
|
||||
|
||||
final FlyoutOpenMode openMode;
|
||||
|
||||
bool _open = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StatefulBuilder(builder: (context, setState) {
|
||||
return Flyout(
|
||||
openMode: openMode,
|
||||
position: FlyoutPosition.side,
|
||||
placement: FlyoutPlacement.end,
|
||||
verticalOffset: 40.0,
|
||||
horizontalOffset: 0.0,
|
||||
content: (context) {
|
||||
return MenuFlyout(
|
||||
items: items,
|
||||
);
|
||||
},
|
||||
onOpen: () => setState(() => _open = true),
|
||||
onClose: () => setState(() => _open = false),
|
||||
child: MenuFlyoutItem(
|
||||
onPressed: () {},
|
||||
text: text,
|
||||
leading: leading,
|
||||
trailing: trailing,
|
||||
selected: _open,
|
||||
).build(context),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
338
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/info_bar.dart
vendored
Normal file
338
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/info_bar.dart
vendored
Normal file
@@ -0,0 +1,338 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' show Icons;
|
||||
|
||||
// This file implements info bar into this library.
|
||||
// It follows this https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/infobar
|
||||
|
||||
/// The severities that can be applied to an [InfoBar]
|
||||
enum InfoBarSeverity {
|
||||
/// 
|
||||
info,
|
||||
|
||||
/// 
|
||||
warning,
|
||||
|
||||
/// 
|
||||
error,
|
||||
|
||||
/// 
|
||||
success,
|
||||
}
|
||||
|
||||
/// The InfoBar control is for displaying app-wide status messages to
|
||||
/// users that are highly visible yet non-intrusive. There are built-in
|
||||
/// Severity levels to easily indicate the type of message shown as well
|
||||
/// as the option to include your own call to action or hyperlink button.
|
||||
/// Since the InfoBar is inline with other UI content the option is there
|
||||
/// for the control to always be visible or dismissed by the user.
|
||||
///
|
||||
/// 
|
||||
class InfoBar extends StatelessWidget {
|
||||
/// Creates an info bar.
|
||||
const InfoBar({
|
||||
Key? key,
|
||||
required this.title,
|
||||
this.content,
|
||||
this.action,
|
||||
this.severity = InfoBarSeverity.info,
|
||||
this.style,
|
||||
this.isLong = false,
|
||||
this.onClose,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The severity of this InfoBar. Defaults to [InfoBarSeverity.info]
|
||||
final InfoBarSeverity severity;
|
||||
|
||||
/// The style applied to this info bar. If non-null, it's
|
||||
/// mescled with [ThemeData.infoBarThemeData]
|
||||
final InfoBarThemeData? style;
|
||||
|
||||
final Widget title;
|
||||
final Widget? content;
|
||||
final Widget? action;
|
||||
|
||||
/// Called when the close button is pressed. If this is null,
|
||||
/// there will be no close button
|
||||
final void Function()? onClose;
|
||||
|
||||
/// If `true`, the info bar will be treated as long.
|
||||
///
|
||||
/// 
|
||||
final bool isLong;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(FlagProperty('long', value: isLong, ifFalse: 'short'))
|
||||
..add(EnumProperty('severity', severity))
|
||||
..add(ObjectFlagProperty.has('onClose', onClose))
|
||||
..add(DiagnosticsProperty('style', style, ifNull: 'no style'));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
final localizations = FluentLocalizations.of(context);
|
||||
final style = InfoBarTheme.of(context).merge(this.style);
|
||||
final icon = style.icon?.call(severity);
|
||||
final closeIcon = style.closeIcon;
|
||||
final title = DefaultTextStyle(
|
||||
style: const TextStyle(),
|
||||
child: this.title,
|
||||
);
|
||||
final content = () {
|
||||
if (this.content == null) return null;
|
||||
return DefaultTextStyle(
|
||||
style: FluentTheme.of(context).typography.body ?? const TextStyle(),
|
||||
softWrap: true,
|
||||
child: this.content!,
|
||||
);
|
||||
}();
|
||||
final action = () {
|
||||
if (this.action == null) return null;
|
||||
return ButtonTheme.merge(
|
||||
child: this.action!,
|
||||
data: ButtonThemeData.all(style.actionStyle),
|
||||
);
|
||||
}();
|
||||
return Container(
|
||||
decoration: style.decoration?.call(severity),
|
||||
padding: style.padding ??
|
||||
const EdgeInsets.only(left: 10, right: 10, top: 10),
|
||||
alignment: Alignment.centerLeft,
|
||||
child:
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (icon != null)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 6.0),
|
||||
child: Icon(icon, color: style.iconColor?.call(severity)),
|
||||
),
|
||||
|
||||
if (icon != null)
|
||||
SizedBox(width: 8.0),
|
||||
|
||||
Expanded(child: title),
|
||||
|
||||
if (action != null) action,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [InfoBar]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [InfoBar] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class InfoBarTheme extends InheritedTheme {
|
||||
/// Creates a info bar theme that controls the configurations for
|
||||
/// [InfoBar].
|
||||
const InfoBarTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [InfoBar] widgets.
|
||||
final InfoBarThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [InfoBar]s should
|
||||
/// look like, and merges in the current toggle button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required InfoBarThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return InfoBarTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static InfoBarThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme = context.dependOnInheritedWidgetOfExactType<InfoBarTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).infoBarTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [InfoBarTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.infoBarTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// InfoBarThemeData theme = InfoBarTheme.of(context);
|
||||
/// ```
|
||||
static InfoBarThemeData of(BuildContext context) {
|
||||
final InfoBarTheme? theme =
|
||||
context.dependOnInheritedWidgetOfExactType<InfoBarTheme>();
|
||||
return InfoBarThemeData.standard(FluentTheme.of(context)).merge(
|
||||
theme?.data ?? FluentTheme.of(context).infoBarTheme,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return InfoBarTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InfoBarTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
||||
|
||||
typedef InfoBarSeverityCheck<T> = T Function(InfoBarSeverity severity);
|
||||
|
||||
class InfoBarThemeData with Diagnosticable {
|
||||
final InfoBarSeverityCheck<Decoration?>? decoration;
|
||||
final InfoBarSeverityCheck<Color?>? iconColor;
|
||||
final InfoBarSeverityCheck<IconData>? icon;
|
||||
|
||||
final ButtonStyle? closeButtonStyle;
|
||||
final IconData? closeIcon;
|
||||
final double? closeIconSize;
|
||||
|
||||
final ButtonStyle? actionStyle;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const InfoBarThemeData({
|
||||
this.decoration,
|
||||
this.icon,
|
||||
this.iconColor,
|
||||
this.closeButtonStyle,
|
||||
this.closeIcon,
|
||||
this.closeIconSize,
|
||||
this.actionStyle,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
factory InfoBarThemeData.standard(ThemeData style) {
|
||||
final isDark = style.brightness == Brightness.dark;
|
||||
return InfoBarThemeData(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: (severity) {
|
||||
late Color color;
|
||||
switch (severity) {
|
||||
case InfoBarSeverity.info:
|
||||
color = isDark ? const Color(0xFF272727) : const Color(0xFFf4f4f4);
|
||||
break;
|
||||
case InfoBarSeverity.warning:
|
||||
color = Colors.warningSecondaryColor
|
||||
.resolveFromBrightness(style.brightness);
|
||||
break;
|
||||
case InfoBarSeverity.success:
|
||||
color = Colors.successSecondaryColor
|
||||
.resolveFromBrightness(style.brightness);
|
||||
break;
|
||||
case InfoBarSeverity.error:
|
||||
color = Colors.errorSecondaryColor
|
||||
.resolveFromBrightness(style.brightness);
|
||||
break;
|
||||
}
|
||||
return BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
boxShadow: kElevationToShadow[2],
|
||||
);
|
||||
},
|
||||
closeIcon: FluentIcons.chrome_close,
|
||||
closeIconSize: 16.0,
|
||||
icon: (severity) {
|
||||
switch (severity) {
|
||||
case InfoBarSeverity.info:
|
||||
return FluentIcons.info_solid;
|
||||
case InfoBarSeverity.warning:
|
||||
return FluentIcons.critical_error_solid;
|
||||
case InfoBarSeverity.success:
|
||||
return Icons.check_circle;
|
||||
case InfoBarSeverity.error:
|
||||
return Icons.cancel;
|
||||
}
|
||||
},
|
||||
iconColor: (severity) {
|
||||
switch (severity) {
|
||||
case InfoBarSeverity.info:
|
||||
return style.accentColor
|
||||
.resolveFromReverseBrightness(style.brightness);
|
||||
case InfoBarSeverity.warning:
|
||||
return isDark ? Colors.yellow : Colors.warningPrimaryColor;
|
||||
case InfoBarSeverity.success:
|
||||
return Colors.successPrimaryColor;
|
||||
case InfoBarSeverity.error:
|
||||
return isDark ? Colors.red : Colors.errorPrimaryColor;
|
||||
}
|
||||
},
|
||||
actionStyle: ButtonStyle(
|
||||
padding: ButtonState.all(const EdgeInsets.all(6)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static InfoBarThemeData lerp(
|
||||
InfoBarThemeData? a,
|
||||
InfoBarThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return InfoBarThemeData(
|
||||
closeIconSize: lerpDouble(a?.closeIconSize, b?.closeIconSize, t),
|
||||
closeIcon: t < 0.5 ? a?.closeIcon : b?.closeIcon,
|
||||
closeButtonStyle:
|
||||
ButtonStyle.lerp(a?.closeButtonStyle, b?.closeButtonStyle, t),
|
||||
icon: t < 0.5 ? a?.icon : b?.icon,
|
||||
decoration: (severity) {
|
||||
return Decoration.lerp(
|
||||
a?.decoration?.call(severity),
|
||||
b?.decoration?.call(severity),
|
||||
t,
|
||||
);
|
||||
},
|
||||
actionStyle: ButtonStyle.lerp(a?.actionStyle, b?.actionStyle, t),
|
||||
iconColor: (severity) {
|
||||
return Color.lerp(
|
||||
a?.iconColor?.call(severity),
|
||||
b?.iconColor?.call(severity),
|
||||
t,
|
||||
);
|
||||
},
|
||||
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
|
||||
);
|
||||
}
|
||||
|
||||
InfoBarThemeData merge(InfoBarThemeData? style) {
|
||||
if (style == null) return this;
|
||||
return InfoBarThemeData(
|
||||
closeIcon: style.closeIcon ?? closeIcon,
|
||||
icon: style.icon ?? icon,
|
||||
decoration: style.decoration ?? decoration,
|
||||
actionStyle: style.actionStyle ?? actionStyle,
|
||||
iconColor: style.iconColor ?? iconColor,
|
||||
padding: style.padding ?? padding,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(ObjectFlagProperty.has('icon', icon))
|
||||
..add(IconDataProperty('closeIcon', closeIcon))
|
||||
..add(ObjectFlagProperty.has('decoration', decoration))
|
||||
..add(ObjectFlagProperty.has('iconColor', iconColor))
|
||||
..add(DiagnosticsProperty<ButtonStyle>(
|
||||
'actionStyle',
|
||||
actionStyle,
|
||||
ifNull: 'no style',
|
||||
));
|
||||
}
|
||||
}
|
||||
191
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/list_tile.dart
vendored
Normal file
191
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/list_tile.dart
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
const kThreeLineTileHeight = 60.0;
|
||||
const kTwoLineTileHeight = 52.0;
|
||||
const kOneLineTileHeight = 40.0;
|
||||
|
||||
const kDefaultContentPadding = EdgeInsets.symmetric(
|
||||
horizontal: 12.0,
|
||||
vertical: 6.0,
|
||||
);
|
||||
|
||||
class ListTile extends StatelessWidget {
|
||||
const ListTile({
|
||||
Key? key,
|
||||
this.tileColor,
|
||||
this.shape,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.trailing,
|
||||
this.isThreeLine = false,
|
||||
this.contentPadding = kDefaultContentPadding,
|
||||
}) : assert(
|
||||
subtitle != null ? title != null : true,
|
||||
'To have a subtitle, there must be a title',
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// The color of the tile
|
||||
final Color? tileColor;
|
||||
|
||||
/// The shape of the tile
|
||||
final ShapeBorder? shape;
|
||||
|
||||
final Widget? leading;
|
||||
final Widget? title;
|
||||
final Widget? subtitle;
|
||||
final Widget? trailing;
|
||||
|
||||
final bool isThreeLine;
|
||||
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
|
||||
bool get isTwoLine => subtitle != null;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ColorProperty('tileColor', tileColor));
|
||||
properties.add(FlagProperty(
|
||||
'isThreeLine',
|
||||
value: isThreeLine,
|
||||
ifFalse: isTwoLine ? 'two lines' : 'one line',
|
||||
));
|
||||
properties.add(DiagnosticsProperty('shape', shape));
|
||||
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
|
||||
'contentPadding',
|
||||
contentPadding,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final style = FluentTheme.of(context);
|
||||
return Container(
|
||||
decoration: ShapeDecoration(
|
||||
shape: shape ?? const ContinuousRectangleBorder(),
|
||||
color: tileColor,
|
||||
),
|
||||
height: isThreeLine
|
||||
? kThreeLineTileHeight
|
||||
: isTwoLine
|
||||
? kTwoLineTileHeight
|
||||
: kOneLineTileHeight,
|
||||
padding: contentPadding,
|
||||
child: Row(children: [
|
||||
if (leading != null)
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 14),
|
||||
child: leading,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null)
|
||||
DefaultTextStyle(
|
||||
style: (style.typography.body ?? const TextStyle()).copyWith(
|
||||
fontSize: 16,
|
||||
),
|
||||
overflow: TextOverflow.clip,
|
||||
child: title!,
|
||||
),
|
||||
if (subtitle != null)
|
||||
DefaultTextStyle(
|
||||
style: style.typography.caption ?? const TextStyle(),
|
||||
overflow: TextOverflow.clip,
|
||||
child: subtitle!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) trailing!,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TappableListTile extends StatelessWidget {
|
||||
const TappableListTile({
|
||||
Key? key,
|
||||
this.tileColor,
|
||||
this.shape,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.trailing,
|
||||
this.isThreeLine = false,
|
||||
this.onTap,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
this.contentPadding = kDefaultContentPadding,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback? onTap;
|
||||
|
||||
final ButtonState<Color>? tileColor;
|
||||
final ButtonState<ShapeBorder>? shape;
|
||||
|
||||
final Widget? leading;
|
||||
final Widget? title;
|
||||
final Widget? subtitle;
|
||||
final Widget? trailing;
|
||||
|
||||
final bool isThreeLine;
|
||||
|
||||
final FocusNode? focusNode;
|
||||
final bool autofocus;
|
||||
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ObjectFlagProperty('onTap', onTap, ifNull: 'disabled'));
|
||||
properties.add(FlagProperty(
|
||||
'autofocus',
|
||||
value: autofocus,
|
||||
defaultValue: false,
|
||||
ifFalse: 'manual focus',
|
||||
));
|
||||
properties.add(ObjectFlagProperty.has('focusNode', focusNode));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final style = FluentTheme.of(context);
|
||||
return HoverButton(
|
||||
onPressed: onTap,
|
||||
focusNode: focusNode,
|
||||
autofocus: autofocus,
|
||||
builder: (context, states) {
|
||||
final Color tileColor = () {
|
||||
if (this.tileColor != null) {
|
||||
return this.tileColor!.resolve(states);
|
||||
} else if (states.isFocused) {
|
||||
return style.accentColor.resolve(context);
|
||||
}
|
||||
return ButtonThemeData.uncheckedInputColor(style, states);
|
||||
}();
|
||||
return ListTile(
|
||||
contentPadding: contentPadding,
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
isThreeLine: isThreeLine,
|
||||
tileColor: tileColor,
|
||||
shape: shape?.resolve(states),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
502
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/progress_indicators.dart
vendored
Normal file
502
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/progress_indicators.dart
vendored
Normal file
@@ -0,0 +1,502 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
const double _kMinProgressRingIndicatorSize = 36.0;
|
||||
const double _kMinProgressBarWidth = 130.0;
|
||||
|
||||
/// A progress control provides feedback to the user that a
|
||||
/// long-running operation is underway. It can mean that the
|
||||
/// user cannot interact with the app when the progress indicator
|
||||
/// is visible, and can also indicate how long the wait time might be.
|
||||
///
|
||||
/// 
|
||||
/// 
|
||||
class ProgressBar extends StatefulWidget {
|
||||
/// Creates a new progress bar.
|
||||
///
|
||||
/// [value], if non-null, must be in the range of 0 to 100.
|
||||
///
|
||||
/// [strokeWidth] must be equal or greater than 0
|
||||
const ProgressBar({
|
||||
Key? key,
|
||||
this.value,
|
||||
this.strokeWidth = 4.5,
|
||||
this.semanticLabel,
|
||||
this.backgroundColor,
|
||||
this.activeColor,
|
||||
}) : assert(value == null || value >= 0 && value <= 100),
|
||||
assert(strokeWidth >= 0),
|
||||
super(key: key);
|
||||
|
||||
/// The current value of the indicator. If non-null, produces
|
||||
/// the following:
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// If null, an indeterminate progress bar is created:
|
||||
///
|
||||
/// 
|
||||
final double? value;
|
||||
|
||||
/// The height of the progess bar. Defaults to 4.5 logical pixels
|
||||
final double strokeWidth;
|
||||
final String? semanticLabel;
|
||||
|
||||
/// The background color of the progress bar. If null,
|
||||
/// [ThemeData.inactiveColor] is used
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The active color of the progress bar. If null,
|
||||
/// [ThemeData.accentColor] is used
|
||||
final Color? activeColor;
|
||||
|
||||
@override
|
||||
_ProgressBarState createState() => _ProgressBarState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DoubleProperty('value', value, ifNull: 'indeterminate'));
|
||||
properties.add(DoubleProperty('strokeWidth', strokeWidth));
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressBarState extends State<ProgressBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 3),
|
||||
vsync: this,
|
||||
);
|
||||
if (widget.value == null) _controller.repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ProgressBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.value == null && !_controller.isAnimating) {
|
||||
_controller.repeat();
|
||||
} else if (widget.value != null && _controller.isAnimating) {
|
||||
_controller.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double p1 = 0, p2 = 0;
|
||||
double idleFrames = 15, cycle = 1, idle = 1;
|
||||
double lastValue = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
return Container(
|
||||
height: widget.strokeWidth,
|
||||
constraints: const BoxConstraints(minWidth: _kMinProgressBarWidth),
|
||||
child: Semantics(
|
||||
label: widget.semanticLabel,
|
||||
value: widget.value?.toStringAsFixed(2),
|
||||
maxValueLength: 100,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
double deltaValue = _controller.value - lastValue;
|
||||
lastValue = _controller.value;
|
||||
if (deltaValue < 0) deltaValue++; // repeat
|
||||
return CustomPaint(
|
||||
painter: _ProgressBarPainter(
|
||||
value: widget.value == null ? null : widget.value! / 100,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
activeColor: widget.activeColor ??
|
||||
theme.accentColor.defaultBrushFor(theme.brightness),
|
||||
backgroundColor:
|
||||
widget.backgroundColor ?? theme.inactiveBackgroundColor,
|
||||
p1: p1,
|
||||
p2: p2,
|
||||
idleFrames: idleFrames,
|
||||
cycle: cycle,
|
||||
idle: idle,
|
||||
deltaValue: deltaValue,
|
||||
onUpdate: (values) {
|
||||
p1 = values[0];
|
||||
p2 = values[1];
|
||||
idleFrames = values[2];
|
||||
cycle = values[3];
|
||||
idle = values[4];
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressBarPainter extends CustomPainter {
|
||||
static const _step1 = 2.7, _step2 = 4.5, _velocityScale = 0.8;
|
||||
static const _short = 0.4; // percentage of short line (0..1)
|
||||
static const _long = 80 / 130; // percentage of long line (0..1)
|
||||
|
||||
double p1, p2, idleFrames, cycle, idle;
|
||||
double deltaValue;
|
||||
|
||||
ValueChanged<List<double>> onUpdate;
|
||||
|
||||
final double strokeWidth;
|
||||
final Color backgroundColor;
|
||||
final Color activeColor;
|
||||
|
||||
final double? value;
|
||||
|
||||
_ProgressBarPainter({
|
||||
required this.p1,
|
||||
required this.p2,
|
||||
required this.idle,
|
||||
required this.cycle,
|
||||
required this.idleFrames,
|
||||
required this.deltaValue,
|
||||
required this.onUpdate,
|
||||
required this.strokeWidth,
|
||||
required this.backgroundColor,
|
||||
required this.activeColor,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
void drawLine(Offset xy1, Offset xy2, Color color) {
|
||||
canvas.drawLine(
|
||||
xy1,
|
||||
xy2,
|
||||
Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round,
|
||||
);
|
||||
}
|
||||
|
||||
// background line
|
||||
drawLine(
|
||||
Offset(0, size.height),
|
||||
Offset(size.width, size.height),
|
||||
backgroundColor,
|
||||
);
|
||||
|
||||
if (value != null) {
|
||||
drawLine(
|
||||
Offset(0, size.height),
|
||||
Offset(value!.clamp(0.0, 1.0) * size.width, size.height),
|
||||
activeColor,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// The math below is cortesy of raitonuberu:
|
||||
// https://gist.github.com/raitonoberu/21dacaee725806b60ddb45ec68147d30
|
||||
// https://github.com/raitonoberu
|
||||
|
||||
void update() {
|
||||
onUpdate([p1, p2, idleFrames, cycle, idle]);
|
||||
}
|
||||
|
||||
Offset coords(double percentage) {
|
||||
return Offset(
|
||||
size.width * percentage,
|
||||
size.height,
|
||||
);
|
||||
}
|
||||
|
||||
double calcVelocity(double p) {
|
||||
return (1 + math.cos(math.pi * p - (math.pi / 2)) * _velocityScale) *
|
||||
deltaValue;
|
||||
}
|
||||
|
||||
final v1 = calcVelocity(p1);
|
||||
final v2 = calcVelocity(p2);
|
||||
|
||||
if (cycle == 1) {
|
||||
// short line
|
||||
p2 = math.min(p2 + _step1 * v2, 1);
|
||||
if (p2 - p1 >= _short || p2 == 1) p1 = math.min(p1 + _step1 * v1, 1);
|
||||
}
|
||||
if (cycle == -1) {
|
||||
// long line
|
||||
p2 = math.min(p2 + _step2 * v2, 1);
|
||||
if (p2 - p1 >= _long || p2 == 1) p1 = math.min(p1 + _step2 * v1, 1);
|
||||
}
|
||||
if (p1 == 1) {
|
||||
// the end reached
|
||||
idle = idleFrames;
|
||||
cycle *= -1;
|
||||
p1 = 0;
|
||||
p2 = 0;
|
||||
}
|
||||
update();
|
||||
|
||||
if (idle != 0) drawLine(coords(p1), coords(p2), activeColor);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_ProgressBarPainter oldDelegate) => true;
|
||||
|
||||
@override
|
||||
bool shouldRebuildSemantics(_ProgressBarPainter oldDelegate) => false;
|
||||
}
|
||||
|
||||
/// A progress control provides feedback to the user that a
|
||||
/// long-running operation is underway. It can mean that the
|
||||
/// user cannot interact with the app when the progress indicator
|
||||
/// is visible, and can also indicate how long the wait time might be.
|
||||
///
|
||||
/// 
|
||||
/// 
|
||||
class ProgressRing extends StatefulWidget {
|
||||
/// Creates progress ring.
|
||||
///
|
||||
/// [value], if non-null, must be in the range of 0 to 100
|
||||
///
|
||||
/// [strokeWidth] must be equal or greater than 0
|
||||
const ProgressRing({
|
||||
Key? key,
|
||||
this.value,
|
||||
this.strokeWidth = 4.5,
|
||||
this.semanticLabel,
|
||||
this.backgroundColor,
|
||||
this.activeColor,
|
||||
this.backwards = false,
|
||||
}) : assert(value == null || value >= 0 && value <= 100),
|
||||
super(key: key);
|
||||
|
||||
/// The current value of the indicator. If non-null, produces
|
||||
/// the following:
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// If null, an indeterminate progress ring is created:
|
||||
///
|
||||
/// 
|
||||
final double? value;
|
||||
|
||||
/// The stroke width of the progress ring. If null, defaults to 4.5 logical pixels
|
||||
final double strokeWidth;
|
||||
final String? semanticLabel;
|
||||
|
||||
/// The background color of the progress ring. If null,
|
||||
/// [ThemeData.inactiveColor] is used
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The active color of the progress ring. If null,
|
||||
/// [ThemeData.accentColor] is used
|
||||
final Color? activeColor;
|
||||
|
||||
/// Whether the indicator spins backwards or not. Defaults to false
|
||||
final bool backwards;
|
||||
|
||||
@override
|
||||
_ProgressRingState createState() => _ProgressRingState();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DoubleProperty('value', value, ifNull: 'indeterminate'));
|
||||
properties.add(DoubleProperty('strokeWidth', strokeWidth));
|
||||
}
|
||||
}
|
||||
|
||||
class _ProgressRingState extends State<ProgressRing>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(seconds: 3),
|
||||
vsync: this,
|
||||
);
|
||||
if (widget.value == null) _controller.repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ProgressRing oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.value == null && !_controller.isAnimating) {
|
||||
_controller.repeat();
|
||||
} else if (widget.value != null && _controller.isAnimating) {
|
||||
_controller.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
double d1 = 0, d2 = 0;
|
||||
double speed1 = 1440, speed2 = 2160; // deg per second
|
||||
double lastValue = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
return Container(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: _kMinProgressRingIndicatorSize,
|
||||
minHeight: _kMinProgressRingIndicatorSize,
|
||||
),
|
||||
child: Semantics(
|
||||
label: widget.semanticLabel,
|
||||
value: widget.value?.toStringAsFixed(2),
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
double deltaValue = _controller.value - lastValue;
|
||||
lastValue = _controller.value;
|
||||
if (deltaValue < 0) deltaValue++; // repeat
|
||||
return CustomPaint(
|
||||
painter: _RingPainter(
|
||||
backgroundColor:
|
||||
widget.backgroundColor ?? theme.inactiveBackgroundColor,
|
||||
value: widget.value,
|
||||
color: widget.activeColor ??
|
||||
theme.accentColor.defaultBrushFor(theme.brightness),
|
||||
strokeWidth: widget.strokeWidth,
|
||||
d1: d1,
|
||||
d2: d2,
|
||||
speed1: speed1,
|
||||
speed2: speed2,
|
||||
deltaValue: deltaValue,
|
||||
onUpdate: (v) {
|
||||
d1 = v[0];
|
||||
d2 = v[1];
|
||||
speed1 = v[2];
|
||||
speed2 = v[3];
|
||||
},
|
||||
backwards: widget.backwards,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RingPainter extends CustomPainter {
|
||||
final Color color;
|
||||
final Color backgroundColor;
|
||||
final double strokeWidth;
|
||||
final double? value;
|
||||
final double d1, d2, speed1, speed2;
|
||||
final double deltaValue;
|
||||
final ValueChanged<List<double>> onUpdate;
|
||||
final bool backwards;
|
||||
|
||||
const _RingPainter({
|
||||
required this.color,
|
||||
required this.backgroundColor,
|
||||
required this.strokeWidth,
|
||||
required this.value,
|
||||
required this.d1,
|
||||
required this.d2,
|
||||
required this.speed1,
|
||||
required this.speed2,
|
||||
required this.deltaValue,
|
||||
required this.onUpdate,
|
||||
required this.backwards,
|
||||
});
|
||||
|
||||
static const double _twoPi = math.pi * 2.0;
|
||||
static const double _epsilon = .001;
|
||||
// Canvas.drawArc(r, 0, 2*PI) doesn't draw anything, so just get close.
|
||||
static const double _sweep = _twoPi - _epsilon;
|
||||
static const double _startAngle = -math.pi / 2.0;
|
||||
static const double _deg2Rad = (2 * math.pi) / 360;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
// Background line
|
||||
canvas.drawArc(
|
||||
Offset.zero & size,
|
||||
_startAngle,
|
||||
100,
|
||||
false,
|
||||
Paint()
|
||||
..color = backgroundColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth,
|
||||
);
|
||||
final Paint paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = StrokeCap.round
|
||||
..style = PaintingStyle.stroke;
|
||||
if (value == null) {
|
||||
double d1 = this.d1,
|
||||
d2 = this.d2,
|
||||
speed1 = this.speed1,
|
||||
speed2 = this.speed2;
|
||||
|
||||
void drawArc() {
|
||||
canvas.drawArc(
|
||||
Offset.zero & size,
|
||||
(backwards ? (90 - d2) : (90 + d2)) * _deg2Rad,
|
||||
((d2 - d1) * _deg2Rad),
|
||||
false,
|
||||
paint,
|
||||
);
|
||||
}
|
||||
|
||||
void update() {
|
||||
d1 += speed1 * deltaValue;
|
||||
d2 += speed2 * deltaValue;
|
||||
if (d1 > 360 && d2 > 360) {
|
||||
d1 -= 360;
|
||||
d2 -= 360;
|
||||
}
|
||||
if ((d1 - d2).abs() >= 180) {
|
||||
final speed = speed1;
|
||||
speed1 = speed2;
|
||||
speed2 = speed;
|
||||
}
|
||||
// update the values changed above
|
||||
onUpdate([d1, d2, speed1, speed2]);
|
||||
}
|
||||
|
||||
update();
|
||||
if (d1 == d2 && d1 % 360 == 0) return;
|
||||
drawArc();
|
||||
} else {
|
||||
canvas.drawArc(
|
||||
Offset.zero & size,
|
||||
_startAngle,
|
||||
(value! / 100).clamp(0, 1) * _sweep,
|
||||
false,
|
||||
paint,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_RingPainter oldDelegate) =>
|
||||
value == null || value != oldDelegate.value;
|
||||
|
||||
@override
|
||||
bool shouldRebuildSemantics(_RingPainter oldDelegate) => false;
|
||||
}
|
||||
291
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/snackbar.dart
vendored
Normal file
291
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/snackbar.dart
vendored
Normal file
@@ -0,0 +1,291 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
const Duration snackbarShortDuration = Duration(seconds: 2);
|
||||
const Duration snackbarLongDuration = Duration(seconds: 2);
|
||||
|
||||
/// Shows a snackbar on the given context.
|
||||
///
|
||||
/// There must be an [Overlay] above in provided [context] tree, otherwise
|
||||
/// an asserion error is thrown.
|
||||
///
|
||||
/// [duration] defaults to [snackbarShortDuration]. It's recommended
|
||||
/// to use [snackbarLongDuration] for a extended snackbar duration. If null,
|
||||
/// the snackbar will long forever, and have to be dismissed manually.
|
||||
///
|
||||
/// [alignment] is used to align the snackbar within the screen. Defaults
|
||||
/// to [Alignment.bottomCenter]
|
||||
///
|
||||
/// [margin] is the margin applied to snackbar. Defaults to 16 logical
|
||||
/// pixels on all sides
|
||||
///
|
||||
/// [onDismiss] is called when the snackbar is dismissed after [duration].
|
||||
/// It's not called if dismissed manually.
|
||||
///
|
||||
/// To dismiss the snackbar manually, use the following code:
|
||||
///
|
||||
/// ```dart
|
||||
/// final result = showSnackbar(context, snackbar);
|
||||
/// result.remove();
|
||||
/// ```
|
||||
OverlayEntry showSnackbar(
|
||||
BuildContext context,
|
||||
Widget snackbar, {
|
||||
Duration? duration = snackbarShortDuration,
|
||||
Alignment alignment = Alignment.bottomCenter,
|
||||
EdgeInsetsGeometry margin = const EdgeInsets.all(16.0),
|
||||
VoidCallback? onDismiss,
|
||||
}) {
|
||||
assert(debugCheckHasOverlay(context));
|
||||
final GlobalKey<SnackbarState> key = snackbar.key is GlobalKey<SnackbarState>
|
||||
? snackbar.key as GlobalKey<SnackbarState>
|
||||
: GlobalKey<SnackbarState>();
|
||||
final entry = OverlayEntry(builder: (context) {
|
||||
if (snackbar is Snackbar) {
|
||||
return Padding(
|
||||
padding: margin,
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: Snackbar(
|
||||
key: key,
|
||||
content: snackbar.content,
|
||||
action: snackbar.action,
|
||||
extended: snackbar.extended,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: margin,
|
||||
child: Align(
|
||||
alignment: alignment,
|
||||
child: snackbar,
|
||||
),
|
||||
);
|
||||
});
|
||||
Overlay.of(context)!.insert(entry);
|
||||
if (duration != null) {
|
||||
Future.delayed(duration).then((value) async {
|
||||
if (entry.mounted) {
|
||||
if (snackbar is Snackbar) await key.currentState?.controller.reverse();
|
||||
}
|
||||
if (entry.mounted) {
|
||||
entry.remove();
|
||||
onDismiss?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// Snackbars provide a brief message about an operation at the
|
||||
/// bottom of the screen. They can contain a custom action or
|
||||
/// view or use a style geared towards making special announcements
|
||||
/// to your users.
|
||||
class Snackbar extends StatefulWidget {
|
||||
/// Creates a snackbar.
|
||||
const Snackbar({
|
||||
Key? key,
|
||||
required this.content,
|
||||
this.action,
|
||||
this.extended = false,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The content of the snackbar.
|
||||
///
|
||||
/// Typically a [Text]
|
||||
final Widget content;
|
||||
|
||||
/// The action of the snackbar.
|
||||
///
|
||||
/// Typically a [Button]
|
||||
final Widget? action;
|
||||
|
||||
/// Whether the snackbar should be extended or not.
|
||||
final bool extended;
|
||||
|
||||
@override
|
||||
SnackbarState createState() => SnackbarState();
|
||||
}
|
||||
|
||||
class SnackbarState extends State<Snackbar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final SnackbarThemeData theme = SnackbarTheme.of(context);
|
||||
final VisualDensity visualDensity = FluentTheme.of(context).visualDensity;
|
||||
return FadeTransition(
|
||||
opacity: controller,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 300.0, minWidth: 32.0),
|
||||
decoration: theme.decoration,
|
||||
padding: theme.padding,
|
||||
child: DefaultTextStyle(
|
||||
style: TextStyle(color: theme.decoration?.color?.basedOnLuminance()),
|
||||
child: Flex(
|
||||
direction: widget.extended ? Axis.vertical : Axis.horizontal,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: widget.extended
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.center,
|
||||
children: [
|
||||
widget.content,
|
||||
if (widget.action != null)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: widget.extended ? 0 : 16.0 + visualDensity.horizontal,
|
||||
top: !widget.extended ? 0 : 8.0 + visualDensity.vertical,
|
||||
),
|
||||
child: ButtonTheme.merge(
|
||||
data: theme.actionStyle ?? const ButtonThemeData(),
|
||||
child: widget.action!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [Snackbar]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [Snackbar] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class SnackbarTheme extends InheritedTheme {
|
||||
/// Creates a info bar theme that controls the configurations for
|
||||
/// [Snackbar].
|
||||
const SnackbarTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [Snackbar] widgets.
|
||||
final SnackbarThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [Snackbar]s should
|
||||
/// look like, and merges in the current toggle button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required SnackbarThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return SnackbarTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static SnackbarThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme = context.dependOnInheritedWidgetOfExactType<SnackbarTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).snackbarTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [SnackbarTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.snackbarTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// SnackbarThemeData theme = SnackbarTheme.of(context);
|
||||
/// ```
|
||||
static SnackbarThemeData of(BuildContext context) {
|
||||
return SnackbarThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return SnackbarTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(SnackbarTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
||||
|
||||
class SnackbarThemeData with Diagnosticable {
|
||||
final BoxDecoration? decoration;
|
||||
final ButtonThemeData? actionStyle;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const SnackbarThemeData({
|
||||
this.decoration,
|
||||
this.actionStyle,
|
||||
this.padding,
|
||||
});
|
||||
|
||||
factory SnackbarThemeData.standard(ThemeData style) {
|
||||
return SnackbarThemeData(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8.0 + style.visualDensity.vertical,
|
||||
horizontal: 16.0 + style.visualDensity.horizontal,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
color: style.brightness == Brightness.light
|
||||
? Colors.black
|
||||
: const Color(0xFF212121),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static SnackbarThemeData lerp(
|
||||
SnackbarThemeData? a,
|
||||
SnackbarThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return SnackbarThemeData(
|
||||
actionStyle: ButtonThemeData.lerp(a?.actionStyle, b?.actionStyle, t),
|
||||
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
|
||||
decoration: BoxDecoration.lerp(a?.decoration, b?.decoration, t),
|
||||
);
|
||||
}
|
||||
|
||||
SnackbarThemeData merge(SnackbarThemeData? style) {
|
||||
if (style == null) return this;
|
||||
return SnackbarThemeData(
|
||||
actionStyle: style.actionStyle ?? actionStyle,
|
||||
padding: style.padding ?? padding,
|
||||
decoration: style.decoration ?? decoration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<ButtonThemeData>(
|
||||
'actionStyle',
|
||||
actionStyle,
|
||||
ifNull: 'no style',
|
||||
));
|
||||
properties.add(DiagnosticsProperty('padding', padding));
|
||||
properties.add(DiagnosticsProperty('decoration', decoration));
|
||||
}
|
||||
}
|
||||
921
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/tooltip.dart
vendored
Normal file
921
dependencies/fluent_ui-3.12.0/lib/src/controls/surfaces/tooltip.dart
vendored
Normal file
@@ -0,0 +1,921 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// A tooltip is a short description that is linked to another
|
||||
/// control or object. Tooltips help users understand unfamiliar
|
||||
/// objects that aren't described directly in the UI. They display
|
||||
/// automatically when the user moves focus to, presses and holds,
|
||||
/// or hovers the mouse pointer over a control. The tooltip disappears
|
||||
/// after a few seconds, or when the user moves the finger, pointer
|
||||
/// or keyboard/gamepad focus.
|
||||
///
|
||||
/// 
|
||||
class Tooltip extends StatefulWidget {
|
||||
/// Creates a tooltip.
|
||||
///
|
||||
/// Wrap any widget in a [Tooltip] to show a message on mouse hover
|
||||
const Tooltip({
|
||||
Key? key,
|
||||
this.message,
|
||||
this.richMessage,
|
||||
this.child,
|
||||
this.style,
|
||||
this.excludeFromSemantics = false,
|
||||
this.useMousePosition = true,
|
||||
this.displayHorizontally = false,
|
||||
this.triggerMode,
|
||||
this.enableFeedback,
|
||||
}) : assert((message == null) != (richMessage == null),
|
||||
'Either `message` or `richMessage` must be specified'),
|
||||
super(key: key);
|
||||
|
||||
/// The text to display in the tooltip.
|
||||
///
|
||||
/// Only one of [message] and [richMessage] may be non-null.
|
||||
final String? message;
|
||||
|
||||
/// The rich text to display in the tooltip.
|
||||
///
|
||||
/// Only one of [message] and [richMessage] may be non-null.
|
||||
final InlineSpan? richMessage;
|
||||
|
||||
/// The widget the tooltip will be displayed, either above or below,
|
||||
/// when the mouse is hovering or whenever it gets long pressed.
|
||||
final Widget? child;
|
||||
|
||||
/// The style of the tooltip. If non-null, it's mescled with
|
||||
/// [ThemeData.tooltipThemeData]
|
||||
final TooltipThemeData? style;
|
||||
|
||||
/// Whether the tooltip's [message] should be excluded from the
|
||||
/// semantics tree.
|
||||
///
|
||||
/// Defaults to false. A tooltip will add a [Semantics] label that
|
||||
/// is set to [Tooltip.message]. Set this property to true if the
|
||||
/// app is going to provide its own custom semantics label.
|
||||
final bool excludeFromSemantics;
|
||||
|
||||
/// Whether the current mouse position should be used to render the
|
||||
/// tooltip on the screen. If no mouse is connected, this value is
|
||||
/// ignored.
|
||||
///
|
||||
/// Defaults to true. A tooltip will show the tooltip on the current
|
||||
/// mouse position and the tooltip will be removed as soon as the
|
||||
/// pointer exit the [child].
|
||||
final bool useMousePosition;
|
||||
|
||||
/// Whether the tooltip should be displayed at the left or right of
|
||||
/// the [child]. If true, [TooltipThemeData.preferBelow] is used as
|
||||
/// "preferLeft"
|
||||
final bool displayHorizontally;
|
||||
|
||||
/// The [TooltipTriggerMode] that will show the tooltip.
|
||||
///
|
||||
/// If this property is null, then [TooltipTriggerMode.longPress] is used
|
||||
final TooltipTriggerMode? triggerMode;
|
||||
|
||||
/// Whether the tooltip should provide acoustic and/or haptic feedback.
|
||||
///
|
||||
/// For example, on Android a tap will produce a clicking sound and a
|
||||
/// long-press will produce a short vibration, when feedback is enabled.
|
||||
///
|
||||
/// When null, the default value is true.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Feedback], for providing platform-specific feedback to certain actions.
|
||||
final bool? enableFeedback;
|
||||
|
||||
static final List<_TooltipState> _openedTooltips = <_TooltipState>[];
|
||||
|
||||
// Causes any current tooltips to be concealed. Only called for mouse hover enter
|
||||
// detections. Won't conceal the supplied tooltip.
|
||||
static void _concealOtherTooltips(_TooltipState current) {
|
||||
if (_openedTooltips.isNotEmpty) {
|
||||
// Avoid concurrent modification.
|
||||
final List<_TooltipState> openedTooltips = _openedTooltips.toList();
|
||||
for (final _TooltipState state in openedTooltips) {
|
||||
if (state == current) {
|
||||
continue;
|
||||
}
|
||||
state._concealTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Causes the most recently concealed tooltip to be revealed. Only called for mouse
|
||||
// hover exit detections.
|
||||
static void _revealLastTooltip() {
|
||||
if (_openedTooltips.isNotEmpty) {
|
||||
_openedTooltips.last._revealTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
/// Dismiss all of the tooltips that are currently shown on the screen.
|
||||
///
|
||||
/// This method returns true if it successfully dismisses the tooltips. It
|
||||
/// returns false if there is no tooltip shown on the screen.
|
||||
static bool dismissAllToolTips() {
|
||||
if (_openedTooltips.isNotEmpty) {
|
||||
// Avoid concurrent modification.
|
||||
final List<_TooltipState> openedTooltips = _openedTooltips.toList();
|
||||
for (final _TooltipState state in openedTooltips) {
|
||||
state._dismissTooltip(immediately: true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
_TooltipState createState() => _TooltipState();
|
||||
}
|
||||
|
||||
class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
|
||||
static const double _defaultVerticalOffset = 24.0;
|
||||
static const bool _defaultPreferBelow = true;
|
||||
static const EdgeInsetsGeometry _defaultMargin = EdgeInsets.zero;
|
||||
static const Duration _fadeInDuration = Duration(milliseconds: 150);
|
||||
static const Duration _fadeOutDuration = Duration(milliseconds: 75);
|
||||
static const Duration _defaultShowDuration = Duration(milliseconds: 1500);
|
||||
static const Duration _defaultHoverShowDuration = Duration(milliseconds: 100);
|
||||
static const Duration _defaultWaitDuration = Duration.zero;
|
||||
static const TooltipTriggerMode _defaultTriggerMode =
|
||||
TooltipTriggerMode.longPress;
|
||||
static const bool _defaultEnableFeedback = true;
|
||||
|
||||
late double height;
|
||||
late EdgeInsetsGeometry padding;
|
||||
late EdgeInsetsGeometry margin;
|
||||
late Decoration decoration;
|
||||
late TextStyle textStyle;
|
||||
late double verticalOffset;
|
||||
late bool preferBelow;
|
||||
late bool excludeFromSemantics;
|
||||
late AnimationController _controller;
|
||||
OverlayEntry? _entry;
|
||||
Timer? _dismissTimer;
|
||||
Timer? _showTimer;
|
||||
late Duration showDuration;
|
||||
late Duration hoverShowDuration;
|
||||
late Duration waitDuration;
|
||||
late bool _mouseIsConnected;
|
||||
bool _pressActivated = false;
|
||||
Offset? mousePosition;
|
||||
late TooltipTriggerMode triggerMode;
|
||||
late bool enableFeedback;
|
||||
late bool _isConcealed;
|
||||
late bool _forceRemoval;
|
||||
late bool _visible;
|
||||
|
||||
/// The plain text message for this tooltip.
|
||||
///
|
||||
/// This value will either come from [widget.message] or [widget.richMessage].
|
||||
String get _tooltipMessage =>
|
||||
widget.message ?? widget.richMessage!.toPlainText();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isConcealed = false;
|
||||
_forceRemoval = false;
|
||||
_mouseIsConnected = RendererBinding.instance.mouseTracker.mouseIsConnected;
|
||||
_controller = AnimationController(
|
||||
duration: _fadeInDuration,
|
||||
reverseDuration: _fadeOutDuration,
|
||||
vsync: this,
|
||||
)..addStatusListener(_handleStatusChanged);
|
||||
// Listen to see when a mouse is added.
|
||||
RendererBinding.instance.mouseTracker
|
||||
.addListener(_handleMouseTrackerChange);
|
||||
// Listen to global pointer events so that we can hide a tooltip immediately
|
||||
// if some other control is clicked on.
|
||||
GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_visible = TooltipVisibility.of(context);
|
||||
}
|
||||
|
||||
// https://material.io/components/tooltips#specs
|
||||
double _getDefaultTooltipHeight() {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return 24.0;
|
||||
default:
|
||||
return 32.0;
|
||||
}
|
||||
}
|
||||
|
||||
EdgeInsets _getDefaultPadding() {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return const EdgeInsets.symmetric(horizontal: 8.0);
|
||||
default:
|
||||
return const EdgeInsets.symmetric(horizontal: 16.0);
|
||||
}
|
||||
}
|
||||
|
||||
double _getDefaultFontSize() {
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.windows:
|
||||
return 10.0;
|
||||
default:
|
||||
return 14.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Forces a rebuild if a mouse has been added or removed.
|
||||
void _handleMouseTrackerChange() {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final bool mouseIsConnected =
|
||||
RendererBinding.instance.mouseTracker.mouseIsConnected;
|
||||
if (mouseIsConnected != _mouseIsConnected) {
|
||||
setState(() {
|
||||
_mouseIsConnected = mouseIsConnected;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _handleStatusChanged(AnimationStatus status) {
|
||||
// If this tip is concealed, don't remove it, even if it is dismissed, so that we can
|
||||
// reveal it later, unless it has explicitly been hidden with _dismissTooltip.
|
||||
if (status == AnimationStatus.dismissed &&
|
||||
(_forceRemoval || !_isConcealed)) {
|
||||
_removeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
void _dismissTooltip({bool immediately = false}) {
|
||||
_showTimer?.cancel();
|
||||
_showTimer = null;
|
||||
if (immediately) {
|
||||
_removeEntry();
|
||||
return;
|
||||
}
|
||||
// So it will be removed when it's done reversing, regardless of whether it is
|
||||
// still concealed or not.
|
||||
_forceRemoval = true;
|
||||
if (_pressActivated) {
|
||||
_dismissTimer ??= Timer(showDuration, _controller.reverse);
|
||||
} else {
|
||||
_dismissTimer ??= Timer(hoverShowDuration, _controller.reverse);
|
||||
}
|
||||
_pressActivated = false;
|
||||
}
|
||||
|
||||
void _showTooltip({bool immediately = false}) {
|
||||
_dismissTimer?.cancel();
|
||||
_dismissTimer = null;
|
||||
if (immediately) {
|
||||
ensureTooltipVisible();
|
||||
return;
|
||||
}
|
||||
_showTimer ??= Timer(waitDuration, ensureTooltipVisible);
|
||||
}
|
||||
|
||||
void _concealTooltip() {
|
||||
if (_isConcealed || _forceRemoval) {
|
||||
// Already concealed, or it's being removed.
|
||||
return;
|
||||
}
|
||||
_isConcealed = true;
|
||||
_dismissTimer?.cancel();
|
||||
_dismissTimer = null;
|
||||
_showTimer?.cancel();
|
||||
_showTimer = null;
|
||||
if (_entry != null) {
|
||||
_entry!.remove();
|
||||
}
|
||||
_controller.reverse();
|
||||
}
|
||||
|
||||
void _revealTooltip() {
|
||||
if (!_isConcealed) {
|
||||
// Already uncovered.
|
||||
return;
|
||||
}
|
||||
_isConcealed = false;
|
||||
_dismissTimer?.cancel();
|
||||
_dismissTimer = null;
|
||||
_showTimer?.cancel();
|
||||
_showTimer = null;
|
||||
if (!_entry!.mounted) {
|
||||
final OverlayState overlayState = Overlay.of(
|
||||
context,
|
||||
debugRequiredFor: widget,
|
||||
)!;
|
||||
overlayState.insert(_entry!);
|
||||
}
|
||||
SemanticsService.tooltip(_tooltipMessage);
|
||||
_controller.forward();
|
||||
}
|
||||
|
||||
/// Shows the tooltip if it is not already visible.
|
||||
///
|
||||
/// Returns `false` when the tooltip shouldn't be shown or when the tooltip
|
||||
/// was already visible.
|
||||
bool ensureTooltipVisible() {
|
||||
if (!_visible) return false;
|
||||
_showTimer?.cancel();
|
||||
_showTimer = null;
|
||||
_forceRemoval = false;
|
||||
if (_isConcealed) {
|
||||
if (_mouseIsConnected) {
|
||||
Tooltip._concealOtherTooltips(this);
|
||||
}
|
||||
_revealTooltip();
|
||||
return true;
|
||||
}
|
||||
if (_entry != null) {
|
||||
// Stop trying to hide, if we were.
|
||||
_dismissTimer?.cancel();
|
||||
_dismissTimer = null;
|
||||
_controller.forward();
|
||||
return false; // Already visible.
|
||||
}
|
||||
_createNewEntry();
|
||||
_controller.forward();
|
||||
return true;
|
||||
}
|
||||
|
||||
static final Set<_TooltipState> _mouseIn = <_TooltipState>{};
|
||||
|
||||
void _handleMouseEnter() {
|
||||
_showTooltip();
|
||||
}
|
||||
|
||||
void _handleMouseExit({bool immediately = true}) {
|
||||
// If the tip is currently covered, we can just remove it without waiting.
|
||||
_dismissTooltip(immediately: _isConcealed || immediately);
|
||||
}
|
||||
|
||||
void _createNewEntry() {
|
||||
final OverlayState overlayState = Overlay.of(
|
||||
context,
|
||||
debugRequiredFor: widget,
|
||||
)!;
|
||||
|
||||
final RenderBox box = context.findRenderObject()! as RenderBox;
|
||||
Offset target = box.localToGlobal(
|
||||
box.size.center(Offset.zero),
|
||||
ancestor: overlayState.context.findRenderObject(),
|
||||
);
|
||||
if (_mouseIsConnected && widget.useMousePosition && mousePosition != null) {
|
||||
target = mousePosition!;
|
||||
}
|
||||
|
||||
// We create this widget outside of the overlay entry's builder to prevent
|
||||
// updated values from happening to leak into the overlay when the overlay
|
||||
// rebuilds.
|
||||
final Widget overlay = Directionality(
|
||||
textDirection: Directionality.of(context),
|
||||
child: _TooltipOverlay(
|
||||
richMessage: widget.richMessage ?? TextSpan(text: widget.message),
|
||||
height: height,
|
||||
padding: padding,
|
||||
margin: margin,
|
||||
onEnter: _mouseIsConnected ? (_) => _handleMouseEnter() : null,
|
||||
onExit: _mouseIsConnected ? (_) => _handleMouseExit() : null,
|
||||
decoration: decoration,
|
||||
textStyle: textStyle,
|
||||
animation: CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.fastOutSlowIn,
|
||||
),
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
preferBelow: preferBelow,
|
||||
displayHorizontally: widget.displayHorizontally,
|
||||
),
|
||||
);
|
||||
_entry = OverlayEntry(builder: (BuildContext context) => overlay);
|
||||
_isConcealed = false;
|
||||
overlayState.insert(_entry!);
|
||||
SemanticsService.tooltip(_tooltipMessage);
|
||||
if (_mouseIsConnected) {
|
||||
// Hovered tooltips shouldn't show more than one at once. For example, a chip with
|
||||
// a delete icon shouldn't show both the delete icon tooltip and the chip tooltip
|
||||
// at the same time.
|
||||
Tooltip._concealOtherTooltips(this);
|
||||
}
|
||||
assert(!Tooltip._openedTooltips.contains(this));
|
||||
Tooltip._openedTooltips.add(this);
|
||||
}
|
||||
|
||||
void _removeEntry() {
|
||||
Tooltip._openedTooltips.remove(this);
|
||||
_mouseIn.remove(this);
|
||||
_dismissTimer?.cancel();
|
||||
_dismissTimer = null;
|
||||
_showTimer?.cancel();
|
||||
_showTimer = null;
|
||||
if (!_isConcealed) {
|
||||
_entry?.remove();
|
||||
}
|
||||
_isConcealed = false;
|
||||
_entry = null;
|
||||
if (_mouseIsConnected) {
|
||||
Tooltip._revealLastTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
void _handlePointerEvent(PointerEvent event) {
|
||||
if (_entry == null) {
|
||||
return;
|
||||
}
|
||||
if (event is PointerUpEvent || event is PointerCancelEvent) {
|
||||
_handleMouseExit();
|
||||
} else if (event is PointerDownEvent) {
|
||||
_handleMouseExit(immediately: true);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
if (_entry != null) {
|
||||
_dismissTooltip(immediately: true);
|
||||
}
|
||||
_showTimer?.cancel();
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
GestureBinding.instance.pointerRouter
|
||||
.removeGlobalRoute(_handlePointerEvent);
|
||||
RendererBinding.instance.mouseTracker
|
||||
.removeListener(_handleMouseTrackerChange);
|
||||
_removeEntry();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handlePress() {
|
||||
_pressActivated = true;
|
||||
final bool tooltipCreated = ensureTooltipVisible();
|
||||
if (tooltipCreated && enableFeedback) {
|
||||
if (triggerMode == TooltipTriggerMode.longPress) {
|
||||
Feedback.forLongPress(context);
|
||||
} else {
|
||||
Feedback.forTap(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If message is empty then no need to create a tooltip overlay to show
|
||||
// the empty black container so just return the wrapped child as is or
|
||||
// empty container if child is not specified.
|
||||
if (_tooltipMessage.isEmpty) {
|
||||
return widget.child ?? const SizedBox();
|
||||
}
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(Overlay.of(context, debugRequiredFor: widget) != null);
|
||||
final ThemeData theme = FluentTheme.of(context);
|
||||
final TooltipThemeData tooltipTheme =
|
||||
TooltipTheme.of(context).merge(widget.style);
|
||||
final TextStyle defaultTextStyle;
|
||||
final BoxDecoration defaultDecoration;
|
||||
defaultTextStyle = theme.typography.body!.copyWith(
|
||||
color: theme.brightness == Brightness.dark ? Colors.black : Colors.white,
|
||||
fontSize: _getDefaultFontSize(),
|
||||
);
|
||||
defaultDecoration = BoxDecoration(
|
||||
color: theme.menuColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
);
|
||||
|
||||
height = tooltipTheme.height ?? _getDefaultTooltipHeight();
|
||||
padding = tooltipTheme.padding ?? _getDefaultPadding();
|
||||
margin = tooltipTheme.margin ?? _defaultMargin;
|
||||
verticalOffset = tooltipTheme.verticalOffset ?? _defaultVerticalOffset;
|
||||
preferBelow = tooltipTheme.preferBelow ?? _defaultPreferBelow;
|
||||
excludeFromSemantics = widget.excludeFromSemantics;
|
||||
decoration = tooltipTheme.decoration ?? defaultDecoration;
|
||||
textStyle = tooltipTheme.textStyle ?? defaultTextStyle;
|
||||
waitDuration = tooltipTheme.waitDuration ?? _defaultWaitDuration;
|
||||
showDuration = tooltipTheme.showDuration ?? _defaultShowDuration;
|
||||
hoverShowDuration = tooltipTheme.showDuration ?? _defaultHoverShowDuration;
|
||||
triggerMode = widget.triggerMode ?? _defaultTriggerMode;
|
||||
enableFeedback = widget.enableFeedback ?? _defaultEnableFeedback;
|
||||
|
||||
Widget result = Semantics(
|
||||
label: excludeFromSemantics ? null : _tooltipMessage,
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
// Only check for gestures if tooltip should be visible.
|
||||
if (_visible) {
|
||||
result = GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPress:
|
||||
(triggerMode == TooltipTriggerMode.longPress) ? _handlePress : null,
|
||||
onTap: (triggerMode == TooltipTriggerMode.tap) ? _handlePress : null,
|
||||
excludeFromSemantics: true,
|
||||
child: result,
|
||||
);
|
||||
// Only check for hovering if there is a mouse connected.
|
||||
if (_mouseIsConnected) {
|
||||
result = MouseRegion(
|
||||
onEnter: (_) => _handleMouseEnter(),
|
||||
onHover: (event) {
|
||||
mousePosition = event.position;
|
||||
},
|
||||
onExit: (_) => _handleMouseExit(),
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [Tooltip]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [Tooltip] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class TooltipTheme extends InheritedTheme {
|
||||
/// Creates a tooltip theme that controls the configurations for
|
||||
/// [Tooltip].
|
||||
const TooltipTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [Tooltip] widgets.
|
||||
final TooltipThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [InfoBar]s should
|
||||
/// look like, and merges in the current toggle button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required TooltipThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return TooltipTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static TooltipThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme = context.dependOnInheritedWidgetOfExactType<TooltipTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).tooltipTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [TooltipTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.tooltipTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// TooltipThemeData theme = TooltipTheme.of(context);
|
||||
/// ```
|
||||
static TooltipThemeData of(BuildContext context) {
|
||||
return TooltipThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return TooltipTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(TooltipTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
||||
|
||||
class TooltipThemeData with Diagnosticable {
|
||||
/// The height of the tooltip's [child].
|
||||
///
|
||||
/// If the [child] is null, then this is the tooltip's intrinsic height.
|
||||
final double? height;
|
||||
|
||||
/// The vertical gap between the widget and the displayed tooltip.
|
||||
///
|
||||
/// When [preferBelow] is set to true and tooltips have sufficient space
|
||||
/// to display themselves, this property defines how much vertical space
|
||||
/// tooltips will position themselves under their corresponding widgets.
|
||||
/// Otherwise, tooltips will position themselves above their corresponding
|
||||
/// widgets with the given offset.
|
||||
final double? verticalOffset;
|
||||
|
||||
/// The amount of space by which to inset the tooltip's [child].
|
||||
///
|
||||
/// Defaults to 10.0 logical pixels in each direction.
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// The empty space that surrounds the tooltip.
|
||||
///
|
||||
/// Defines the tooltip's outer [Container.margin]. By default, a long
|
||||
/// tooltip will span the width of its window. If long enough, a tooltip
|
||||
/// might also span the window's height. This property allows one to define
|
||||
/// how much space the tooltip must be inset from the edges of their display
|
||||
/// window.
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
/// Whether the tooltip defaults to being displayed below the widget.
|
||||
///
|
||||
/// Defaults to true. If there is insufficient space to display the tooltip
|
||||
/// in the preferred direction, the tooltip will be displayed in the opposite
|
||||
/// direction.
|
||||
final bool? preferBelow;
|
||||
|
||||
/// Specifies the tooltip's shape and background color.
|
||||
///
|
||||
/// The tooltip shape defaults to a rounded rectangle with a border radius of 4.0.
|
||||
/// Tooltips will also default to an opacity of 90% and with the color [Colors.grey]
|
||||
/// if [ThemeData.brightness] is [Brightness.dark], and [Colors.white] if it is
|
||||
/// [Brightness.light].
|
||||
final Decoration? decoration;
|
||||
|
||||
/// The length of time that a pointer must hover over a tooltip's widget before
|
||||
/// the tooltip will be shown.
|
||||
///
|
||||
/// Once the pointer leaves the widget, the tooltip will immediately disappear.
|
||||
///
|
||||
/// Defaults to 0 milliseconds (tooltips are shown immediately upon hover).
|
||||
final Duration? waitDuration;
|
||||
|
||||
/// The length of time that the tooltip will be shown after a long press is released.
|
||||
///
|
||||
/// Defaults to 1.5 seconds.
|
||||
final Duration? showDuration;
|
||||
|
||||
/// The style to use for the message of the tooltip.
|
||||
///
|
||||
/// If null, [Typography.caption] is used
|
||||
final TextStyle? textStyle;
|
||||
|
||||
const TooltipThemeData({
|
||||
this.height,
|
||||
this.verticalOffset,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.preferBelow,
|
||||
this.decoration,
|
||||
this.showDuration,
|
||||
this.waitDuration,
|
||||
this.textStyle,
|
||||
});
|
||||
|
||||
factory TooltipThemeData.standard(ThemeData style) {
|
||||
return TooltipThemeData(
|
||||
height: 32.0,
|
||||
verticalOffset: 24.0,
|
||||
preferBelow: false,
|
||||
margin: EdgeInsets.zero,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
showDuration: const Duration(milliseconds: 1500),
|
||||
waitDuration: const Duration(seconds: 1),
|
||||
textStyle: style.typography.caption,
|
||||
decoration: () {
|
||||
final radius = BorderRadius.circular(4.0);
|
||||
final shadow = [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
offset: const Offset(1, 1),
|
||||
blurRadius: 10.0,
|
||||
),
|
||||
];
|
||||
if (style.brightness == Brightness.light) {
|
||||
return BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: radius,
|
||||
boxShadow: shadow,
|
||||
);
|
||||
} else {
|
||||
return BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: radius,
|
||||
boxShadow: shadow,
|
||||
);
|
||||
}
|
||||
}(),
|
||||
);
|
||||
}
|
||||
|
||||
static TooltipThemeData lerp(
|
||||
TooltipThemeData? a,
|
||||
TooltipThemeData? b,
|
||||
double t,
|
||||
) {
|
||||
return TooltipThemeData(
|
||||
decoration: Decoration.lerp(a?.decoration, b?.decoration, t),
|
||||
height: lerpDouble(a?.height, b?.height, t),
|
||||
margin: EdgeInsetsGeometry.lerp(a?.margin, b?.margin, t),
|
||||
padding: EdgeInsetsGeometry.lerp(a?.padding, b?.padding, t),
|
||||
preferBelow: t < 0.5 ? a?.preferBelow : b?.preferBelow,
|
||||
showDuration: lerpDuration(a?.showDuration ?? Duration.zero,
|
||||
b?.showDuration ?? Duration.zero, t),
|
||||
textStyle: TextStyle.lerp(a?.textStyle, b?.textStyle, t),
|
||||
verticalOffset: lerpDouble(a?.verticalOffset, b?.verticalOffset, t),
|
||||
waitDuration: lerpDuration(a?.waitDuration ?? Duration.zero,
|
||||
b?.waitDuration ?? Duration.zero, t),
|
||||
);
|
||||
}
|
||||
|
||||
TooltipThemeData merge(TooltipThemeData? style) {
|
||||
if (style == null) return this;
|
||||
return TooltipThemeData(
|
||||
decoration: style.decoration ?? decoration,
|
||||
height: style.height ?? height,
|
||||
margin: style.margin ?? margin,
|
||||
padding: style.padding ?? padding,
|
||||
preferBelow: style.preferBelow ?? preferBelow,
|
||||
showDuration: style.showDuration ?? showDuration,
|
||||
textStyle: style.textStyle ?? textStyle,
|
||||
verticalOffset: style.verticalOffset ?? verticalOffset,
|
||||
waitDuration: style.waitDuration ?? waitDuration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DoubleProperty('height', height));
|
||||
properties.add(DoubleProperty('verticalOffset', verticalOffset));
|
||||
properties.add(
|
||||
DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<EdgeInsetsGeometry>('margin', margin),
|
||||
);
|
||||
properties.add(FlagProperty(
|
||||
'preferBelow',
|
||||
value: preferBelow,
|
||||
ifFalse: 'prefer above',
|
||||
));
|
||||
properties.add(DiagnosticsProperty<Decoration>('decoration', decoration));
|
||||
properties.add(DiagnosticsProperty<Duration>('waitDuration', waitDuration));
|
||||
properties.add(DiagnosticsProperty<Duration>('showDuration', showDuration));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('textStyle', textStyle));
|
||||
}
|
||||
}
|
||||
|
||||
/// A delegate for computing the layout of a tooltip to be displayed above or
|
||||
/// bellow a target specified in the global coordinate system.
|
||||
class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
|
||||
/// Creates a delegate for computing the layout of a tooltip.
|
||||
///
|
||||
/// The arguments must not be null.
|
||||
const _TooltipPositionDelegate({
|
||||
required this.target,
|
||||
required this.verticalOffset,
|
||||
required this.preferBelow,
|
||||
required this.horizontal,
|
||||
});
|
||||
|
||||
/// The offset of the target the tooltip is positioned near in the global
|
||||
/// coordinate system.
|
||||
final Offset target;
|
||||
|
||||
/// The amount of vertical distance between the target and the displayed
|
||||
/// tooltip.
|
||||
final double verticalOffset;
|
||||
|
||||
/// Whether the tooltip is displayed below its widget by default.
|
||||
///
|
||||
/// If there is insufficient space to display the tooltip in the preferred
|
||||
/// direction, the tooltip will be displayed in the opposite direction.
|
||||
final bool preferBelow;
|
||||
|
||||
final bool horizontal;
|
||||
|
||||
@override
|
||||
BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>
|
||||
constraints.loosen();
|
||||
|
||||
@override
|
||||
Offset getPositionForChild(Size size, Size childSize) {
|
||||
if (horizontal) {
|
||||
return horizontalPositionDependentBox(
|
||||
size: size,
|
||||
childSize: childSize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
preferLeft: preferBelow,
|
||||
);
|
||||
} else {
|
||||
return positionDependentBox(
|
||||
size: size,
|
||||
childSize: childSize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
preferBelow: preferBelow,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_TooltipPositionDelegate oldDelegate) {
|
||||
return target != oldDelegate.target ||
|
||||
verticalOffset != oldDelegate.verticalOffset ||
|
||||
preferBelow != oldDelegate.preferBelow;
|
||||
}
|
||||
}
|
||||
|
||||
class _TooltipOverlay extends StatelessWidget {
|
||||
const _TooltipOverlay({
|
||||
Key? key,
|
||||
required this.height,
|
||||
required this.richMessage,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.decoration,
|
||||
this.textStyle,
|
||||
required this.animation,
|
||||
required this.target,
|
||||
required this.verticalOffset,
|
||||
required this.preferBelow,
|
||||
this.displayHorizontally = false,
|
||||
this.onEnter,
|
||||
this.onExit,
|
||||
}) : super(key: key);
|
||||
|
||||
final InlineSpan richMessage;
|
||||
final double height;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final Decoration? decoration;
|
||||
final TextStyle? textStyle;
|
||||
final Animation<double> animation;
|
||||
final Offset target;
|
||||
final double verticalOffset;
|
||||
final bool preferBelow;
|
||||
final bool displayHorizontally;
|
||||
final PointerEnterEventListener? onEnter;
|
||||
final PointerExitEventListener? onExit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget result = IgnorePointer(
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: height),
|
||||
child: DefaultTextStyle(
|
||||
style: FluentTheme.of(context).typography.body!,
|
||||
child: Container(
|
||||
decoration: decoration,
|
||||
padding: padding,
|
||||
margin: margin,
|
||||
child: Center(
|
||||
widthFactor: 1.0,
|
||||
heightFactor: 1.0,
|
||||
child: Text.rich(
|
||||
richMessage,
|
||||
style: textStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
if (onEnter != null || onExit != null) {
|
||||
result = MouseRegion(
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
return Positioned.fill(
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: _TooltipPositionDelegate(
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
preferBelow: preferBelow,
|
||||
horizontal: displayHorizontally,
|
||||
),
|
||||
child: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
189
dependencies/fluent_ui-3.12.0/lib/src/controls/utils/divider.dart
vendored
Normal file
189
dependencies/fluent_ui-3.12.0/lib/src/controls/utils/divider.dart
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class Divider extends StatelessWidget {
|
||||
/// Creates a divider.
|
||||
const Divider({
|
||||
Key? key,
|
||||
this.direction = Axis.horizontal,
|
||||
this.style,
|
||||
this.size,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The current direction of the slider. Uses [Axis.horizontal] by default
|
||||
final Axis direction;
|
||||
|
||||
/// The `style` of the divider. It's mescled with [ThemeData.dividerThemeData]
|
||||
final DividerThemeData? style;
|
||||
|
||||
/// The size of the divider. The opposite of the [DividerThemeData.thickness]
|
||||
final double? size;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DoubleProperty(
|
||||
'size',
|
||||
size,
|
||||
ifNull: 'indeterminate',
|
||||
defaultValue: 1.0,
|
||||
));
|
||||
properties.add(DiagnosticsProperty('style', style));
|
||||
properties.add(EnumProperty(
|
||||
'direction',
|
||||
direction,
|
||||
defaultValue: Axis.horizontal,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final style = DividerTheme.of(context).merge(this.style);
|
||||
return Container(
|
||||
height: direction == Axis.horizontal ? style.thickness : size,
|
||||
width: direction == Axis.vertical ? style.thickness : size,
|
||||
margin: direction == Axis.horizontal
|
||||
? style.horizontalMargin
|
||||
: style.verticalMargin,
|
||||
decoration: style.decoration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [Divider]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [Divider] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class DividerTheme extends InheritedTheme {
|
||||
/// Creates a divider theme that controls the configurations for
|
||||
/// [Divider].
|
||||
const DividerTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [Divider] widgets.
|
||||
final DividerThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [Divider]s should
|
||||
/// look like, and merges in the current toggle button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required DividerThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return DividerTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static DividerThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme = context.dependOnInheritedWidgetOfExactType<DividerTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).dividerTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [DividerTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.dividerTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// DividerThemeData theme = DividerTheme.of(context);
|
||||
/// ```
|
||||
static DividerThemeData of(BuildContext context) {
|
||||
return DividerThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return DividerTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(DividerTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class DividerThemeData with Diagnosticable {
|
||||
/// The thickness of the style.
|
||||
///
|
||||
/// If it's horizontal, it corresponds to the divider
|
||||
/// `height`, otherwise it corresponds to its `width`
|
||||
final double? thickness;
|
||||
|
||||
/// The decoration of the style. If null, defaults to a
|
||||
/// [BoxDecoration] with a `Color(0xFFB7B7B7)` for light
|
||||
/// mode and `Color(0xFF484848)` for dark mode
|
||||
final Decoration? decoration;
|
||||
|
||||
/// The vertical margin of the style.
|
||||
final EdgeInsetsGeometry? verticalMargin;
|
||||
|
||||
/// The horizontal margin of the style.
|
||||
final EdgeInsetsGeometry? horizontalMargin;
|
||||
|
||||
const DividerThemeData({
|
||||
this.thickness,
|
||||
this.decoration,
|
||||
this.verticalMargin,
|
||||
this.horizontalMargin,
|
||||
});
|
||||
|
||||
factory DividerThemeData.standard(ThemeData style) {
|
||||
return DividerThemeData(
|
||||
thickness: 1,
|
||||
horizontalMargin: const EdgeInsets.symmetric(horizontal: 10),
|
||||
verticalMargin: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: () {
|
||||
if (style.brightness == Brightness.light) {
|
||||
return const BoxDecoration(color: Color(0xFFB7B7B7));
|
||||
} else {
|
||||
return const BoxDecoration(color: Color(0xFF484848));
|
||||
}
|
||||
}(),
|
||||
);
|
||||
}
|
||||
|
||||
static DividerThemeData lerp(
|
||||
DividerThemeData? a, DividerThemeData? b, double t) {
|
||||
return DividerThemeData(
|
||||
decoration: Decoration.lerp(a?.decoration, b?.decoration, t),
|
||||
thickness: lerpDouble(a?.thickness, b?.thickness, t),
|
||||
horizontalMargin:
|
||||
EdgeInsetsGeometry.lerp(a?.horizontalMargin, b?.horizontalMargin, t),
|
||||
verticalMargin:
|
||||
EdgeInsetsGeometry.lerp(a?.verticalMargin, b?.verticalMargin, t),
|
||||
);
|
||||
}
|
||||
|
||||
DividerThemeData merge(DividerThemeData? style) {
|
||||
if (style == null) return this;
|
||||
return DividerThemeData(
|
||||
decoration: style.decoration ?? decoration,
|
||||
thickness: style.thickness ?? thickness,
|
||||
horizontalMargin: style.horizontalMargin ?? horizontalMargin,
|
||||
verticalMargin: style.verticalMargin ?? verticalMargin,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<Decoration>('decoration', decoration));
|
||||
properties.add(DiagnosticsProperty('horizontalMargin', horizontalMargin));
|
||||
properties.add(DiagnosticsProperty('verticalMargin', verticalMargin));
|
||||
properties.add(DoubleProperty('thickness', thickness, defaultValue: 1.0));
|
||||
}
|
||||
}
|
||||
326
dependencies/fluent_ui-3.12.0/lib/src/controls/utils/hover_button.dart
vendored
Normal file
326
dependencies/fluent_ui-3.12.0/lib/src/controls/utils/hover_button.dart
vendored
Normal file
@@ -0,0 +1,326 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
typedef ButtonStateWidgetBuilder = Widget Function(
|
||||
BuildContext,
|
||||
Set<ButtonStates> state,
|
||||
);
|
||||
|
||||
/// Base widget for any widget that requires input. It
|
||||
/// provides a [builder] callback to build the child with
|
||||
/// the current input state: none, hovering, pressing or
|
||||
/// focused.
|
||||
///
|
||||
/// It's used by the following widgets:
|
||||
/// - [Button]
|
||||
/// - [Checkbox]
|
||||
/// - [ComboBox]
|
||||
/// - [DatePicker]
|
||||
/// - [IconButton]
|
||||
/// - [RadioButton]
|
||||
/// - [TabView]'s [Tab]
|
||||
/// - [TappableListTile]
|
||||
/// - [TimePicker]
|
||||
/// - [ToggleSwitch]
|
||||
class HoverButton extends StatefulWidget {
|
||||
/// Creates a hover button.
|
||||
const HoverButton({
|
||||
Key? key,
|
||||
required this.builder,
|
||||
this.cursor,
|
||||
this.onPressed,
|
||||
this.onLongPress,
|
||||
this.focusNode,
|
||||
this.margin,
|
||||
this.semanticLabel,
|
||||
this.onTapDown,
|
||||
this.onTapUp,
|
||||
this.onTapCancel,
|
||||
this.onLongPressEnd,
|
||||
this.onLongPressStart,
|
||||
this.onHorizontalDragStart,
|
||||
this.onHorizontalDragUpdate,
|
||||
this.onHorizontalDragEnd,
|
||||
this.onFocusChange,
|
||||
this.autofocus = false,
|
||||
this.actionsEnabled = true,
|
||||
this.focusEnabled = true,
|
||||
}) : super(key: key);
|
||||
|
||||
/// {@template fluent_ui.controls.inputs.HoverButton.mouseCursor}
|
||||
/// The cursor for a mouse pointer when it enters or is hovering over the
|
||||
/// widget.
|
||||
///
|
||||
/// The [mouseCursor] defaults to [MouseCursor.defer], deferring the choice of
|
||||
/// cursor to the next region behind it in hit-test order.
|
||||
/// {@endtemplate}
|
||||
final MouseCursor? cursor;
|
||||
final VoidCallback? onLongPress;
|
||||
final VoidCallback? onLongPressStart;
|
||||
final VoidCallback? onLongPressEnd;
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onTapUp;
|
||||
final VoidCallback? onTapDown;
|
||||
final VoidCallback? onTapCancel;
|
||||
|
||||
final GestureDragStartCallback? onHorizontalDragStart;
|
||||
final GestureDragUpdateCallback? onHorizontalDragUpdate;
|
||||
final GestureDragEndCallback? onHorizontalDragEnd;
|
||||
|
||||
final ButtonStateWidgetBuilder builder;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// The margin created around this button. The margin is added
|
||||
/// around the [Semantics] widget, if any.
|
||||
final EdgeInsetsGeometry? margin;
|
||||
|
||||
/// {@template fluent_ui.controls.inputs.HoverButton.semanticLabel}
|
||||
/// Semantic label for the input.
|
||||
///
|
||||
/// Announced in accessibility modes (e.g TalkBack/VoiceOver).
|
||||
/// This label does not show in the UI.
|
||||
///
|
||||
/// * [SemanticsProperties.label], which is set to [semanticLabel] in the
|
||||
/// underlying [Semantics] widget.
|
||||
///
|
||||
/// If null, no [Semantics] widget is added to the tree
|
||||
/// {@endtemplate}
|
||||
final String? semanticLabel;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
final ValueChanged<bool>? onFocusChange;
|
||||
|
||||
/// Whether actions and shortcuts are enabled
|
||||
final bool actionsEnabled;
|
||||
|
||||
/// Whether the focus is enabled.
|
||||
///
|
||||
/// If disabled, actions and shortcurts will not work, regardless of what is
|
||||
/// set on [actionsEnabled].
|
||||
final bool focusEnabled;
|
||||
|
||||
@override
|
||||
_HoverButtonState createState() => _HoverButtonState();
|
||||
}
|
||||
|
||||
class _HoverButtonState extends State<HoverButton> {
|
||||
late FocusNode node;
|
||||
|
||||
late Map<Type, Action<Intent>> _actionMap;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
node = widget.focusNode ?? _createFocusNode();
|
||||
void _handleActionTap() async {
|
||||
if (!enabled) return;
|
||||
setState(() => _pressing = true);
|
||||
widget.onPressed?.call();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (mounted) setState(() => _pressing = false);
|
||||
}
|
||||
|
||||
_actionMap = <Type, Action<Intent>>{
|
||||
ActivateIntent: CallbackAction<ActivateIntent>(
|
||||
onInvoke: (ActivateIntent intent) => _handleActionTap(),
|
||||
),
|
||||
ButtonActivateIntent: CallbackAction<ButtonActivateIntent>(
|
||||
onInvoke: (ButtonActivateIntent intent) => _handleActionTap(),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(HoverButton oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.focusNode != oldWidget.focusNode) {
|
||||
node = widget.focusNode ?? node;
|
||||
}
|
||||
}
|
||||
|
||||
FocusNode _createFocusNode() {
|
||||
return FocusNode(debugLabel: '${widget.runtimeType}');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.focusNode == null) node.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool _hovering = false;
|
||||
bool _pressing = false;
|
||||
bool _shouldShowFocus = false;
|
||||
|
||||
bool get enabled =>
|
||||
widget.onPressed != null ||
|
||||
widget.onTapUp != null ||
|
||||
widget.onTapDown != null ||
|
||||
widget.onTapDown != null ||
|
||||
widget.onLongPress != null ||
|
||||
widget.onLongPressStart != null ||
|
||||
widget.onLongPressEnd != null;
|
||||
|
||||
Set<ButtonStates> get states {
|
||||
if (!enabled) return {ButtonStates.disabled};
|
||||
return {
|
||||
if (_pressing) ButtonStates.pressing,
|
||||
if (_hovering) ButtonStates.hovering,
|
||||
if (_shouldShowFocus) ButtonStates.focused,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget w = GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: enabled ? widget.onPressed : null,
|
||||
onTapDown: (_) {
|
||||
if (!enabled) return;
|
||||
if (mounted) setState(() => _pressing = true);
|
||||
widget.onTapDown?.call();
|
||||
},
|
||||
onTapUp: (_) async {
|
||||
if (!enabled) return;
|
||||
widget.onTapUp?.call();
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (mounted) setState(() => _pressing = false);
|
||||
},
|
||||
onTapCancel: () {
|
||||
if (!enabled) return;
|
||||
widget.onTapCancel?.call();
|
||||
if (mounted) setState(() => _pressing = false);
|
||||
},
|
||||
onLongPress: enabled ? widget.onLongPress : null,
|
||||
onLongPressStart: (_) {
|
||||
if (!enabled) return;
|
||||
widget.onLongPressStart?.call();
|
||||
if (mounted) setState(() => _pressing = true);
|
||||
},
|
||||
onLongPressEnd: (_) {
|
||||
if (!enabled) return;
|
||||
widget.onLongPressEnd?.call();
|
||||
if (mounted) setState(() => _pressing = false);
|
||||
},
|
||||
onHorizontalDragStart: widget.onHorizontalDragStart,
|
||||
onHorizontalDragUpdate: widget.onHorizontalDragUpdate,
|
||||
onHorizontalDragEnd: widget.onHorizontalDragEnd,
|
||||
child: widget.builder(context, states),
|
||||
);
|
||||
if (widget.focusEnabled) {
|
||||
w = FocusableActionDetector(
|
||||
mouseCursor: widget.cursor ?? MouseCursor.defer,
|
||||
focusNode: node,
|
||||
autofocus: widget.autofocus,
|
||||
enabled: enabled,
|
||||
actions: widget.actionsEnabled ? _actionMap : {},
|
||||
onFocusChange: widget.onFocusChange,
|
||||
onShowFocusHighlight: (v) {
|
||||
if (mounted) setState(() => _shouldShowFocus = v);
|
||||
},
|
||||
onShowHoverHighlight: (v) {
|
||||
if (mounted) setState(() => _hovering = v);
|
||||
},
|
||||
child: w,
|
||||
);
|
||||
} else {
|
||||
w = MouseRegion(
|
||||
onEnter: (e) {
|
||||
if (mounted) setState(() => _hovering = true);
|
||||
},
|
||||
onExit: (e) {
|
||||
if (mounted) setState(() => _hovering = false);
|
||||
},
|
||||
child: w,
|
||||
);
|
||||
}
|
||||
w = MergeSemantics(
|
||||
child: Semantics(
|
||||
label: widget.semanticLabel,
|
||||
button: true,
|
||||
enabled: enabled,
|
||||
focusable: enabled && node.canRequestFocus,
|
||||
focused: node.hasFocus,
|
||||
child: w,
|
||||
),
|
||||
);
|
||||
if (widget.margin != null) w = Padding(padding: widget.margin!, child: w);
|
||||
return w;
|
||||
}
|
||||
}
|
||||
|
||||
enum ButtonStates { disabled, hovering, pressing, focused, none }
|
||||
|
||||
// typedef ButtonState<T> = T Function(Set<ButtonStates>);
|
||||
|
||||
/// Signature for the function that returns a value of type `T` based on a given
|
||||
/// set of states.
|
||||
typedef ButtonStateResolver<T> = T Function(Set<ButtonStates> states);
|
||||
|
||||
abstract class ButtonState<T> {
|
||||
T resolve(Set<ButtonStates> states);
|
||||
|
||||
static ButtonState<T> all<T>(T value) => _AllButtonState(value);
|
||||
|
||||
static ButtonState<T> resolveWith<T>(ButtonStateResolver<T> callback) {
|
||||
return _ButtonState(callback);
|
||||
}
|
||||
|
||||
static ButtonState<T?>? lerp<T>(
|
||||
ButtonState<T?>? a,
|
||||
ButtonState<T?>? b,
|
||||
double t,
|
||||
T? Function(T?, T?, double) lerpFunction,
|
||||
) {
|
||||
if (a == null && b == null) return null;
|
||||
return _LerpProperties<T>(a, b, t, lerpFunction);
|
||||
}
|
||||
}
|
||||
|
||||
class _ButtonState<T> extends ButtonState<T> {
|
||||
_ButtonState(this._resolve);
|
||||
|
||||
final ButtonStateResolver<T> _resolve;
|
||||
|
||||
@override
|
||||
T resolve(Set<ButtonStates> states) => _resolve(states);
|
||||
}
|
||||
|
||||
class _AllButtonState<T> extends ButtonState<T> {
|
||||
_AllButtonState(this._value);
|
||||
|
||||
final T _value;
|
||||
|
||||
@override
|
||||
T resolve(states) => _value;
|
||||
}
|
||||
|
||||
class _LerpProperties<T> implements ButtonState<T?> {
|
||||
const _LerpProperties(this.a, this.b, this.t, this.lerpFunction);
|
||||
|
||||
final ButtonState<T?>? a;
|
||||
final ButtonState<T?>? b;
|
||||
final double t;
|
||||
final T? Function(T?, T?, double) lerpFunction;
|
||||
|
||||
@override
|
||||
T? resolve(Set<ButtonStates> states) {
|
||||
final T? resolvedA = a?.resolve(states);
|
||||
final T? resolvedB = b?.resolve(states);
|
||||
return lerpFunction(resolvedA, resolvedB, t);
|
||||
}
|
||||
}
|
||||
|
||||
extension ButtonStatesExtension on Set<ButtonStates> {
|
||||
bool get isFocused => contains(ButtonStates.focused);
|
||||
bool get isDisabled => contains(ButtonStates.disabled);
|
||||
bool get isPressing => contains(ButtonStates.pressing);
|
||||
bool get isHovering => contains(ButtonStates.hovering);
|
||||
bool get isNone => isEmpty;
|
||||
}
|
||||
83
dependencies/fluent_ui-3.12.0/lib/src/controls/utils/info_badge.dart
vendored
Normal file
83
dependencies/fluent_ui-3.12.0/lib/src/controls/utils/info_badge.dart
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// An InfoBadge is a small piece of UI that can be added
|
||||
/// into an app and customized to display a number, icon,
|
||||
/// or a simple dot.
|
||||
///
|
||||
/// Learn more:
|
||||
///
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/controls/info-badge>
|
||||
class InfoBadge extends StatelessWidget {
|
||||
/// Creates an info badge.
|
||||
const InfoBadge({
|
||||
Key? key,
|
||||
this.source,
|
||||
this.color,
|
||||
this.foregroundColor,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The source of the badge.
|
||||
///
|
||||
/// Usually a [Text] or an [Icon]
|
||||
final Widget? source;
|
||||
|
||||
/// The background color of the badge. If null, the current
|
||||
/// [FluentTheme.accentColor] is used
|
||||
///
|
||||
/// Some other used colors are:
|
||||
///
|
||||
/// * [Colors.errorPrimaryColor]
|
||||
/// * [Colors.successPrimaryColor]
|
||||
/// * [Colors.warningPrimaryColor]
|
||||
final Color? color;
|
||||
|
||||
/// The foreground color.
|
||||
///
|
||||
/// Applied to [Text]s and [Icon]s
|
||||
final Color? foregroundColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
|
||||
final theme = FluentTheme.of(context);
|
||||
final color = this.color ??
|
||||
theme.accentColor.resolveFromReverseBrightness(
|
||||
theme.brightness,
|
||||
level: 1,
|
||||
);
|
||||
|
||||
return Container(
|
||||
constraints: source == null
|
||||
? const BoxConstraints(
|
||||
maxWidth: 10.0,
|
||||
maxHeight: 10.0,
|
||||
)
|
||||
: const BoxConstraints(
|
||||
minWidth: 16.0,
|
||||
minHeight: 16.0,
|
||||
maxHeight: 16.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(100),
|
||||
),
|
||||
child: source == null
|
||||
? null
|
||||
: DefaultTextStyle(
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: foregroundColor ?? color.basedOnLuminance(),
|
||||
fontSize: 11.0,
|
||||
),
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(
|
||||
color: foregroundColor ?? color.basedOnLuminance(),
|
||||
size: 8.0,
|
||||
),
|
||||
child: source!,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
488
dependencies/fluent_ui-3.12.0/lib/src/controls/utils/scrollbar.dart
vendored
Normal file
488
dependencies/fluent_ui-3.12.0/lib/src/controls/utils/scrollbar.dart
vendored
Normal file
@@ -0,0 +1,488 @@
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
/// {@macro flutter.widgets.Scrollbar}
|
||||
class Scrollbar extends RawScrollbar {
|
||||
/// Creates a fluent-styled scrollbar that wraps the given [child].
|
||||
///
|
||||
/// The [child], or a descendant of the [child], should be a
|
||||
/// source of [ScrollNotification] notifications, typically a
|
||||
/// [Scrollable] widget.
|
||||
///
|
||||
/// The [child], [fadeDuration], and [timeToFade] arguments must not be null.
|
||||
const Scrollbar({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
ScrollController? controller,
|
||||
bool thumbVisibility = true,
|
||||
this.style,
|
||||
Duration fadeDuration = const Duration(milliseconds: 300),
|
||||
Duration timeToFade = const Duration(milliseconds: 600),
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
thumbVisibility: thumbVisibility,
|
||||
controller: controller,
|
||||
timeToFade: timeToFade,
|
||||
fadeDuration: fadeDuration,
|
||||
);
|
||||
|
||||
/// The style applied to the scroll bar. If non-null, it's mescled
|
||||
/// with [ThemeData.scrollbarThemeData]
|
||||
final ScrollbarThemeData? style;
|
||||
|
||||
@override
|
||||
_ScrollbarState createState() => _ScrollbarState();
|
||||
}
|
||||
|
||||
class _ScrollbarState extends RawScrollbarState<Scrollbar> {
|
||||
late AnimationController _hoverController;
|
||||
late ScrollbarThemeData _scrollbarTheme;
|
||||
bool _dragIsActive = false;
|
||||
bool _hoverIsActive = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_hoverController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 90),
|
||||
);
|
||||
_hoverController.addListener(() {
|
||||
updateScrollbarPainter();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
_scrollbarTheme = ScrollbarTheme.of(context).merge(widget.style);
|
||||
_hoverController.duration = _scrollbarTheme.animationDuration ??
|
||||
FluentTheme.of(context).fasterAnimationDuration;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
ButtonStates get _currentState {
|
||||
if (_dragIsActive) {
|
||||
return ButtonStates.pressing;
|
||||
} else if (_hoverIsActive) {
|
||||
return ButtonStates.hovering;
|
||||
} else {
|
||||
return ButtonStates.none;
|
||||
}
|
||||
}
|
||||
|
||||
Color _trackColor(ButtonStates state) {
|
||||
// if (state == ButtonStates.hovering || state == ButtonStates.pressing) {
|
||||
// return _scrollbarTheme.backgroundColor ?? Colors.transparent;
|
||||
// }
|
||||
return Colors.transparent;
|
||||
}
|
||||
|
||||
Color _thumbColor(ButtonStates state) {
|
||||
Color? color;
|
||||
if (state == ButtonStates.pressing) {
|
||||
color = _scrollbarTheme.scrollbarPressingColor;
|
||||
}
|
||||
color ??= _scrollbarTheme.scrollbarColor ?? Colors.transparent;
|
||||
return color;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateScrollbarPainter() {
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final animation = CurvedAnimation(
|
||||
parent: _hoverController,
|
||||
curve: _scrollbarTheme.animationCurve ?? Curves.linear,
|
||||
);
|
||||
scrollbarPainter
|
||||
..color = _thumbColor(_currentState)
|
||||
..trackColor = _trackColor(_currentState)
|
||||
..trackBorderColor = Color.lerp(
|
||||
_scrollbarTheme.trackBorderColor,
|
||||
_scrollbarTheme.hoveringTrackBorderColor,
|
||||
animation.value,
|
||||
) ??
|
||||
Colors.transparent
|
||||
..textDirection = Directionality.of(context)
|
||||
..thickness = Tween<double>(
|
||||
begin: _scrollbarTheme.thickness ?? 2.0,
|
||||
end: _scrollbarTheme.hoveringThickness ?? 16.0,
|
||||
).evaluate(animation)
|
||||
..radius = _hoverController.status != AnimationStatus.dismissed
|
||||
? _scrollbarTheme.hoveringRadius
|
||||
: _scrollbarTheme.radius
|
||||
..crossAxisMargin = Tween<double>(
|
||||
begin: _scrollbarTheme.crossAxisMargin ?? 2.0,
|
||||
end: _scrollbarTheme.hoveringCrossAxisMargin ?? 0.0,
|
||||
).evaluate(animation)
|
||||
..mainAxisMargin = Tween<double>(
|
||||
begin: _scrollbarTheme.mainAxisMargin ?? 6.0,
|
||||
end: _scrollbarTheme.hoveringMainAxisMargin ?? 0.0,
|
||||
).evaluate(animation)
|
||||
..minLength = _scrollbarTheme.minThumbLength ?? 48.0
|
||||
..padding = MediaQuery.of(context).padding;
|
||||
}
|
||||
|
||||
@override
|
||||
void handleThumbPressStart(Offset localPosition) {
|
||||
super.handleThumbPressStart(localPosition);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_dragIsActive = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleThumbPressEnd(Offset localPosition, Velocity velocity) {
|
||||
super.handleThumbPressEnd(localPosition, velocity);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_dragIsActive = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleHover(PointerHoverEvent event) async {
|
||||
super.handleHover(event);
|
||||
// Check if the position of the pointer falls over the painted scrollbar
|
||||
if (isPointerOverScrollbar(event.position, event.kind)) {
|
||||
// Pointer is hovering over the scrollbar
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hoverIsActive = true;
|
||||
});
|
||||
}
|
||||
_hoverController.forward();
|
||||
} else if (_hoverIsActive) {
|
||||
await _hoverController.reverse();
|
||||
// Pointer was, but is no longer over painted scrollbar.
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hoverIsActive = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void handleHoverExit(PointerExitEvent event) {
|
||||
super.handleHoverExit(event);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hoverIsActive = false;
|
||||
});
|
||||
}
|
||||
_hoverController.reverse();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hoverController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited widget that defines the configuration for
|
||||
/// [Scrollbar]s in this widget's subtree.
|
||||
///
|
||||
/// Values specified here are used for [Scrollbar] properties that are not
|
||||
/// given an explicit non-null value.
|
||||
class ScrollbarTheme extends InheritedTheme {
|
||||
/// Creates a scrollbar theme that controls the configurations for
|
||||
/// [Scrollbar].
|
||||
const ScrollbarTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The properties for descendant [Scrollbar] widgets.
|
||||
final ScrollbarThemeData data;
|
||||
|
||||
/// Creates a button theme that controls how descendant [Scrollbar]s should
|
||||
/// look like, and merges in the current toggle button theme, if any.
|
||||
static Widget merge({
|
||||
Key? key,
|
||||
required ScrollbarThemeData data,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Builder(builder: (BuildContext context) {
|
||||
return ScrollbarTheme(
|
||||
key: key,
|
||||
data: _getInheritedThemeData(context).merge(data),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
static ScrollbarThemeData _getInheritedThemeData(BuildContext context) {
|
||||
final theme = context.dependOnInheritedWidgetOfExactType<ScrollbarTheme>();
|
||||
return theme?.data ?? FluentTheme.of(context).scrollbarTheme;
|
||||
}
|
||||
|
||||
/// Returns the [data] from the closest [ScrollbarTheme] ancestor. If there is
|
||||
/// no ancestor, it returns [ThemeData.scrollbarTheme]. Applications can assume
|
||||
/// that the returned value will not be null.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// ScrollbarThemeData theme = ScrollbarTheme.of(context);
|
||||
/// ```
|
||||
static ScrollbarThemeData of(BuildContext context) {
|
||||
return ScrollbarThemeData.standard(FluentTheme.of(context)).merge(
|
||||
_getInheritedThemeData(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return ScrollbarTheme(data: data, child: child);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ScrollbarTheme oldWidget) => data != oldWidget.data;
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ScrollbarThemeData with Diagnosticable {
|
||||
/// Thickness of the scrollbar in its cross-axis in logical
|
||||
/// pixels. If null, `2.0` is used
|
||||
final double? thickness;
|
||||
|
||||
/// Thickness of the scrollbar in its cross-axis in logical
|
||||
/// pixels when the user is hovering or pressing it. If null,
|
||||
/// `16.0` is used
|
||||
final double? hoveringThickness;
|
||||
|
||||
/// The background color of the scrollbar when the user is
|
||||
/// hovering or pressing it. If null, `Color(0xFFe9e9e9)` is
|
||||
/// used for light theme and `Color(0xFF1b1b1b)` is used for
|
||||
/// dark theme.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The color of the scrollbar thumb on its default state. If
|
||||
/// null, `Color(0xFF8c8c8c)` is used for light theme and
|
||||
/// `Color(0xFF767676)` is used for dark theme.
|
||||
final Color? scrollbarColor;
|
||||
|
||||
/// The color of the scrollbar thumb when the user is hovering
|
||||
/// or pressing it. If null, `const Color(0xFF5d5d5d)` is used
|
||||
/// for light theme and `Color(0xFFa4a4a4)` is used for dark
|
||||
/// theme by default.
|
||||
final Color? scrollbarPressingColor;
|
||||
|
||||
/// The default radius of the scrollbar. Defaults to
|
||||
/// `Radius.circular(100.0)`
|
||||
final Radius? radius;
|
||||
|
||||
/// The radius of the scrollbar when the user is hovering or
|
||||
/// pressing. Defaults to `Radius.circular(0.0)`
|
||||
final Radius? hoveringRadius;
|
||||
|
||||
/// Distance from the scrollbar's start and end to the edge of
|
||||
/// the viewport in logical pixels. It affects the amount of
|
||||
/// available paint area. Defaults to `2.0`
|
||||
final double? mainAxisMargin;
|
||||
|
||||
/// Distance from the scrollbar's start and end to the edge of
|
||||
/// the viewport in logical pixels. It affects the amount of
|
||||
/// available paint area. Defaults to `0.0`
|
||||
final double? hoveringMainAxisMargin;
|
||||
|
||||
/// Distance from the scrollbar's side to the nearest edge in
|
||||
/// logical pixels. Defaults to `0.0`
|
||||
final double? crossAxisMargin;
|
||||
|
||||
/// Distance from the scrollbar's side to the nearest edge in
|
||||
/// logical pixels when the user is hovering or pressing.
|
||||
/// Defaults to `2.0`
|
||||
final double? hoveringCrossAxisMargin;
|
||||
|
||||
/// Sets the preferred smallest size the scrollbar can shrink
|
||||
/// to when the total scrollable extent is large, the current
|
||||
/// visible viewport is small, and the viewport is not overscrolled.
|
||||
/// Defaults to `48.0`
|
||||
final double? minThumbLength;
|
||||
|
||||
/// [Color] of the track border. Defaults to [Colors.transparent]
|
||||
final Color? trackBorderColor;
|
||||
|
||||
/// [Color] of the track border when the user is hovering or pressing.
|
||||
/// Defaults to [Colors.transparent]
|
||||
final Color? hoveringTrackBorderColor;
|
||||
|
||||
/// The duration of the animation. Defaults to [ThemeData.fasterAnimationDuration].
|
||||
/// To disable the animation, set this to [Duration.zero]
|
||||
final Duration? animationDuration;
|
||||
|
||||
/// The curve used during the animation. Defaults to [ThemeData.animationCurve]
|
||||
final Curve? animationCurve;
|
||||
|
||||
const ScrollbarThemeData({
|
||||
this.thickness,
|
||||
this.hoveringThickness,
|
||||
this.backgroundColor,
|
||||
this.scrollbarColor,
|
||||
this.scrollbarPressingColor,
|
||||
this.radius,
|
||||
this.hoveringRadius,
|
||||
this.mainAxisMargin,
|
||||
this.hoveringMainAxisMargin,
|
||||
this.crossAxisMargin,
|
||||
this.hoveringCrossAxisMargin,
|
||||
this.minThumbLength,
|
||||
this.trackBorderColor,
|
||||
this.hoveringTrackBorderColor,
|
||||
this.animationDuration,
|
||||
this.animationCurve,
|
||||
});
|
||||
|
||||
factory ScrollbarThemeData.standard(ThemeData style) {
|
||||
final brightness = style.brightness;
|
||||
return ScrollbarThemeData(
|
||||
scrollbarColor: brightness.isLight
|
||||
? const Color(0xFF8c8c8c)
|
||||
: const Color(0xFF767676),
|
||||
scrollbarPressingColor: brightness.isLight
|
||||
? const Color(0xFF5d5d5d)
|
||||
: const Color(0xFFa4a4a4),
|
||||
thickness: 2.0,
|
||||
hoveringThickness: 6.0,
|
||||
backgroundColor: brightness.isLight
|
||||
? const Color(0xFFf9f9f9)
|
||||
: const Color(0xFF2c2f2a),
|
||||
radius: const Radius.circular(100.0),
|
||||
hoveringRadius: const Radius.circular(100.0),
|
||||
crossAxisMargin: 4.0,
|
||||
hoveringCrossAxisMargin: 4.0,
|
||||
mainAxisMargin: 2.0,
|
||||
hoveringMainAxisMargin: 2.0,
|
||||
minThumbLength: 48.0,
|
||||
trackBorderColor: Colors.transparent,
|
||||
hoveringTrackBorderColor: Colors.transparent,
|
||||
animationDuration: style.fasterAnimationDuration,
|
||||
animationCurve: Curves.linear,
|
||||
);
|
||||
}
|
||||
|
||||
static ScrollbarThemeData lerp(
|
||||
ScrollbarThemeData? a, ScrollbarThemeData? b, double t) {
|
||||
return ScrollbarThemeData(
|
||||
backgroundColor: Color.lerp(a?.backgroundColor, b?.backgroundColor, t),
|
||||
scrollbarColor: Color.lerp(a?.scrollbarColor, b?.scrollbarColor, t),
|
||||
scrollbarPressingColor:
|
||||
Color.lerp(a?.scrollbarPressingColor, b?.scrollbarPressingColor, t),
|
||||
thickness: lerpDouble(a?.thickness, b?.thickness, t),
|
||||
hoveringThickness:
|
||||
lerpDouble(a?.hoveringThickness, b?.hoveringThickness, t),
|
||||
radius: Radius.lerp(a?.radius, b?.radius, t),
|
||||
hoveringRadius: Radius.lerp(a?.hoveringRadius, b?.hoveringRadius, t),
|
||||
crossAxisMargin: lerpDouble(a?.crossAxisMargin, b?.crossAxisMargin, t),
|
||||
hoveringCrossAxisMargin:
|
||||
lerpDouble(a?.hoveringCrossAxisMargin, b?.hoveringCrossAxisMargin, t),
|
||||
mainAxisMargin: lerpDouble(a?.mainAxisMargin, b?.mainAxisMargin, t),
|
||||
hoveringMainAxisMargin:
|
||||
lerpDouble(a?.hoveringMainAxisMargin, b?.hoveringMainAxisMargin, t),
|
||||
minThumbLength: lerpDouble(a?.minThumbLength, b?.minThumbLength, t),
|
||||
trackBorderColor: Color.lerp(a?.trackBorderColor, b?.trackBorderColor, t),
|
||||
hoveringTrackBorderColor: Color.lerp(
|
||||
a?.hoveringTrackBorderColor, b?.hoveringTrackBorderColor, t),
|
||||
animationCurve: t < 0.5 ? a?.animationCurve : b?.animationCurve,
|
||||
animationDuration: lerpDuration(a?.animationDuration ?? Duration.zero,
|
||||
b?.animationDuration ?? Duration.zero, t),
|
||||
);
|
||||
}
|
||||
|
||||
ScrollbarThemeData merge(ScrollbarThemeData? style) {
|
||||
if (style == null) return this;
|
||||
return ScrollbarThemeData(
|
||||
backgroundColor: style.backgroundColor ?? backgroundColor,
|
||||
scrollbarColor: style.scrollbarColor ?? scrollbarColor,
|
||||
scrollbarPressingColor:
|
||||
style.scrollbarPressingColor ?? scrollbarPressingColor,
|
||||
hoveringThickness: style.hoveringThickness ?? hoveringThickness,
|
||||
thickness: style.thickness ?? thickness,
|
||||
radius: style.radius ?? radius,
|
||||
hoveringRadius: style.hoveringRadius ?? hoveringRadius,
|
||||
crossAxisMargin: style.crossAxisMargin ?? crossAxisMargin,
|
||||
hoveringCrossAxisMargin:
|
||||
style.hoveringCrossAxisMargin ?? hoveringCrossAxisMargin,
|
||||
mainAxisMargin: style.mainAxisMargin ?? mainAxisMargin,
|
||||
hoveringMainAxisMargin:
|
||||
style.hoveringMainAxisMargin ?? hoveringMainAxisMargin,
|
||||
minThumbLength: style.minThumbLength ?? minThumbLength,
|
||||
hoveringTrackBorderColor:
|
||||
style.hoveringTrackBorderColor ?? hoveringTrackBorderColor,
|
||||
trackBorderColor: style.trackBorderColor ?? trackBorderColor,
|
||||
animationCurve: style.animationCurve ?? animationCurve,
|
||||
animationDuration: style.animationDuration ?? animationDuration,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ColorProperty('scrollbarColor', scrollbarColor));
|
||||
properties.add(
|
||||
ColorProperty('scrollbarPressingColor', scrollbarPressingColor),
|
||||
);
|
||||
properties.add(ColorProperty('backgroundColor', backgroundColor));
|
||||
properties.add(DoubleProperty('thickness', thickness, defaultValue: 2.0));
|
||||
properties.add(DoubleProperty(
|
||||
'hoveringThickness',
|
||||
hoveringThickness,
|
||||
defaultValue: 16.0,
|
||||
));
|
||||
properties.add(DiagnosticsProperty<Radius>(
|
||||
'radius',
|
||||
radius,
|
||||
defaultValue: const Radius.circular(100),
|
||||
));
|
||||
properties.add(DiagnosticsProperty<Radius>(
|
||||
'hoveringRadius',
|
||||
hoveringRadius,
|
||||
defaultValue: Radius.zero,
|
||||
));
|
||||
properties.add(
|
||||
DoubleProperty('mainAxisMargin', mainAxisMargin, defaultValue: 2.0),
|
||||
);
|
||||
properties.add(DoubleProperty(
|
||||
'hoveringMainAxisMargin',
|
||||
hoveringMainAxisMargin,
|
||||
defaultValue: 0.0,
|
||||
));
|
||||
properties.add(
|
||||
DoubleProperty('crossAxisMargin', mainAxisMargin, defaultValue: 2.0),
|
||||
);
|
||||
properties.add(DoubleProperty(
|
||||
'hoveringCrossAxisMargin',
|
||||
hoveringMainAxisMargin,
|
||||
defaultValue: 0.0,
|
||||
));
|
||||
properties.add(
|
||||
DoubleProperty('minThumbLength', minThumbLength, defaultValue: 48.0),
|
||||
);
|
||||
properties.add(ColorProperty('trackBorderColor', trackBorderColor));
|
||||
properties.add(
|
||||
ColorProperty('hoveringTrackBorderColor', hoveringTrackBorderColor),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<Duration>(
|
||||
'animationDuration',
|
||||
animationDuration,
|
||||
defaultValue: const Duration(milliseconds: 90),
|
||||
));
|
||||
properties.add(DiagnosticsProperty<Curve>(
|
||||
'animationCurve',
|
||||
animationCurve,
|
||||
defaultValue: Curves.linear,
|
||||
));
|
||||
}
|
||||
}
|
||||
15497
dependencies/fluent_ui-3.12.0/lib/src/icons.dart
vendored
Normal file
15497
dependencies/fluent_ui-3.12.0/lib/src/icons.dart
vendored
Normal file
File diff suppressed because it is too large
Load Diff
801
dependencies/fluent_ui-3.12.0/lib/src/layout/dynamic_overflow.dart
vendored
Normal file
801
dependencies/fluent_ui-3.12.0/lib/src/layout/dynamic_overflow.dart
vendored
Normal file
@@ -0,0 +1,801 @@
|
||||
// Copyright 2022 Bruno D'Luka.
|
||||
// Portions copyright 2014 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
/// Signature of a function that is called to notify that the children
|
||||
/// that have been hidden due to overflow has changed.
|
||||
typedef DynamicOverflowChangedCallback = void Function(
|
||||
List<int> hiddenChildren);
|
||||
|
||||
/// Lays out children widgets in a single run, and if there is not
|
||||
/// room to display them all, it will hide widgets that don't fit,
|
||||
/// and display the "overflow widget" at the end. Optionally, the
|
||||
/// "overflow widget" can be displayed all the time. Displaying the
|
||||
/// overflow widget will take precedence over any children widgets.
|
||||
///
|
||||
/// Adapted from [Wrap].
|
||||
class DynamicOverflow extends MultiChildRenderObjectWidget {
|
||||
/// {@macro flutter.widgets.wrap.direction}
|
||||
final Axis direction;
|
||||
|
||||
/// {@macro flutter.widgets.wrap.alignment}
|
||||
final MainAxisAlignment alignment;
|
||||
|
||||
/// {@macro flutter.widgets.wrap.crossAxisAlignment}
|
||||
final CrossAxisAlignment crossAxisAlignment;
|
||||
|
||||
/// {@macro flutter.widgets.wrap.textDirection}
|
||||
final TextDirection? textDirection;
|
||||
|
||||
/// {@macro flutter.widgets.wrap.verticalDirection}
|
||||
final VerticalDirection verticalDirection;
|
||||
|
||||
/// {@macro flutter.material.Material.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.none].
|
||||
final Clip clipBehavior;
|
||||
|
||||
/// The alignment of the overflow widget between the end of the
|
||||
/// visible regular children and the end of the container.
|
||||
final MainAxisAlignment overflowWidgetAlignment;
|
||||
|
||||
/// Whether or not to always display the overflowWidget, even if
|
||||
/// all other widgets are able to be displayed.
|
||||
final bool alwaysDisplayOverflowWidget;
|
||||
|
||||
/// Function that is called when the list of children that are
|
||||
/// hidden because of the dynamic overflow has changed.
|
||||
final DynamicOverflowChangedCallback? overflowChangedCallback;
|
||||
|
||||
DynamicOverflow({
|
||||
Key? key,
|
||||
this.direction = Axis.horizontal,
|
||||
this.alignment = MainAxisAlignment.start,
|
||||
this.crossAxisAlignment = CrossAxisAlignment.center,
|
||||
this.textDirection,
|
||||
this.verticalDirection = VerticalDirection.down,
|
||||
this.clipBehavior = Clip.none,
|
||||
this.alwaysDisplayOverflowWidget = false,
|
||||
this.overflowWidgetAlignment = MainAxisAlignment.end,
|
||||
this.overflowChangedCallback,
|
||||
required List<Widget> children,
|
||||
required Widget overflowWidget,
|
||||
}) : super(key: key, children: [...children, overflowWidget]);
|
||||
|
||||
@override
|
||||
RenderDynamicOverflow createRenderObject(BuildContext context) {
|
||||
return RenderDynamicOverflow(
|
||||
direction: direction,
|
||||
alignment: alignment,
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
textDirection: textDirection ?? Directionality.maybeOf(context),
|
||||
verticalDirection: verticalDirection,
|
||||
clipBehavior: clipBehavior,
|
||||
overflowWidgetAlignment: overflowWidgetAlignment,
|
||||
alwaysDisplayOverflowWidget: alwaysDisplayOverflowWidget,
|
||||
overflowChangedCallback: overflowChangedCallback,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(
|
||||
BuildContext context, RenderDynamicOverflow renderObject) {
|
||||
renderObject
|
||||
..direction = direction
|
||||
..alignment = alignment
|
||||
..crossAxisAlignment = crossAxisAlignment
|
||||
..textDirection = textDirection ?? Directionality.maybeOf(context)
|
||||
..verticalDirection = verticalDirection
|
||||
..clipBehavior = clipBehavior
|
||||
..overflowWidgetAlignment = overflowWidgetAlignment
|
||||
..alwaysDisplayOverflowWidget = alwaysDisplayOverflowWidget
|
||||
..overflowChangedCallback = overflowChangedCallback;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(EnumProperty<Axis>('direction', direction));
|
||||
properties.add(EnumProperty<MainAxisAlignment>('alignment', alignment));
|
||||
properties.add(EnumProperty<CrossAxisAlignment>(
|
||||
'crossAxisAlignment', crossAxisAlignment));
|
||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
|
||||
defaultValue: null));
|
||||
properties.add(EnumProperty<VerticalDirection>(
|
||||
'verticalDirection', verticalDirection));
|
||||
properties.add(EnumProperty<Clip>('clipBehavior', clipBehavior));
|
||||
properties.add(EnumProperty<MainAxisAlignment>(
|
||||
'overflowWidgetAlignment', overflowWidgetAlignment));
|
||||
properties.add(FlagProperty(
|
||||
'alwaysDisplayOverflowWidget',
|
||||
value: alwaysDisplayOverflowWidget,
|
||||
ifTrue: 'always display overflow widget',
|
||||
ifFalse: 'do not always display overflow widget',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Parent data for use with [RenderDynamicOverflow].
|
||||
class DynamicOverflowParentData extends ContainerBoxParentData<RenderBox> {
|
||||
bool _isHidden = false;
|
||||
}
|
||||
|
||||
/// Rendering logic for [DynamicOverflow] widget.
|
||||
/// Adapted from [RenderWrap].
|
||||
class RenderDynamicOverflow extends RenderBox
|
||||
with
|
||||
ContainerRenderObjectMixin<RenderBox, DynamicOverflowParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, DynamicOverflowParentData> {
|
||||
RenderDynamicOverflow({
|
||||
required Axis direction,
|
||||
required MainAxisAlignment alignment,
|
||||
required CrossAxisAlignment crossAxisAlignment,
|
||||
required TextDirection? textDirection,
|
||||
required VerticalDirection verticalDirection,
|
||||
required Clip clipBehavior,
|
||||
required MainAxisAlignment overflowWidgetAlignment,
|
||||
required bool alwaysDisplayOverflowWidget,
|
||||
required this.overflowChangedCallback,
|
||||
}) : _direction = direction,
|
||||
_alignment = alignment,
|
||||
_crossAxisAlignment = crossAxisAlignment,
|
||||
_textDirection = textDirection,
|
||||
_verticalDirection = verticalDirection,
|
||||
_clipBehavior = clipBehavior,
|
||||
_overflowWidgetAlignment = overflowWidgetAlignment,
|
||||
_alwaysDisplayOverflowWidget = alwaysDisplayOverflowWidget;
|
||||
|
||||
Axis _direction;
|
||||
Axis get direction => _direction;
|
||||
set direction(Axis value) {
|
||||
if (_direction != value) {
|
||||
_direction = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
MainAxisAlignment _alignment;
|
||||
MainAxisAlignment get alignment => _alignment;
|
||||
set alignment(MainAxisAlignment value) {
|
||||
if (_alignment != value) {
|
||||
_alignment = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
CrossAxisAlignment _crossAxisAlignment;
|
||||
CrossAxisAlignment get crossAxisAlignment => _crossAxisAlignment;
|
||||
set crossAxisAlignment(CrossAxisAlignment value) {
|
||||
if (_crossAxisAlignment != value) {
|
||||
_crossAxisAlignment = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
TextDirection? _textDirection;
|
||||
TextDirection? get textDirection => _textDirection;
|
||||
set textDirection(TextDirection? value) {
|
||||
if (_textDirection != value) {
|
||||
_textDirection = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
VerticalDirection? _verticalDirection;
|
||||
VerticalDirection? get verticalDirection => _verticalDirection;
|
||||
set verticalDirection(VerticalDirection? value) {
|
||||
if (_verticalDirection != value) {
|
||||
_verticalDirection = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
Clip _clipBehavior;
|
||||
Clip get clipBehavior => _clipBehavior;
|
||||
set clipBehavior(Clip value) {
|
||||
if (_clipBehavior != value) {
|
||||
_clipBehavior = value;
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
MainAxisAlignment _overflowWidgetAlignment;
|
||||
MainAxisAlignment get overflowWidgetAlignment => _overflowWidgetAlignment;
|
||||
set overflowWidgetAlignment(MainAxisAlignment value) {
|
||||
if (_overflowWidgetAlignment != value) {
|
||||
_overflowWidgetAlignment = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
bool _alwaysDisplayOverflowWidget;
|
||||
bool get alwaysDisplayOverflowWidget => _alwaysDisplayOverflowWidget;
|
||||
set alwaysDisplayOverflowWidget(bool value) {
|
||||
if (_alwaysDisplayOverflowWidget != value) {
|
||||
_alwaysDisplayOverflowWidget = value;
|
||||
markNeedsLayout();
|
||||
}
|
||||
}
|
||||
|
||||
DynamicOverflowChangedCallback? overflowChangedCallback;
|
||||
|
||||
bool get _debugHasNecessaryDirections {
|
||||
if (firstChild != null && lastChild != firstChild) {
|
||||
// i.e. there's more than one child
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
assert(textDirection != null,
|
||||
'Horizontal $runtimeType with multiple children has a null textDirection, so the layout order is undefined.');
|
||||
break;
|
||||
case Axis.vertical:
|
||||
assert(verticalDirection != null,
|
||||
'Vertical $runtimeType with multiple children has a null verticalDirection, so the layout order is undefined.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (alignment == MainAxisAlignment.start ||
|
||||
alignment == MainAxisAlignment.end) {
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
assert(textDirection != null,
|
||||
'Horizontal $runtimeType with alignment $alignment has a null textDirection, so the alignment cannot be resolved.');
|
||||
break;
|
||||
case Axis.vertical:
|
||||
assert(verticalDirection != null,
|
||||
'Vertical $runtimeType with alignment $alignment has a null verticalDirection, so the alignment cannot be resolved.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (crossAxisAlignment == CrossAxisAlignment.start ||
|
||||
crossAxisAlignment == CrossAxisAlignment.end) {
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
assert(verticalDirection != null,
|
||||
'Horizontal $runtimeType with crossAxisAlignment $crossAxisAlignment has a null verticalDirection, so the alignment cannot be resolved.');
|
||||
break;
|
||||
case Axis.vertical:
|
||||
assert(textDirection != null,
|
||||
'Vertical $runtimeType with crossAxisAlignment $crossAxisAlignment has a null textDirection, so the alignment cannot be resolved.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! DynamicOverflowParentData) {
|
||||
child.parentData = DynamicOverflowParentData();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
// The min intrinsic width is the width of the last child, which must
|
||||
// be the renderbox of the "overflow widget"
|
||||
double width = 0.0;
|
||||
RenderBox? child = lastChild;
|
||||
if (child != null) {
|
||||
width = child.getMinIntrinsicWidth(double.infinity);
|
||||
}
|
||||
return width;
|
||||
case Axis.vertical:
|
||||
return computeDryLayout(BoxConstraints(maxHeight: height)).width;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
// The max intrinsic width is the width of all children, except
|
||||
// potentially the last child if we do not always display the
|
||||
// "overflow widget"
|
||||
double width = 0.0;
|
||||
double lastChildWidth = 0.0;
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
lastChildWidth = child.getMaxIntrinsicWidth(double.infinity);
|
||||
width += lastChildWidth;
|
||||
child = childAfter(child);
|
||||
}
|
||||
if (!alwaysDisplayOverflowWidget && lastChild != null) {
|
||||
// we don't have to display the overflow item if
|
||||
// all other items are visible
|
||||
width -= lastChildWidth;
|
||||
}
|
||||
return width;
|
||||
case Axis.vertical:
|
||||
return computeDryLayout(BoxConstraints(maxHeight: height)).width;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicHeight(double width) {
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
return computeDryLayout(BoxConstraints(maxWidth: width)).height;
|
||||
case Axis.vertical:
|
||||
// The min intrinsic height is the height of the last child, which must
|
||||
// be the renderbox of the "overflow widget"
|
||||
double height = 0.0;
|
||||
RenderBox? child = lastChild;
|
||||
if (child != null) {
|
||||
height = child.getMinIntrinsicHeight(double.infinity);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicHeight(double width) {
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
return computeDryLayout(BoxConstraints(maxWidth: width)).height;
|
||||
case Axis.vertical:
|
||||
// The max intrinsic height is the height of all children, except
|
||||
// potentially the last child if we do not always display the
|
||||
// "overflow widget"
|
||||
double height = 0.0;
|
||||
double lastChildHeight = 0.0;
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
lastChildHeight = child.getMaxIntrinsicHeight(double.infinity);
|
||||
height += lastChildHeight;
|
||||
child = childAfter(child);
|
||||
}
|
||||
if (!alwaysDisplayOverflowWidget && lastChild != null) {
|
||||
// we don't have to display the overflow item if
|
||||
// all other items are visible
|
||||
height -= lastChildHeight;
|
||||
}
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double? computeDistanceToActualBaseline(TextBaseline baseline) {
|
||||
return defaultComputeDistanceToHighestActualBaseline(baseline);
|
||||
}
|
||||
|
||||
double _getMainAxisExtent(Size childSize) {
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
return childSize.width;
|
||||
case Axis.vertical:
|
||||
return childSize.height;
|
||||
}
|
||||
}
|
||||
|
||||
double _getCrossAxisExtent(Size childSize) {
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
return childSize.height;
|
||||
case Axis.vertical:
|
||||
return childSize.width;
|
||||
}
|
||||
}
|
||||
|
||||
Offset _getOffset(double mainAxisOffset, double crossAxisOffset) {
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
return Offset(mainAxisOffset, crossAxisOffset);
|
||||
case Axis.vertical:
|
||||
return Offset(crossAxisOffset, mainAxisOffset);
|
||||
}
|
||||
}
|
||||
|
||||
double _getChildCrossAxisOffset(
|
||||
bool flipCrossAxis, double crossAxisExtent, double childCrossAxisExtent) {
|
||||
final double freeSpace = crossAxisExtent - childCrossAxisExtent;
|
||||
switch (crossAxisAlignment) {
|
||||
case CrossAxisAlignment.start:
|
||||
return flipCrossAxis ? freeSpace : 0.0;
|
||||
case CrossAxisAlignment.end:
|
||||
return flipCrossAxis ? 0.0 : freeSpace;
|
||||
case CrossAxisAlignment.center:
|
||||
return freeSpace / 2.0;
|
||||
case CrossAxisAlignment.stretch:
|
||||
throw UnsupportedError(
|
||||
"CrossAxisAlignment.stretch is not supported by DynamicOverflow");
|
||||
case CrossAxisAlignment.baseline:
|
||||
throw UnsupportedError(
|
||||
"CrossAxisAlignment.baseline is not supported by DynamicOverflow");
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasVisualOverflow = false;
|
||||
// indexes of the children that we hid, excluding the overflow item
|
||||
List<int> _hiddenChildren = [];
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
return _computeDryLayout(constraints);
|
||||
}
|
||||
|
||||
Size _computeDryLayout(BoxConstraints constraints,
|
||||
[ChildLayouter layoutChild = ChildLayoutHelper.dryLayoutChild]) {
|
||||
final BoxConstraints childConstraints;
|
||||
double mainAxisLimit = 0.0;
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
childConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
|
||||
mainAxisLimit = constraints.maxWidth;
|
||||
break;
|
||||
case Axis.vertical:
|
||||
childConstraints = BoxConstraints(maxHeight: constraints.maxHeight);
|
||||
mainAxisLimit = constraints.maxHeight;
|
||||
break;
|
||||
}
|
||||
|
||||
// The last item is always the overflow item
|
||||
double overflowItemMainAxisExtent = 0.0;
|
||||
double overflowItemCrossAxisExtent = 0.0;
|
||||
if (lastChild != null) {
|
||||
final Size lastChildSize = layoutChild(lastChild!, childConstraints);
|
||||
overflowItemMainAxisExtent = _getMainAxisExtent(lastChildSize);
|
||||
overflowItemCrossAxisExtent = _getCrossAxisExtent(lastChildSize);
|
||||
}
|
||||
|
||||
double mainAxisExtent = 0.0;
|
||||
double crossAxisExtent = 0.0;
|
||||
bool overflowed = false;
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null && child != lastChild) {
|
||||
final Size childSize = layoutChild(child, childConstraints);
|
||||
final double childMainAxisExtent = _getMainAxisExtent(childSize);
|
||||
final double childCrossAxisExtent = _getCrossAxisExtent(childSize);
|
||||
|
||||
// To keep things simpler, always include the extent of the overflow item
|
||||
// in the run limit calculation, even if it would not need to be displayed.
|
||||
// This results in the overflow item being shown a little bit sooner than
|
||||
// is needed in some cases, but that is OK.
|
||||
if (mainAxisExtent + childMainAxisExtent + overflowItemMainAxisExtent >
|
||||
mainAxisLimit) {
|
||||
// This child is not going to be rendered, but the overflow item is.
|
||||
mainAxisExtent += overflowItemMainAxisExtent;
|
||||
crossAxisExtent =
|
||||
math.max(crossAxisExtent, overflowItemCrossAxisExtent);
|
||||
overflowed = true;
|
||||
break;
|
||||
}
|
||||
mainAxisExtent += childMainAxisExtent;
|
||||
crossAxisExtent = math.max(crossAxisExtent, childCrossAxisExtent);
|
||||
child = childAfter(child);
|
||||
}
|
||||
if (!overflowed && _alwaysDisplayOverflowWidget) {
|
||||
mainAxisExtent += overflowItemMainAxisExtent;
|
||||
crossAxisExtent = math.max(crossAxisExtent, overflowItemCrossAxisExtent);
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
return constraints.constrain(Size(mainAxisExtent, crossAxisExtent));
|
||||
case Axis.vertical:
|
||||
return constraints.constrain(Size(crossAxisExtent, mainAxisExtent));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
final BoxConstraints constraints = this.constraints;
|
||||
assert(_debugHasNecessaryDirections);
|
||||
RenderBox? child = firstChild;
|
||||
if (child == null) {
|
||||
size = constraints.smallest;
|
||||
return;
|
||||
}
|
||||
final BoxConstraints childConstraints;
|
||||
double mainAxisLimit = 0.0;
|
||||
bool flipMainAxis = false;
|
||||
bool flipCrossAxis = false;
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
childConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
|
||||
mainAxisLimit = constraints.maxWidth;
|
||||
if (textDirection == TextDirection.rtl) flipMainAxis = true;
|
||||
if (verticalDirection == VerticalDirection.up) flipCrossAxis = true;
|
||||
break;
|
||||
case Axis.vertical:
|
||||
childConstraints = BoxConstraints(maxHeight: constraints.maxHeight);
|
||||
mainAxisLimit = constraints.maxHeight;
|
||||
if (verticalDirection == VerticalDirection.up) flipMainAxis = true;
|
||||
if (textDirection == TextDirection.rtl) flipCrossAxis = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// The last item is always the overflow item
|
||||
double overflowItemMainAxisExtent = 0.0;
|
||||
double overflowItemCrossAxisExtent = 0.0;
|
||||
if (lastChild != null) {
|
||||
lastChild!.layout(childConstraints, parentUsesSize: true);
|
||||
overflowItemMainAxisExtent = _getMainAxisExtent(lastChild!.size);
|
||||
overflowItemCrossAxisExtent = _getCrossAxisExtent(lastChild!.size);
|
||||
}
|
||||
|
||||
double mainAxisExtent = 0.0;
|
||||
double crossAxisExtent = 0.0;
|
||||
int childIndex = 0;
|
||||
int visibleChildCount = 0;
|
||||
bool overflowed = false;
|
||||
bool overflowItemVisible = false;
|
||||
// Indexes of hidden children. Never includes the index for the
|
||||
// overflow item.
|
||||
List<int> hiddenChildren = [];
|
||||
// First determine how many items will fit into the one run and
|
||||
// if there is any overflow.
|
||||
while (child != null && child != lastChild) {
|
||||
child.layout(childConstraints, parentUsesSize: true);
|
||||
final double childMainAxisExtent = _getMainAxisExtent(child.size);
|
||||
final double childCrossAxisExtent = _getCrossAxisExtent(child.size);
|
||||
|
||||
// To keep things simpler, always include the extent of the overflow item
|
||||
// in the run limit calculation, even if it would not need to be displayed.
|
||||
// This results in the overflow item being shown a little bit sooner than
|
||||
// is needed in some cases, but that is OK.
|
||||
if (overflowed) {
|
||||
hiddenChildren.add(childIndex);
|
||||
} else if (mainAxisExtent +
|
||||
childMainAxisExtent +
|
||||
overflowItemMainAxisExtent >
|
||||
mainAxisLimit) {
|
||||
// This child is not going to be rendered, but the overflow item is.
|
||||
mainAxisExtent += overflowItemMainAxisExtent;
|
||||
crossAxisExtent =
|
||||
math.max(crossAxisExtent, overflowItemCrossAxisExtent);
|
||||
overflowItemVisible = true;
|
||||
overflowed = true;
|
||||
hiddenChildren.add(childIndex);
|
||||
// Don't break since we are obligated to call layout for all
|
||||
// children via the contract of performLayout.
|
||||
} else {
|
||||
mainAxisExtent += childMainAxisExtent;
|
||||
crossAxisExtent = math.max(crossAxisExtent, childCrossAxisExtent);
|
||||
visibleChildCount += 1;
|
||||
}
|
||||
|
||||
childIndex += 1;
|
||||
final DynamicOverflowParentData childParentData =
|
||||
child.parentData! as DynamicOverflowParentData;
|
||||
childParentData._isHidden = overflowed;
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
if (!overflowed && _alwaysDisplayOverflowWidget) {
|
||||
mainAxisExtent += overflowItemMainAxisExtent;
|
||||
crossAxisExtent = math.max(crossAxisExtent, overflowItemCrossAxisExtent);
|
||||
overflowItemVisible = true;
|
||||
}
|
||||
if (lastChild != null) {
|
||||
final DynamicOverflowParentData overflowItemParentData =
|
||||
lastChild!.parentData! as DynamicOverflowParentData;
|
||||
overflowItemParentData._isHidden = !overflowItemVisible;
|
||||
}
|
||||
if (overflowItemVisible) {
|
||||
// The overflow item should be counted as visible so that spacing
|
||||
// and alignment consider the overflow item as well.
|
||||
visibleChildCount += 1;
|
||||
}
|
||||
|
||||
double containerMainAxisExtent = 0.0;
|
||||
double containerCrossAxisExtent = 0.0;
|
||||
|
||||
switch (direction) {
|
||||
case Axis.horizontal:
|
||||
size = constraints.constrain(Size(mainAxisExtent, crossAxisExtent));
|
||||
containerMainAxisExtent = size.width;
|
||||
containerCrossAxisExtent = size.height;
|
||||
break;
|
||||
case Axis.vertical:
|
||||
size = constraints.constrain(Size(crossAxisExtent, mainAxisExtent));
|
||||
containerMainAxisExtent = size.height;
|
||||
containerCrossAxisExtent = size.width;
|
||||
break;
|
||||
}
|
||||
|
||||
_hasVisualOverflow = containerMainAxisExtent < mainAxisExtent ||
|
||||
containerCrossAxisExtent < crossAxisExtent;
|
||||
|
||||
// Notify callback if the children we've hidden has changed
|
||||
if (!listEquals(_hiddenChildren, hiddenChildren)) {
|
||||
_hiddenChildren = hiddenChildren;
|
||||
if (overflowChangedCallback != null) {
|
||||
// This will likely trigger setState in a parent widget,
|
||||
// so schedule to happen at the end of the frame...
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
overflowChangedCallback!(hiddenChildren);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate alignment parameters based on the axis extents.
|
||||
|
||||
double crossAxisOffset =
|
||||
flipCrossAxis ? (containerCrossAxisExtent - crossAxisExtent) : 0;
|
||||
|
||||
final double mainAxisFreeSpace =
|
||||
math.max(0.0, containerMainAxisExtent - mainAxisExtent);
|
||||
double childLeadingSpace = 0.0;
|
||||
double childBetweenSpace = 0.0;
|
||||
|
||||
switch (alignment) {
|
||||
case MainAxisAlignment.start:
|
||||
break;
|
||||
case MainAxisAlignment.end:
|
||||
childLeadingSpace = mainAxisFreeSpace;
|
||||
break;
|
||||
case MainAxisAlignment.center:
|
||||
childLeadingSpace = mainAxisFreeSpace / 2.0;
|
||||
break;
|
||||
case MainAxisAlignment.spaceBetween:
|
||||
childBetweenSpace = visibleChildCount > 1
|
||||
? mainAxisFreeSpace / (visibleChildCount - 1)
|
||||
: 0.0;
|
||||
break;
|
||||
case MainAxisAlignment.spaceAround:
|
||||
childBetweenSpace =
|
||||
visibleChildCount > 0 ? mainAxisFreeSpace / visibleChildCount : 0.0;
|
||||
childLeadingSpace = childBetweenSpace / 2.0;
|
||||
break;
|
||||
case MainAxisAlignment.spaceEvenly:
|
||||
childBetweenSpace = mainAxisFreeSpace / (visibleChildCount + 1);
|
||||
childLeadingSpace = childBetweenSpace;
|
||||
break;
|
||||
}
|
||||
|
||||
double childMainPosition = flipMainAxis
|
||||
? containerMainAxisExtent - childLeadingSpace
|
||||
: childLeadingSpace;
|
||||
|
||||
// Enumerate through all items again and calculate their position,
|
||||
// now that we know the actual main and cross axis extents and can
|
||||
// calculate proper positions given the desired alignment parameters.
|
||||
child = firstChild;
|
||||
while (child != null) {
|
||||
final DynamicOverflowParentData childParentData =
|
||||
child.parentData! as DynamicOverflowParentData;
|
||||
|
||||
if (childParentData._isHidden) {
|
||||
// Hide the widget by setting its offset to outside of the
|
||||
// container's extent, so it will be guaranteed to be cropped...
|
||||
childParentData.offset = _getOffset(
|
||||
containerMainAxisExtent + 100, containerCrossAxisExtent + 100);
|
||||
} else {
|
||||
final double childMainAxisExtent = _getMainAxisExtent(child.size);
|
||||
final double childCrossAxisExtent = _getCrossAxisExtent(child.size);
|
||||
final double childCrossAxisOffset = _getChildCrossAxisOffset(
|
||||
flipCrossAxis, crossAxisExtent, childCrossAxisExtent);
|
||||
if (flipMainAxis) {
|
||||
childMainPosition -= childMainAxisExtent;
|
||||
}
|
||||
if (child == lastChild) {
|
||||
// There is a special layout for the overflow item. We may want
|
||||
// it to be aligned at the "opposite side" as this looks visually
|
||||
// more consistent
|
||||
late double overflowChildMainPosition;
|
||||
double endAlignedMainAxisPosition =
|
||||
flipMainAxis ? 0 : containerMainAxisExtent - childMainAxisExtent;
|
||||
switch (_overflowWidgetAlignment) {
|
||||
case MainAxisAlignment.start:
|
||||
// we're already in the right spot
|
||||
overflowChildMainPosition = childMainPosition;
|
||||
break;
|
||||
case MainAxisAlignment.center:
|
||||
overflowChildMainPosition =
|
||||
(childMainPosition + endAlignedMainAxisPosition) / 2;
|
||||
break;
|
||||
case MainAxisAlignment.end:
|
||||
default:
|
||||
overflowChildMainPosition = endAlignedMainAxisPosition;
|
||||
break;
|
||||
}
|
||||
childParentData.offset = _getOffset(overflowChildMainPosition,
|
||||
crossAxisOffset + childCrossAxisOffset);
|
||||
} else {
|
||||
childParentData.offset = _getOffset(
|
||||
childMainPosition, crossAxisOffset + childCrossAxisOffset);
|
||||
}
|
||||
if (flipMainAxis) {
|
||||
childMainPosition -= childBetweenSpace;
|
||||
} else {
|
||||
childMainPosition += childMainAxisExtent + childBetweenSpace;
|
||||
}
|
||||
}
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
|
||||
RenderBox? child = lastChild;
|
||||
while (child != null) {
|
||||
final DynamicOverflowParentData childParentData =
|
||||
child.parentData! as DynamicOverflowParentData;
|
||||
// Hidden children cannot generate a hit
|
||||
if (!childParentData._isHidden) {
|
||||
// The x, y parameters have the top left of the node's box as the origin.
|
||||
final bool isHit = result.addWithPaintOffset(
|
||||
offset: childParentData.offset,
|
||||
position: position,
|
||||
hitTest: (BoxHitTestResult result, Offset transformed) {
|
||||
assert(transformed == position - childParentData.offset);
|
||||
return child!.hitTest(result, position: transformed);
|
||||
},
|
||||
);
|
||||
if (isHit) return true;
|
||||
}
|
||||
child = childParentData.previousSibling;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (_hasVisualOverflow && clipBehavior != Clip.none) {
|
||||
_clipRectLayer.layer = context.pushClipRect(
|
||||
needsCompositing,
|
||||
offset,
|
||||
Offset.zero & size,
|
||||
_paintSkipHiddenChildren,
|
||||
clipBehavior: clipBehavior,
|
||||
oldLayer: _clipRectLayer.layer,
|
||||
);
|
||||
} else {
|
||||
_clipRectLayer.layer = null;
|
||||
_paintSkipHiddenChildren(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
void _paintSkipHiddenChildren(PaintingContext context, Offset offset) {
|
||||
RenderBox? child = firstChild;
|
||||
while (child != null) {
|
||||
final DynamicOverflowParentData childParentData =
|
||||
child.parentData! as DynamicOverflowParentData;
|
||||
if (!childParentData._isHidden) {
|
||||
context.paintChild(child, childParentData.offset + offset);
|
||||
}
|
||||
child = childParentData.nextSibling;
|
||||
}
|
||||
}
|
||||
|
||||
final LayerHandle<ClipRectLayer> _clipRectLayer =
|
||||
LayerHandle<ClipRectLayer>();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_clipRectLayer.layer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(EnumProperty<Axis>('direction', direction));
|
||||
properties.add(EnumProperty<MainAxisAlignment>('alignment', alignment));
|
||||
properties.add(EnumProperty<CrossAxisAlignment>(
|
||||
'crossAxisAlignment', crossAxisAlignment));
|
||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
|
||||
defaultValue: null));
|
||||
properties.add(EnumProperty<VerticalDirection>(
|
||||
'verticalDirection', verticalDirection));
|
||||
properties.add(EnumProperty<Clip>('clipBehavior', clipBehavior));
|
||||
properties.add(EnumProperty<MainAxisAlignment>(
|
||||
'overflowWidgetAlignment', overflowWidgetAlignment));
|
||||
properties.add(FlagProperty(
|
||||
'alwaysDisplayOverflowWidget',
|
||||
value: alwaysDisplayOverflowWidget,
|
||||
ifTrue: 'always display overflow widget',
|
||||
ifFalse: 'do not always display overflow widget',
|
||||
));
|
||||
}
|
||||
}
|
||||
183
dependencies/fluent_ui-3.12.0/lib/src/layout/page.dart
vendored
Normal file
183
dependencies/fluent_ui-3.12.0/lib/src/layout/page.dart
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// The default vertical padding of the scaffold page
|
||||
///
|
||||
/// Eyeballed from Windows 10
|
||||
const double kPageDefaultVerticalPadding = 24.0;
|
||||
|
||||
/// Creates a page that follows fluent-ui design guidelines.
|
||||
///
|
||||
/// See also:
|
||||
/// * [PageHeader], usually used on the [header] property
|
||||
/// * [NavigationBody], the widget that implements fluent page transitions
|
||||
/// into navigation view.
|
||||
/// * [ScaffoldPageParent], used by [NavigationView] to tell `ScaffoldPage`
|
||||
/// if a button is necessary to be displayed before [title]
|
||||
class ScaffoldPage extends StatelessWidget {
|
||||
/// Creates a new scaffold page.
|
||||
const ScaffoldPage({
|
||||
Key? key,
|
||||
this.header,
|
||||
this.content = const SizedBox.expand(),
|
||||
this.bottomBar,
|
||||
this.padding,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Creates a scrollable page
|
||||
///
|
||||
/// The default horizontal and vertical padding is added automatically
|
||||
ScaffoldPage.scrollable({
|
||||
Key? key,
|
||||
this.header,
|
||||
this.bottomBar,
|
||||
this.padding,
|
||||
ScrollController? scrollController,
|
||||
required List<Widget> children,
|
||||
}) : content = Builder(builder: (context) {
|
||||
return ListView(
|
||||
controller: scrollController,
|
||||
padding: EdgeInsets.only(
|
||||
bottom: kPageDefaultVerticalPadding,
|
||||
left: PageHeader.horizontalPadding(context),
|
||||
right: PageHeader.horizontalPadding(context),
|
||||
),
|
||||
children: children,
|
||||
);
|
||||
}),
|
||||
super(key: key);
|
||||
|
||||
/// Creates a page with padding applied to [content]
|
||||
ScaffoldPage.withPadding({
|
||||
Key? key,
|
||||
this.header,
|
||||
this.bottomBar,
|
||||
this.padding,
|
||||
required Widget content,
|
||||
}) : content = Builder(builder: (context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: kPageDefaultVerticalPadding,
|
||||
left: PageHeader.horizontalPadding(context),
|
||||
right: PageHeader.horizontalPadding(context),
|
||||
),
|
||||
child: content,
|
||||
);
|
||||
}),
|
||||
super(key: key);
|
||||
|
||||
/// The content of this page. The content area is where most of the information
|
||||
/// for the selected nav category is displayed.
|
||||
///
|
||||
/// If this widget is scrollable, you may want to provide [contentScrollController]
|
||||
/// as well, to add a scrollbar to the right of the page.
|
||||
///
|
||||
/// 
|
||||
final Widget content;
|
||||
|
||||
/// The header of this page. Usually a [PageHeader] is used.
|
||||
///
|
||||
/// 
|
||||
final Widget? header;
|
||||
|
||||
/// The bottom bar of this page. This is usually provided when the current
|
||||
/// screen is small.
|
||||
///
|
||||
/// Usually a [BottomNavigation]
|
||||
final Widget? bottomBar;
|
||||
|
||||
/// The padding used by this widget.
|
||||
///
|
||||
/// If [contentScrollController] is not null, the scrollbar is rendered over
|
||||
/// this padding
|
||||
///
|
||||
/// If null, [PageHeader.horizontalPadding] is used horizontally and
|
||||
/// [kPageDefaultVerticalPadding] is used vertically
|
||||
final EdgeInsets? padding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = FluentTheme.of(context);
|
||||
// final parentView = InheritedNavigationView.maybeOf(context);
|
||||
return Column(children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
padding: EdgeInsets.only(
|
||||
top: padding?.top ?? kPageDefaultVerticalPadding,
|
||||
// bottom: padding?.bottom ?? kPageDefaultVerticalPadding,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (header != null) header!,
|
||||
Expanded(child: content),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (bottomBar != null) bottomBar!,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class PageHeader extends StatelessWidget {
|
||||
/// Creates a page header.
|
||||
const PageHeader({
|
||||
Key? key,
|
||||
this.leading,
|
||||
this.title,
|
||||
this.commandBar,
|
||||
this.padding,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The widget displayed before [title]. If null, some widget
|
||||
/// can be inserted here implicitly. To avoid this, set this
|
||||
/// property to `SizedBox.shrink()`.
|
||||
final Widget? leading;
|
||||
|
||||
/// The title of this bar.
|
||||
///
|
||||
/// Usually a [Text] widget.
|
||||
///
|
||||
/// 
|
||||
final Widget? title;
|
||||
|
||||
/// A bar with a list of actions an user can take
|
||||
final Widget? commandBar;
|
||||
|
||||
final double? padding;
|
||||
|
||||
static double horizontalPadding(BuildContext context) {
|
||||
assert(debugCheckHasMediaQuery(context));
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final bool isSmallScreen = screenWidth < 640.0;
|
||||
final double horizontalPadding =
|
||||
isSmallScreen ? 12.0 : kPageDefaultVerticalPadding;
|
||||
return horizontalPadding;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final leading = this.leading;
|
||||
final horizontalPadding = padding ?? PageHeader.horizontalPadding(context);
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 18.0,
|
||||
left: leading != null ? 0 : horizontalPadding,
|
||||
right: horizontalPadding,
|
||||
),
|
||||
child: Row(children: [
|
||||
if (leading != null) leading,
|
||||
Expanded(
|
||||
child: DefaultTextStyle(
|
||||
style: FluentTheme.of(context).typography.title!,
|
||||
child: title ?? const SizedBox(),
|
||||
),
|
||||
),
|
||||
if (commandBar != null) commandBar!,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
228
dependencies/fluent_ui-3.12.0/lib/src/localization.dart
vendored
Normal file
228
dependencies/fluent_ui-3.12.0/lib/src/localization.dart
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:fluent_ui/generated/l10n.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Defines the localized resource values used by the fluent widgets
|
||||
///
|
||||
/// See also:
|
||||
/// * [DefaultFluentLocalizations], the default implementation
|
||||
/// of this interface.
|
||||
abstract class FluentLocalizations {
|
||||
FluentLocalizations._();
|
||||
|
||||
/// Label for "close" buttons and menu items.
|
||||
String get closeButtonLabel;
|
||||
|
||||
/// Label for "search" text fields.
|
||||
String get searchLabel;
|
||||
|
||||
/// The tooltip for the back button on [NavigationAppBar].
|
||||
String get backButtonTooltip;
|
||||
|
||||
/// The tooltip for the toggle navigation button.
|
||||
String get closeNavigationTooltip;
|
||||
|
||||
/// The tooltip for the toogle navigation button.
|
||||
String get openNavigationTooltip;
|
||||
|
||||
/// The tooltip for the "Click to Search" button.
|
||||
String get clickToSearch;
|
||||
|
||||
/// Label read out by accessibility tools (TalkBack or VoiceOver) for a modal
|
||||
/// barrier to indicate that a tap dismisses the barrier.
|
||||
///
|
||||
/// A modal barrier can for example be found behind an alert or popup to block
|
||||
/// user interaction with elements behind it.
|
||||
String get modalBarrierDismissLabel;
|
||||
|
||||
/// The tooltip used by the "Minimize" button on desktop windows.
|
||||
String get minimizeWindowTooltip;
|
||||
|
||||
/// The tooltip used by the "Restore" button on desktop windows.
|
||||
String get restoreWindowTooltip;
|
||||
|
||||
/// The tooltip used by the "Close" button on desktop windows.
|
||||
String get closeWindowTooltip;
|
||||
|
||||
/// The dialog label
|
||||
String get dialogLabel;
|
||||
|
||||
/// The label used by [TabView]'s new button
|
||||
String get newTabLabel;
|
||||
|
||||
/// The label used by [TabView]'s close button
|
||||
String get closeTabLabel;
|
||||
|
||||
/// The label used by [TabView]'s scroll backward button
|
||||
String get scrollTabBackwardLabel;
|
||||
|
||||
/// The label used by [TabView]'s scroll forward button
|
||||
String get scrollTabForwardLabel;
|
||||
|
||||
/// The label used by [AutoSuggestBox] when the results can't be found
|
||||
String get noResultsFoundLabel;
|
||||
|
||||
/// The label for the cut action on the text selection controls
|
||||
String get cutActionLabel;
|
||||
|
||||
/// The cut shortcut label used by text selection controls
|
||||
String get cutShortcut;
|
||||
|
||||
/// The tooltip for the cut action on the text selection controls
|
||||
String get cutActionTooltip;
|
||||
|
||||
/// The label for the copy action on the text selection controls
|
||||
String get copyActionLabel;
|
||||
|
||||
/// The copy shortcut label used by text selection controls
|
||||
String get copyShortcut;
|
||||
|
||||
/// The tooltip for the copy action on the text selection controls
|
||||
String get copyActionTooltip;
|
||||
|
||||
/// The label for the paste button on the text selection controls
|
||||
String get pasteActionLabel;
|
||||
|
||||
/// The paste shortcut label used by text selection controls
|
||||
String get pasteShortcut;
|
||||
|
||||
/// The tooltip for the paste action on the text selection controls
|
||||
String get pasteActionTooltip;
|
||||
|
||||
/// The label for the select all button on the text selection controls
|
||||
String get selectAllActionLabel;
|
||||
|
||||
/// The select all shortcut label used by text selection controls
|
||||
String get selectAllShortcut;
|
||||
|
||||
/// The tooltip for the select all action on the text selection controls
|
||||
String get selectAllActionTooltip;
|
||||
|
||||
/// The `FluentLocalizations` from the closest [Localizations] instance
|
||||
/// that encloses the given context.
|
||||
///
|
||||
/// If no [FluentLocalizations] are available in the given `context`, this
|
||||
/// method throws an exception.
|
||||
///
|
||||
/// This method is just a convenient shorthand for:
|
||||
/// `Localizations.of<FluentLocalizations>(context, FluentLocalizations)!`.
|
||||
///
|
||||
/// References to the localized resources defined by this class are typically
|
||||
/// written in terms of this method. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// tooltip: FluentLocalizations.of(context).backButtonTooltip,
|
||||
/// ```
|
||||
static FluentLocalizations of(BuildContext context) {
|
||||
assert(debugCheckHasFluentLocalizations(context));
|
||||
return Localizations.of<FluentLocalizations>(context, FluentLocalizations)!;
|
||||
}
|
||||
}
|
||||
|
||||
// List of supported locales. This MUST be on sync with available intl_xx.arb
|
||||
// files in lib/l10n folder
|
||||
//
|
||||
// NOTE: This should be INTO DefaultFluentLocalizations as an static member but,
|
||||
// for some strange reason, doing so results in a very strange compile error.
|
||||
// This has been the only way to get it working without errors.
|
||||
|
||||
// I tried to replace this with S.delegate.supportedLocales, but doing this
|
||||
// din't let me set the default value in FluentApp.supportedLocales
|
||||
const List<Locale> defaultSupportedLocales = <Locale>[
|
||||
Locale('ar'),
|
||||
Locale('de'),
|
||||
Locale('en'),
|
||||
Locale('es'),
|
||||
Locale('fr'),
|
||||
Locale('hi'),
|
||||
Locale('pt'),
|
||||
Locale('ru'),
|
||||
Locale('zh'),
|
||||
];
|
||||
|
||||
/// Strings for the fluent widgets.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [FluentApp.localizationsDelegates], which automatically includes
|
||||
/// * [DefaultFluentLocalizations.delegate] by default.
|
||||
class DefaultFluentLocalizations extends S implements FluentLocalizations {
|
||||
final Locale locale;
|
||||
|
||||
DefaultFluentLocalizations._defaultFluentLocalizations(this.locale) {
|
||||
S.load(locale);
|
||||
}
|
||||
|
||||
static bool supports(Locale locale) {
|
||||
return S.delegate.supportedLocales.contains(locale);
|
||||
}
|
||||
|
||||
// Special cases - Those that include operating system dependent messages
|
||||
|
||||
String get _ctrlCmd {
|
||||
if (defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
return 'Cmd';
|
||||
}
|
||||
return 'Ctrl';
|
||||
}
|
||||
|
||||
String get _closeTabCmd {
|
||||
if (defaultTargetPlatform == TargetPlatform.macOS) {
|
||||
return 'W';
|
||||
}
|
||||
return 'F4';
|
||||
}
|
||||
|
||||
// Close tab => <Message> (<shortcut>)
|
||||
@override
|
||||
String get closeTabLabel {
|
||||
return '${super.closeTabLabelSuffix} ($_ctrlCmd+$_closeTabCmd)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get cutShortcut => '$_ctrlCmd+X';
|
||||
|
||||
@override
|
||||
String get copyShortcut => '$_ctrlCmd+C';
|
||||
|
||||
@override
|
||||
String get pasteShortcut => '$_ctrlCmd+V';
|
||||
|
||||
@override
|
||||
String get selectAllShortcut => '$_ctrlCmd+A';
|
||||
|
||||
/// Creates an object that provides localized resource values for the fluent
|
||||
/// library widgets.
|
||||
///
|
||||
/// This method is typically used to create a [LocalizationsDelegate].
|
||||
/// The [FluentApp] does so by default.
|
||||
static Future<FluentLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<FluentLocalizations>(
|
||||
DefaultFluentLocalizations._defaultFluentLocalizations(locale));
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<FluentLocalizations> delegate =
|
||||
_FluentLocalizationsDelegate();
|
||||
}
|
||||
|
||||
class _FluentLocalizationsDelegate
|
||||
extends LocalizationsDelegate<FluentLocalizations> {
|
||||
const _FluentLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) {
|
||||
return DefaultFluentLocalizations.supports(locale);
|
||||
// defaultSupportedLocales.contains(locale);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FluentLocalizations> load(Locale locale) {
|
||||
return DefaultFluentLocalizations.load(locale);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReload(_FluentLocalizationsDelegate old) => false;
|
||||
|
||||
@override
|
||||
String toString() => DefaultFluentLocalizations.delegate.toString();
|
||||
}
|
||||
57
dependencies/fluent_ui-3.12.0/lib/src/navigation/route.dart
vendored
Normal file
57
dependencies/fluent_ui-3.12.0/lib/src/navigation/route.dart
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// A modal route that replaces the entire screen.
|
||||
class FluentPageRoute<T> extends PageRoute<T> {
|
||||
late final WidgetBuilder _builder;
|
||||
// ignore: prefer_final_fields
|
||||
bool _maintainState = true;
|
||||
final String? _barrierLabel;
|
||||
|
||||
/// Creates a modal route that replaces the entire screen.
|
||||
FluentPageRoute({
|
||||
bool maintainState = true,
|
||||
String? barrierLabel,
|
||||
required WidgetBuilder builder,
|
||||
RouteSettings? settings,
|
||||
bool fullscreenDialog = false,
|
||||
}) : _maintainState = maintainState,
|
||||
_barrierLabel = barrierLabel,
|
||||
_builder = builder,
|
||||
super(
|
||||
settings: settings,
|
||||
fullscreenDialog: fullscreenDialog,
|
||||
);
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => _barrierLabel;
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final Widget result = _builder(context);
|
||||
return Semantics(
|
||||
scopesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
child: DrillInPageTransition(
|
||||
animation: CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
),
|
||||
child: result,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get maintainState => _maintainState;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 200);
|
||||
}
|
||||
582
dependencies/fluent_ui-3.12.0/lib/src/styles/acrylic.dart
vendored
Normal file
582
dependencies/fluent_ui-3.12.0/lib/src/styles/acrylic.dart
vendored
Normal file
@@ -0,0 +1,582 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show ImageFilter;
|
||||
import 'dart:ui' as ui show Image;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' as m;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
const double kBlurAmount = 30.0;
|
||||
|
||||
const double kDefaultAcrylicAlpha = 0.8;
|
||||
|
||||
/// Acrylic is a type of Brush that creates a translucent texture.
|
||||
/// You can apply acrylic to app surfaces to add depth and help
|
||||
/// establish a visual hierarchy.
|
||||
///
|
||||
/// 
|
||||
class Acrylic extends StatefulWidget {
|
||||
const Acrylic({
|
||||
Key? key,
|
||||
this.tint,
|
||||
this.child,
|
||||
this.tintAlpha,
|
||||
this.luminosityAlpha,
|
||||
this.blurAmount,
|
||||
this.shape,
|
||||
this.shadowColor,
|
||||
this.elevation = 0.0,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The tint to apply to the acrylic layers.
|
||||
///
|
||||
/// Defaults to the acrylicBackgroundColor from the nearest [FluentTheme].
|
||||
final Color? tint;
|
||||
|
||||
/// The opacity applied to the [tint] from 0.0 to 1.0.
|
||||
///
|
||||
/// Defaults to 0.8.
|
||||
final double? tintAlpha;
|
||||
|
||||
/// The child contained by this box
|
||||
final Widget? child;
|
||||
|
||||
/// The opacity applied to the luminosity layer of the acrylic, from 0.0 to 1.0.
|
||||
///
|
||||
/// Defaults to 0.8.
|
||||
final double? luminosityAlpha;
|
||||
|
||||
/// The amount of blur to apply to the content behind the acrylic.
|
||||
///
|
||||
/// Defaults to 30.
|
||||
final double? blurAmount;
|
||||
|
||||
/// The shape of the acrylic.
|
||||
///
|
||||
/// Defaults to a square [RoundedRectangleBorder].
|
||||
final ShapeBorder? shape;
|
||||
|
||||
/// The color of the elevation
|
||||
///
|
||||
/// Defaults to the shadowColor from the nearest [FluentTheme].
|
||||
final Color? shadowColor;
|
||||
|
||||
/// The z-coordinate relative to the parent at which to place this physical object.
|
||||
///
|
||||
/// The value is non-negative. Defaults to 0.
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ColorProperty('tint', tint));
|
||||
properties.add(DoubleProperty('tintAlpha', tintAlpha));
|
||||
properties.add(DoubleProperty('luminosityAlpha', luminosityAlpha));
|
||||
properties.add(DoubleProperty('blurAmount', blurAmount));
|
||||
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape));
|
||||
properties.add(ColorProperty('shadowColor', shadowColor));
|
||||
properties.add(DoubleProperty('elevation', elevation));
|
||||
}
|
||||
|
||||
@override
|
||||
State<Acrylic> createState() => _AcrylicState();
|
||||
}
|
||||
|
||||
class _AcrylicState extends State<Acrylic> {
|
||||
AcrylicProperties _properties = const AcrylicProperties.empty();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_NoiseTextureCacher._instance ??= _NoiseTextureCacher._new();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
_updateProperties();
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Acrylic old) {
|
||||
super.didUpdateWidget(old);
|
||||
|
||||
if (_compareAcrylics(old)) {
|
||||
_updateProperties();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_updateProperties();
|
||||
}
|
||||
|
||||
bool _compareAcrylics(Acrylic other) {
|
||||
return widget.blurAmount != other.blurAmount ||
|
||||
widget.elevation != other.elevation ||
|
||||
widget.luminosityAlpha != other.luminosityAlpha ||
|
||||
widget.shape != other.shape ||
|
||||
widget.tint != other.tint ||
|
||||
widget.tintAlpha != other.tintAlpha;
|
||||
}
|
||||
|
||||
void _updateProperties() {
|
||||
_properties = AcrylicProperties(
|
||||
tint: widget.tint ?? FluentTheme.of(context).acrylicBackgroundColor,
|
||||
tintAlpha: widget.tintAlpha ?? kDefaultAcrylicAlpha,
|
||||
luminosityAlpha: widget.luminosityAlpha ?? kDefaultAcrylicAlpha,
|
||||
blurAmount: widget.blurAmount ?? 30,
|
||||
shape: widget.shape ?? const RoundedRectangleBorder(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
assert(widget.elevation >= 0, "The elevation must be always positive");
|
||||
assert(_properties.tintAlpha >= 0, "The tintAlpha must be always positive");
|
||||
assert(_properties.luminosityAlpha >= 0,
|
||||
"The luminosityAlpha must be always positive");
|
||||
|
||||
final Color shadowColor =
|
||||
widget.shadowColor ?? FluentTheme.of(context).shadowColor;
|
||||
|
||||
return _AcrylicInheritedWidget(
|
||||
state: this,
|
||||
child: DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: _properties.shape,
|
||||
shadows: [
|
||||
/* The shadows were taken from the official FluentUI design kit on Figma */
|
||||
BoxShadow(
|
||||
color: shadowColor.withOpacity(0.13),
|
||||
blurRadius: 0.9 * widget.elevation,
|
||||
offset: Offset(0, 0.4 * widget.elevation),
|
||||
),
|
||||
BoxShadow(
|
||||
color: shadowColor.withOpacity(0.11),
|
||||
blurRadius: 0.225 * widget.elevation,
|
||||
offset: Offset(0, 0.085 * widget.elevation),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _AcrylicGuts(
|
||||
child: m.Material(
|
||||
type: m.MaterialType.transparency,
|
||||
shape: widget.shape,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedAcrylic extends ImplicitlyAnimatedWidget {
|
||||
const AnimatedAcrylic({
|
||||
Key? key,
|
||||
this.tint,
|
||||
this.child,
|
||||
this.tintAlpha,
|
||||
this.luminosityAlpha,
|
||||
this.blurAmount,
|
||||
this.shape,
|
||||
this.shadowColor,
|
||||
this.elevation = 0.0,
|
||||
Curve curve = Curves.linear,
|
||||
required Duration duration,
|
||||
}) : super(key: key, curve: curve, duration: duration);
|
||||
|
||||
/// The tint to apply to the acrylic layers.
|
||||
///
|
||||
/// Defaults to the acrylicBackgroundColor from the nearest [FluentTheme].
|
||||
final Color? tint;
|
||||
|
||||
/// The opacity applied to the [tint] from 0.0 to 1.0.
|
||||
///
|
||||
/// Defaults to 0.8.
|
||||
final double? tintAlpha;
|
||||
|
||||
/// The child contained by this box
|
||||
final Widget? child;
|
||||
|
||||
/// The opacity applied to the luminosity layer of the acrylic, from 0.0 to 1.0.
|
||||
///
|
||||
/// Defaults to 0.8.
|
||||
final double? luminosityAlpha;
|
||||
|
||||
/// The amount of blur to apply to the content behind the acrylic.
|
||||
///
|
||||
/// Defaults to 30.
|
||||
final double? blurAmount;
|
||||
|
||||
/// The shape of the acrylic.
|
||||
///
|
||||
/// Defaults to a square [RoundedRectangleBorder].
|
||||
final ShapeBorder? shape;
|
||||
|
||||
/// The color of the elevation
|
||||
///
|
||||
/// Defaults to the shadowColor from the nearest [FluentTheme].
|
||||
final Color? shadowColor;
|
||||
|
||||
/// The z-coordinate relative to the parent at which to place this physical object.
|
||||
///
|
||||
/// The value is non-negative. Defaults to 0.
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(ColorProperty('tint', tint));
|
||||
properties.add(DoubleProperty('tintAlpha', tintAlpha));
|
||||
properties.add(DoubleProperty('luminosityAlpha', luminosityAlpha));
|
||||
properties.add(DoubleProperty('blurAmount', blurAmount));
|
||||
properties.add(DiagnosticsProperty<ShapeBorder>('shape', shape));
|
||||
properties.add(ColorProperty('shadowColor', shadowColor));
|
||||
properties.add(DoubleProperty('elevation', elevation));
|
||||
}
|
||||
|
||||
@override
|
||||
_AnimatedAcrylicState createState() => _AnimatedAcrylicState();
|
||||
}
|
||||
|
||||
class _AnimatedAcrylicState extends AnimatedWidgetBaseState<AnimatedAcrylic> {
|
||||
ColorTween? _tint;
|
||||
Tween<double?>? _tintAlpha;
|
||||
Tween<double?>? _luminosityAlpha;
|
||||
Tween<double?>? _blurAmount;
|
||||
m.ShapeBorderTween? _shape;
|
||||
ColorTween? _shadowColor;
|
||||
Tween<double?>? _elevation;
|
||||
|
||||
@override
|
||||
void forEachTween(TweenVisitor<dynamic> visitor) {
|
||||
_tint = visitor(_tint, widget.tint,
|
||||
(dynamic value) => ColorTween(begin: value as Color)) as ColorTween?;
|
||||
_tintAlpha = visitor(_tintAlpha, widget.tintAlpha,
|
||||
(dynamic value) => Tween<double>(begin: value as double))
|
||||
as Tween<double>?;
|
||||
_luminosityAlpha = visitor(_luminosityAlpha, widget.luminosityAlpha,
|
||||
(dynamic value) => Tween<double>(begin: value as double))
|
||||
as Tween<double>?;
|
||||
_blurAmount = visitor(_blurAmount, widget.blurAmount,
|
||||
(dynamic value) => Tween<double>(begin: value as double))
|
||||
as Tween<double>?;
|
||||
_shape = visitor(_shape, widget.shape,
|
||||
(dynamic value) => m.ShapeBorderTween(begin: value as ShapeBorder))
|
||||
as m.ShapeBorderTween?;
|
||||
_shadowColor = visitor(_shadowColor, widget.shadowColor,
|
||||
(dynamic value) => ColorTween(begin: value as Color)) as ColorTween?;
|
||||
_elevation = visitor(_elevation, widget.elevation,
|
||||
(dynamic value) => Tween<double>(begin: value as double))
|
||||
as Tween<double>?;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Acrylic(
|
||||
tint: _tint?.evaluate(animation),
|
||||
tintAlpha: _tintAlpha?.evaluate(animation),
|
||||
luminosityAlpha: _luminosityAlpha?.evaluate(animation),
|
||||
blurAmount: _blurAmount?.evaluate(animation),
|
||||
shape: _shape?.evaluate(animation),
|
||||
shadowColor: _shadowColor?.evaluate(animation),
|
||||
elevation: _elevation?.evaluate(animation) ?? 0,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the properties of an Acrylic material
|
||||
@immutable
|
||||
class AcrylicProperties {
|
||||
final Color tint;
|
||||
final double tintAlpha;
|
||||
final double luminosityAlpha;
|
||||
final double blurAmount;
|
||||
final ShapeBorder shape;
|
||||
|
||||
const AcrylicProperties({
|
||||
required this.tint,
|
||||
required this.tintAlpha,
|
||||
required this.luminosityAlpha,
|
||||
required this.blurAmount,
|
||||
required this.shape,
|
||||
});
|
||||
|
||||
const AcrylicProperties.empty()
|
||||
: tint = Colors.black,
|
||||
tintAlpha = kDefaultAcrylicAlpha,
|
||||
luminosityAlpha = kDefaultAcrylicAlpha,
|
||||
blurAmount = kBlurAmount,
|
||||
shape = const RoundedRectangleBorder();
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(
|
||||
tint,
|
||||
tintAlpha,
|
||||
luminosityAlpha,
|
||||
blurAmount,
|
||||
shape,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other is AcrylicProperties) {
|
||||
return tint == other.tint &&
|
||||
tintAlpha == other.tintAlpha &&
|
||||
luminosityAlpha == other.luminosityAlpha &&
|
||||
blurAmount == other.blurAmount &&
|
||||
shape == other.shape;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static AcrylicProperties of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<_AcrylicInheritedWidget>()!
|
||||
.state
|
||||
._properties;
|
||||
}
|
||||
}
|
||||
|
||||
class _AcrylicInheritedWidget extends InheritedWidget {
|
||||
final _AcrylicState state;
|
||||
|
||||
const _AcrylicInheritedWidget({
|
||||
required this.state,
|
||||
required Widget child,
|
||||
}) : super(child: child);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_AcrylicInheritedWidget old) {
|
||||
return state != old.state;
|
||||
}
|
||||
}
|
||||
|
||||
class _AcrylicGuts extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const _AcrylicGuts({required this.child, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final properties = AcrylicProperties.of(context);
|
||||
final tint = AcrylicHelper.getEffectiveTintColor(
|
||||
properties.tint,
|
||||
AcrylicHelper.getTintOpacityModifier(properties.tint),
|
||||
);
|
||||
|
||||
final disabled = DisableAcrylic.of(context) != null;
|
||||
|
||||
return ClipPath(
|
||||
clipper: ShapeBorderClipper(shape: properties.shape),
|
||||
child: CustomPaint(
|
||||
painter: _AcrylicPainter(
|
||||
tintColor: tint,
|
||||
luminosityColor: AcrylicHelper.getLuminosityColor(
|
||||
tint,
|
||||
properties.luminosityAlpha,
|
||||
),
|
||||
),
|
||||
child: disabled
|
||||
? child
|
||||
: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: properties.blurAmount,
|
||||
sigmaY: properties.blurAmount,
|
||||
),
|
||||
child: Stack(children: [
|
||||
const Opacity(
|
||||
opacity: 0.02,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
"assets/AcrylicNoise.png",
|
||||
package: "fluent_ui",
|
||||
),
|
||||
alignment: Alignment.topLeft,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
backgroundBlendMode: BlendMode.srcOver,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
),
|
||||
child,
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AcrylicPainter extends CustomPainter {
|
||||
static final Color red = const Color(0xFFFF0000).withOpacity(0.12);
|
||||
static final Color blue = const Color(0xFF00FF00).withOpacity(0.12);
|
||||
static final Color green = const Color(0xFF0000FF).withOpacity(0.12);
|
||||
|
||||
final Color luminosityColor;
|
||||
final Color tintColor;
|
||||
|
||||
_AcrylicPainter({
|
||||
required this.luminosityColor,
|
||||
required this.tintColor,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas.drawColor(luminosityColor, BlendMode.luminosity);
|
||||
canvas.drawColor(red, BlendMode.saturation);
|
||||
canvas.drawColor(blue, BlendMode.saturation);
|
||||
canvas.drawColor(green, BlendMode.saturation);
|
||||
canvas.drawColor(
|
||||
tintColor,
|
||||
tintColor.opacity == 1 ? BlendMode.srcIn : BlendMode.color,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_AcrylicPainter old) {
|
||||
return luminosityColor != old.luminosityColor || tintColor != old.tintColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Credits: @HrX03 (https://github.com/hrx03)
|
||||
/// Microsoft utils converted from C# to dart
|
||||
class AcrylicHelper {
|
||||
static Color getEffectiveTintColor(Color color, double opacity) {
|
||||
// Update tintColor's alpha with the combined opacity value
|
||||
// If LuminosityOpacity was specified, we don't intervene into users parameters
|
||||
return color.withOpacity(opacity);
|
||||
}
|
||||
|
||||
static Color getLuminosityColor(Color tintColor, double? luminosityOpacity) {
|
||||
// If luminosity opacity is specified, just use the values as is
|
||||
if (luminosityOpacity != null) {
|
||||
return Color.fromRGBO(
|
||||
tintColor.red,
|
||||
tintColor.green,
|
||||
tintColor.blue,
|
||||
luminosityOpacity.clamp(0.0, 1.0),
|
||||
);
|
||||
} else {
|
||||
// To create the Luminosity blend input color without luminosity opacity,
|
||||
// we're taking the TintColor input, converting to HSV, and clamping the V between these values
|
||||
const double minHsvV = 0.125;
|
||||
const double maxHsvV = 0.965;
|
||||
|
||||
HSVColor hsvTintColor = HSVColor.fromColor(tintColor);
|
||||
|
||||
double clampedHsvV = hsvTintColor.value.clamp(minHsvV, maxHsvV);
|
||||
|
||||
HSVColor hsvLuminosityColor = hsvTintColor.withValue(clampedHsvV);
|
||||
Color rgbLuminosityColor = hsvLuminosityColor.toColor();
|
||||
|
||||
// Now figure out luminosity opacity
|
||||
// Map original *tint* opacity to this range
|
||||
const double minLuminosityOpacity = 0.15;
|
||||
const double maxLuminosityOpacity = 1.03;
|
||||
|
||||
const double luminosityOpacityRangeMax =
|
||||
maxLuminosityOpacity - minLuminosityOpacity;
|
||||
double mappedTintOpacity =
|
||||
((tintColor.alpha / 255.0) * luminosityOpacityRangeMax) +
|
||||
minLuminosityOpacity;
|
||||
|
||||
// Finally, combine the luminosity opacity and the HsvV-clamped tint color
|
||||
return Color.fromRGBO(
|
||||
rgbLuminosityColor.red,
|
||||
rgbLuminosityColor.green,
|
||||
rgbLuminosityColor.blue,
|
||||
math.min(mappedTintOpacity, 1.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static double getTintOpacityModifier(Color color) {
|
||||
// Mid point of HsvV range that these calculations are based on. This is here for easy tuning.
|
||||
const double midPoint = 0.50;
|
||||
|
||||
const double whiteMaxOpacity = 0.45; // 100% luminosity
|
||||
const double midPointMaxOpacity = 0.90; // 50% luminosity
|
||||
const double blackMaxOpacity = 0.85; // 0% luminosity
|
||||
|
||||
HSVColor hsv = HSVColor.fromColor(color);
|
||||
|
||||
double opacityModifier = midPointMaxOpacity;
|
||||
|
||||
if (hsv.value != midPoint) {
|
||||
// Determine maximum suppression amount
|
||||
double lowestMaxOpacity = midPointMaxOpacity;
|
||||
double maxDeviation = midPoint;
|
||||
|
||||
if (hsv.value > midPoint) {
|
||||
lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV)
|
||||
maxDeviation = 1 - maxDeviation;
|
||||
} else if (hsv.value < midPoint) {
|
||||
lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV)
|
||||
}
|
||||
|
||||
double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity;
|
||||
|
||||
// Determine normalized deviation from the midpoint
|
||||
double deviation = (hsv.value - midPoint);
|
||||
double normalizedDeviation = deviation / maxDeviation;
|
||||
|
||||
// If we have saturation, reduce opacity suppression to allow that color to come through more
|
||||
if (hsv.saturation > 0) {
|
||||
// Dampen opacity suppression based on how much saturation there is
|
||||
maxOpacitySuppression *= math.max(1 - (hsv.saturation * 2), 0.0);
|
||||
}
|
||||
|
||||
double opacitySuppression = maxOpacitySuppression * normalizedDeviation;
|
||||
|
||||
opacityModifier = midPointMaxOpacity - opacitySuppression;
|
||||
}
|
||||
|
||||
return opacityModifier;
|
||||
}
|
||||
}
|
||||
|
||||
class _NoiseTextureCacher {
|
||||
static _NoiseTextureCacher? _instance;
|
||||
|
||||
ui.Image? texture;
|
||||
|
||||
_NoiseTextureCacher._new() {
|
||||
_computeImage();
|
||||
}
|
||||
|
||||
void _computeImage() async {
|
||||
const ImageProvider provider = AssetImage(
|
||||
"assets/AcrylicNoise.png",
|
||||
package: "fluent_ui",
|
||||
);
|
||||
|
||||
provider.resolve(const ImageConfiguration()).addListener(
|
||||
ImageStreamListener((image, synchronousCall) {
|
||||
texture = image.image;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DisableAcrylic extends InheritedWidget {
|
||||
const DisableAcrylic({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
static DisableAcrylic? of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<DisableAcrylic>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(DisableAcrylic oldWidget) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
410
dependencies/fluent_ui-3.12.0/lib/src/styles/color.dart
vendored
Normal file
410
dependencies/fluent_ui-3.12.0/lib/src/styles/color.dart
vendored
Normal file
@@ -0,0 +1,410 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// All the fluent colors
|
||||
class Colors {
|
||||
/// The transparent color. This should not be used in animations
|
||||
/// because it'll cause a weird effect.
|
||||
static const Color transparent = Color(0x00000000);
|
||||
|
||||
/// A black opaque color.
|
||||
static const Color black = Color(0xFF000000);
|
||||
|
||||
/// The grey color.
|
||||
///
|
||||
/// It's a shaded color with the following available shades:
|
||||
/// - 220
|
||||
/// - 210
|
||||
/// - 200
|
||||
/// - 190
|
||||
/// - 180
|
||||
/// - 170
|
||||
/// - 160
|
||||
/// - 150
|
||||
/// - 140
|
||||
/// - 130
|
||||
/// - 120
|
||||
/// - 110
|
||||
/// - 100
|
||||
/// - 90
|
||||
/// - 80
|
||||
/// - 70
|
||||
/// - 60
|
||||
/// - 50
|
||||
/// - 40
|
||||
/// - 30
|
||||
/// - 20
|
||||
/// - 10
|
||||
///
|
||||
/// To use any of these shades, call `Colors.grey[SHADE]`,
|
||||
/// where `SHADE` is the number of the shade you want
|
||||
static const ShadedColor grey = ShadedColor(
|
||||
0xFF323130, // grey160
|
||||
<int, Color>{
|
||||
220: Color(0xFF11100F),
|
||||
210: Color(0xFF161514),
|
||||
200: Color(0xFF1B1A19),
|
||||
190: Color(0xFF201F1E),
|
||||
180: Color(0xFF252423),
|
||||
170: Color(0xFF292827),
|
||||
160: Color(0xFF323130),
|
||||
150: Color(0xFF3B3A39),
|
||||
140: Color(0xFF484644),
|
||||
130: Color(0xFF605E5C),
|
||||
120: Color(0xFF797775),
|
||||
110: Color(0xFF8A8886),
|
||||
100: Color(0xFF979593),
|
||||
90: Color(0xFFA19F9D),
|
||||
80: Color(0xFFB3B0AD),
|
||||
70: Color(0xFFBEBBB8),
|
||||
60: Color(0xFFC8C6C4),
|
||||
50: Color(0xFFD2D0CE),
|
||||
40: Color(0xFFE1DFDD),
|
||||
30: Color(0xFFEDEBE9),
|
||||
20: Color(0xFFF3F2F1),
|
||||
10: Color(0xFFFAF9F8),
|
||||
},
|
||||
);
|
||||
|
||||
/// A opaque white color.
|
||||
static const Color white = Color(0xFFFFFFFF);
|
||||
|
||||
static final AccentColor yellow = AccentColor.swatch(const <String, Color>{
|
||||
'darkest': Color(0xfff9a825),
|
||||
'darker': Color(0xfffbc02d),
|
||||
'dark': Color(0xfffdd835),
|
||||
'normal': Color(0xffffeb3b),
|
||||
'light': Color(0xffffee58),
|
||||
'lighter': Color(0xfffff176),
|
||||
'lightest': Color(0xfffff59d),
|
||||
});
|
||||
|
||||
static final AccentColor orange = AccentColor.swatch(const <String, Color>{
|
||||
'darkest': Color(0xff993d07),
|
||||
'darker': Color(0xffac4508),
|
||||
'dark': Color(0xffd1540a),
|
||||
'normal': Color(0xfff7630c),
|
||||
'light': Color(0xfff87a30),
|
||||
'lighter': Color(0xfff99154),
|
||||
'lightest': Color(0xfffa9e68),
|
||||
});
|
||||
|
||||
static final AccentColor red = AccentColor.swatch(const <String, Color>{
|
||||
'darkest': Color(0xff8f0a15),
|
||||
'darker': Color(0xffa20b18),
|
||||
'dark': Color(0xffb90d1c),
|
||||
'normal': Color(0xffe81123),
|
||||
'light': Color(0xffec404f),
|
||||
'lighter': Color(0xffee5865),
|
||||
'lightest': Color(0xfff06b76),
|
||||
});
|
||||
|
||||
static final AccentColor magenta = AccentColor.swatch(const <String, Color>{
|
||||
'darkest': Color(0xff6f0061),
|
||||
'darker': Color(0xff7e006e),
|
||||
'dark': Color(0xff90007e),
|
||||
'normal': Color(0xffb4009e),
|
||||
'light': Color(0xffc333b1),
|
||||
'lighter': Color(0xffca4cbb),
|
||||
'lightest': Color(0xffd060c2),
|
||||
});
|
||||
|
||||
static final AccentColor purple = AccentColor.swatch(const <String, Color>{
|
||||
'darkest': Color(0xff472f68),
|
||||
'darker': Color(0xff513576),
|
||||
'dark': Color(0xff644293),
|
||||
'normal': Color(0xFF744da9),
|
||||
'light': Color(0xff8664b4),
|
||||
'lighter': Color(0xff9d82c2),
|
||||
'lightest': Color(0xffa890c9),
|
||||
});
|
||||
|
||||
static final AccentColor blue = AccentColor.swatch(const <String, Color>{
|
||||
'darkest': Color(0xff004a83),
|
||||
'darker': Color(0xff005494),
|
||||
'dark': Color(0xff0066b4),
|
||||
'normal': Color(0xff0078d4),
|
||||
'light': Color(0xff268cda),
|
||||
'lighter': Color(0xff4ca0e0),
|
||||
'lightest': Color(0xff60abe4),
|
||||
});
|
||||
|
||||
static final AccentColor teal = AccentColor.swatch(const <String, Color>{
|
||||
'darkest': Color(0xff006e5b),
|
||||
'darker': Color(0xff007c67),
|
||||
'dark': Color(0xff00977d),
|
||||
'normal': Color(0xff00b294),
|
||||
'light': Color(0xff26bda4),
|
||||
'lighter': Color(0xff4cc9b4),
|
||||
'lightest': Color(0xff60cfbc),
|
||||
});
|
||||
|
||||
static final AccentColor green = AccentColor.swatch(const <String, Color>{
|
||||
'darkest': Color(0xff094c09),
|
||||
'darker': Color(0xff0c5d0c),
|
||||
'dark': Color(0xff0e6f0e),
|
||||
'normal': Color(0xff107c10),
|
||||
'light': Color(0xff278927),
|
||||
'lighter': Color(0xff4b9c4b),
|
||||
'lightest': Color(0xff6aad6a),
|
||||
});
|
||||
|
||||
static const Color warningPrimaryColor = Color(0xFFd83b01);
|
||||
static final warningSecondaryColor = AccentColor.swatch(const <String, Color>{
|
||||
'dark': Color(0xFF433519),
|
||||
'normal': Color(0xFFfff4ce),
|
||||
});
|
||||
static const Color errorPrimaryColor = Color(0xFFa80000);
|
||||
static final errorSecondaryColor = AccentColor.swatch(const <String, Color>{
|
||||
'dark': Color(0xFF442726),
|
||||
'normal': Color(0xFFfde7e9),
|
||||
});
|
||||
static const Color successPrimaryColor = Color(0xFF107c10);
|
||||
static final successSecondaryColor = AccentColor.swatch(const <String, Color>{
|
||||
'dark': Color(0xFF393d1b),
|
||||
'normal': Color(0xFFdff6dd),
|
||||
});
|
||||
|
||||
/// A list of all the accent colors provided by this library.
|
||||
static final List<AccentColor> accentColors = [
|
||||
yellow,
|
||||
orange,
|
||||
red,
|
||||
magenta,
|
||||
purple,
|
||||
blue,
|
||||
teal,
|
||||
green,
|
||||
];
|
||||
}
|
||||
|
||||
class ShadedColor extends ColorSwatch<int> {
|
||||
const ShadedColor(int primary, Map<int, Color> swatch)
|
||||
: super(primary, swatch);
|
||||
|
||||
@override
|
||||
Color operator [](int index) {
|
||||
return super[index]!;
|
||||
}
|
||||
}
|
||||
|
||||
/// An accent color is a color that can have multiple shades. It's similar to
|
||||
/// [ShadedColor] and [ColorSwatch], but it has helper methods to help you
|
||||
/// access the color variant you want easily. These shades may not be accessible
|
||||
/// on every accent color.
|
||||
///
|
||||
/// This library already provides some accent colors by default:
|
||||
///
|
||||
/// - [Colors.yellow]
|
||||
/// - [Colors.orange]
|
||||
/// - [Colors.red]
|
||||
/// - [Colors.magenta]
|
||||
/// - [Colors.purple]
|
||||
/// - [Colors.blue]
|
||||
/// - [Colors.teal]
|
||||
/// - [Colors.green]
|
||||
///
|
||||
/// Use [Colors.accentColors] to get all the accent colors provided
|
||||
/// by default.
|
||||
class AccentColor extends ColorSwatch<String> {
|
||||
/// The default shade for this color. This can't be null
|
||||
final String primary;
|
||||
|
||||
/// The avaiable shades for this color. This can't be null nor empty
|
||||
final Map<String, Color> swatch;
|
||||
|
||||
/// Creates a new accent color.
|
||||
AccentColor(this.primary, this.swatch)
|
||||
: super(swatch[primary]!.value, swatch);
|
||||
|
||||
/// Creates a new accent color based on a swatch
|
||||
AccentColor.swatch(this.swatch)
|
||||
: primary = 'normal',
|
||||
super(swatch['normal']!.value, swatch);
|
||||
|
||||
/// The darkest shade of the color.
|
||||
Color get darkest => swatch['darkest'] ?? darker.withOpacity(0.7);
|
||||
|
||||
/// The darker shade of the color.
|
||||
///
|
||||
/// Usually used for shadows
|
||||
Color get darker => swatch['darker'] ?? dark.withOpacity(0.8);
|
||||
|
||||
/// The dark shade of the color.
|
||||
///
|
||||
/// Usually used for the mouse press effect;
|
||||
Color get dark => swatch['dark'] ?? normal.withOpacity(0.9);
|
||||
|
||||
/// The default shade of the color.
|
||||
Color get normal => swatch['normal']!;
|
||||
|
||||
/// The light shade of the color.
|
||||
///
|
||||
/// Usually used for the mouse hover effect
|
||||
Color get light => swatch['light'] ?? normal.withOpacity(0.9);
|
||||
|
||||
/// The lighter shade of the color.
|
||||
///
|
||||
/// Usually used for shadows
|
||||
Color get lighter => swatch['lighter'] ?? light.withOpacity(0.8);
|
||||
|
||||
/// The lighest shade of the color
|
||||
Color get lightest => swatch['lightest'] ?? lighter.withOpacity(0.7);
|
||||
|
||||
static AccentColor lerp(AccentColor a, AccentColor b, double t) {
|
||||
final darkest = Color.lerp(a.darkest, b.darkest, t);
|
||||
final darker = Color.lerp(a.darker, b.darker, t);
|
||||
final dark = Color.lerp(a.dark, b.dark, t);
|
||||
final light = Color.lerp(a.light, b.light, t);
|
||||
final lighter = Color.lerp(a.lighter, b.lighter, t);
|
||||
final lightest = Color.lerp(a.lightest, b.lightest, t);
|
||||
return AccentColor.swatch({
|
||||
if (darkest != null) 'darkest': darkest,
|
||||
if (darker != null) 'darker': darker,
|
||||
if (dark != null) 'dark': dark,
|
||||
'normal': Color.lerp(a.normal, b.normal, t)!,
|
||||
if (light != null) 'light': light,
|
||||
if (lighter != null) 'lighter': lighter,
|
||||
if (lightest != null) 'lightest': lightest,
|
||||
});
|
||||
}
|
||||
|
||||
static Color resolve(Color resolvable, BuildContext context) {
|
||||
return (resolvable is AccentColor)
|
||||
? resolvable.resolveFrom(context)
|
||||
: resolvable;
|
||||
}
|
||||
|
||||
Color resolveFrom(BuildContext context, [Brightness? bright]) {
|
||||
final ThemeData? theme = FluentTheme.maybeOf(context);
|
||||
final brightness = bright ?? theme?.brightness ?? Brightness.light;
|
||||
return resolveFromBrightness(brightness);
|
||||
}
|
||||
|
||||
Color resolveFromBrightness(Brightness brightness, {int level = 0}) {
|
||||
switch (brightness) {
|
||||
case Brightness.light:
|
||||
return level == 0
|
||||
? light
|
||||
: level == 1
|
||||
? lighter
|
||||
: lightest;
|
||||
case Brightness.dark:
|
||||
return level == 0
|
||||
? dark
|
||||
: level == 1
|
||||
? darker
|
||||
: darkest;
|
||||
}
|
||||
}
|
||||
|
||||
Color resolveFromReverseBrightness(Brightness brightness, {int level = 0}) {
|
||||
switch (brightness) {
|
||||
case Brightness.dark:
|
||||
return level == 0
|
||||
? light
|
||||
: level == 1
|
||||
? lighter
|
||||
: lightest;
|
||||
case Brightness.light:
|
||||
return level == 0
|
||||
? dark
|
||||
: level == 1
|
||||
? darker
|
||||
: darkest;
|
||||
}
|
||||
}
|
||||
|
||||
Color defaultBrushFor(Brightness brightness) {
|
||||
if (brightness.isDark) {
|
||||
return lighter;
|
||||
} else {
|
||||
return dark;
|
||||
}
|
||||
}
|
||||
|
||||
Color secondaryBrushFor(Brightness brightness) {
|
||||
if (brightness.isDark) {
|
||||
return lighter.withOpacity(0.9);
|
||||
} else {
|
||||
return dark.withOpacity(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
Color tertiaryBrushFor(Brightness brightness) {
|
||||
if (brightness.isDark) {
|
||||
return lighter.withOpacity(0.8);
|
||||
} else {
|
||||
return dark.withOpacity(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension methods to help dealing with colors.
|
||||
extension ColorExtension on Color {
|
||||
/// Creates a new accent color based on this color. This provides
|
||||
/// the shades by lerping this color with [Colors.black] if dark
|
||||
/// or darker, and with [Colors.white] if light or lighter.
|
||||
///
|
||||
/// See also:
|
||||
/// - [Color.lerp]
|
||||
/// - [lerpWith]
|
||||
/// - [Colors.black]
|
||||
/// - [Color.white]
|
||||
AccentColor toAccentColor({
|
||||
double darkestFactor = 0.38,
|
||||
double darkerFactor = 0.30,
|
||||
double darkFactor = 0.15,
|
||||
double lightFactor = 0.15,
|
||||
double lighterFactor = 0.30,
|
||||
double lightestFactor = 0.38,
|
||||
}) {
|
||||
// if (this is AccentColor) {
|
||||
// return this as AccentColor;
|
||||
// }
|
||||
return AccentColor.swatch({
|
||||
'darkest': lerpWith(Colors.black, darkestFactor),
|
||||
'darker': lerpWith(Colors.black, darkerFactor),
|
||||
'dark': lerpWith(Colors.black, darkFactor),
|
||||
'normal': this,
|
||||
'light': lerpWith(Colors.white, lightFactor),
|
||||
'lighter': lerpWith(Colors.white, lighterFactor),
|
||||
'lightest': lerpWith(Colors.white, lightestFactor),
|
||||
});
|
||||
}
|
||||
|
||||
/// Get a constrast color based on the luminance of this color. If
|
||||
/// the luminance is bigger than 0.5, [darkColor] is used, otherwise
|
||||
/// [lightColor] is used.
|
||||
///
|
||||
/// This is usually used to constrast text colors with the background.
|
||||
Color basedOnLuminance({
|
||||
Color darkColor = Colors.black,
|
||||
Color lightColor = Colors.white,
|
||||
}) {
|
||||
return computeLuminance() < 0.5 ? lightColor : darkColor;
|
||||
}
|
||||
|
||||
/// Lerp this color with another color.
|
||||
///
|
||||
/// [t] must be in range of 0.0 to 1.0
|
||||
///
|
||||
/// See also:
|
||||
/// - [Color.lerp]
|
||||
Color lerpWith(Color color, double t) {
|
||||
return Color.lerp(this, color, t)!;
|
||||
}
|
||||
|
||||
Color resolve(BuildContext context) {
|
||||
if (this is AccentColor) {
|
||||
return AccentColor.resolve(this, context);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
class ColorConst extends Color {
|
||||
const ColorConst.withOpacity(int value, double opacity)
|
||||
: super(
|
||||
((((opacity * 0xff ~/ 1) & 0xff) << 24) | ((0x00ffffff & value))) &
|
||||
0xFFFFFFFF);
|
||||
}
|
||||
275
dependencies/fluent_ui-3.12.0/lib/src/styles/focus.dart
vendored
Normal file
275
dependencies/fluent_ui-3.12.0/lib/src/styles/focus.dart
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// A focus border creates an animated border around a widget
|
||||
/// whenever it has the application primary focus.
|
||||
///
|
||||
/// 
|
||||
class FocusBorder extends StatelessWidget {
|
||||
/// Creates a focus border.
|
||||
const FocusBorder({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.focused = true,
|
||||
this.style,
|
||||
this.renderOutside,
|
||||
this.useStackApproach = true,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The child that will receive the border
|
||||
final Widget child;
|
||||
|
||||
/// Whether to show the border. Defaults to true
|
||||
final bool focused;
|
||||
|
||||
/// The style of this focus border. If non-null, this
|
||||
/// is mescled with [ThemeData.focusThemeData]
|
||||
final FocusThemeData? style;
|
||||
|
||||
/// Whether the border should be rendered outside of the
|
||||
/// box or not. If null, [FocusThemeData.renderOutside]
|
||||
/// is used.
|
||||
final bool? renderOutside;
|
||||
|
||||
/// Whether wrapping the widget in a stack is the approach
|
||||
/// that is goind to be used to render the box. If false,
|
||||
/// a transparent border is created around the [child] as a
|
||||
/// placeholder, and the real border is only displayed when
|
||||
/// [focused] is true.
|
||||
///
|
||||
/// Using the stack approach is recommended for widgets that
|
||||
/// have a defined size (height and width). You should not use
|
||||
/// it with widgets that require dragging.
|
||||
///
|
||||
/// This property is disabled by default on the following widgets:
|
||||
/// - [Slider]
|
||||
final bool useStackApproach;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
FlagProperty('focused', value: focused, ifFalse: 'unfocused'),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<FocusThemeData>('style', style));
|
||||
properties.add(FlagProperty(
|
||||
'renderOutside',
|
||||
value: renderOutside,
|
||||
ifFalse: 'render inside',
|
||||
));
|
||||
properties.add(FlagProperty(
|
||||
'useStackApproach',
|
||||
value: useStackApproach,
|
||||
defaultValue: true,
|
||||
ifFalse: 'use border approach',
|
||||
));
|
||||
}
|
||||
|
||||
static Widget buildBorder(
|
||||
BuildContext context,
|
||||
FocusThemeData style,
|
||||
bool focused, [
|
||||
Widget? child,
|
||||
]) {
|
||||
return IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
duration: FluentTheme.of(context).fasterAnimationDuration,
|
||||
curve: FluentTheme.of(context).animationCurve,
|
||||
decoration: style.buildPrimaryDecoration(focused),
|
||||
child: DecoratedBox(
|
||||
decoration: style.buildSecondaryDecoration(focused),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final style = FocusTheme.of(context).merge(this.style);
|
||||
final double borderWidth =
|
||||
(style.primaryBorder?.width ?? 0) + (style.secondaryBorder?.width ?? 0);
|
||||
if (useStackApproach) {
|
||||
final renderOutside = this.renderOutside ?? style.renderOutside ?? true;
|
||||
final clipBehavior = renderOutside ? Clip.none : Clip.hardEdge;
|
||||
return Stack(
|
||||
fit: StackFit.passthrough,
|
||||
clipBehavior: clipBehavior,
|
||||
children: [
|
||||
child,
|
||||
Positioned.fill(
|
||||
left: renderOutside ? -borderWidth : 0,
|
||||
right: renderOutside ? -borderWidth : 0,
|
||||
top: renderOutside ? -borderWidth : 0,
|
||||
bottom: renderOutside ? -borderWidth : 0,
|
||||
child: buildBorder(context, style, focused),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return buildBorder(context, style, focused, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FocusTheme extends InheritedWidget {
|
||||
const FocusTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final FocusThemeData data;
|
||||
|
||||
static FocusThemeData of(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final theme = context.dependOnInheritedWidgetOfExactType<FocusTheme>();
|
||||
return FluentTheme.of(context).focusTheme.merge(theme?.data);
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(FocusTheme oldWidget) => oldWidget.data != data;
|
||||
}
|
||||
|
||||
class FocusThemeData with Diagnosticable {
|
||||
final BorderRadius? borderRadius;
|
||||
final BorderSide? primaryBorder;
|
||||
final BorderSide? secondaryBorder;
|
||||
final Color? glowColor;
|
||||
final double? glowFactor;
|
||||
final bool? renderOutside;
|
||||
|
||||
const FocusThemeData({
|
||||
this.borderRadius,
|
||||
this.primaryBorder,
|
||||
this.secondaryBorder,
|
||||
this.glowColor,
|
||||
this.glowFactor,
|
||||
this.renderOutside,
|
||||
}) : assert(glowFactor == null || glowFactor >= 0);
|
||||
|
||||
static FocusThemeData of(BuildContext context) {
|
||||
return FluentTheme.of(context).focusTheme;
|
||||
}
|
||||
|
||||
factory FocusThemeData.standard({
|
||||
required Color primaryBorderColor,
|
||||
required Color secondaryBorderColor,
|
||||
required Color glowColor,
|
||||
}) {
|
||||
return FocusThemeData(
|
||||
borderRadius: BorderRadius.zero,
|
||||
primaryBorder: BorderSide(width: 2, color: primaryBorderColor),
|
||||
secondaryBorder: BorderSide(width: 1, color: secondaryBorderColor),
|
||||
glowColor: glowColor,
|
||||
glowFactor: 0.0,
|
||||
renderOutside: true,
|
||||
);
|
||||
}
|
||||
|
||||
static FocusThemeData lerp(FocusThemeData? a, FocusThemeData? b, double t) {
|
||||
return FocusThemeData(
|
||||
borderRadius: BorderRadius.lerp(a?.borderRadius, b?.borderRadius, t),
|
||||
primaryBorder: BorderSide.lerp(a?.primaryBorder ?? BorderSide.none,
|
||||
b?.primaryBorder ?? BorderSide.none, t),
|
||||
secondaryBorder: BorderSide.lerp(a?.secondaryBorder ?? BorderSide.none,
|
||||
b?.secondaryBorder ?? BorderSide.none, t),
|
||||
glowColor: Color.lerp(a?.glowColor, b?.glowColor, t),
|
||||
glowFactor: lerpDouble(a?.glowFactor, b?.glowFactor, t),
|
||||
renderOutside: t < 0.5 ? a?.renderOutside : b?.renderOutside,
|
||||
);
|
||||
}
|
||||
|
||||
FocusThemeData merge(FocusThemeData? other) {
|
||||
if (other == null) return this;
|
||||
return FocusThemeData(
|
||||
primaryBorder: other.primaryBorder ?? primaryBorder,
|
||||
secondaryBorder: other.secondaryBorder ?? secondaryBorder,
|
||||
borderRadius: other.borderRadius ?? borderRadius,
|
||||
glowFactor: other.glowFactor ?? glowFactor,
|
||||
glowColor: other.glowColor ?? glowColor,
|
||||
renderOutside: other.renderOutside ?? renderOutside,
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration buildPrimaryDecoration(bool focused) {
|
||||
return BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
border: Border.fromBorderSide(
|
||||
!focused ? BorderSide.none : primaryBorder ?? BorderSide.none,
|
||||
),
|
||||
boxShadow: focused && glowFactor != 0 && glowColor != null
|
||||
? [
|
||||
BoxShadow(
|
||||
offset: const Offset(1, 1),
|
||||
color: glowColor!,
|
||||
spreadRadius: glowFactor!,
|
||||
blurRadius: glowFactor! * 2.5,
|
||||
),
|
||||
BoxShadow(
|
||||
offset: const Offset(-1, -1),
|
||||
color: glowColor!,
|
||||
spreadRadius: glowFactor!,
|
||||
blurRadius: glowFactor! * 2.5,
|
||||
),
|
||||
BoxShadow(
|
||||
offset: const Offset(-1, 1),
|
||||
color: glowColor!,
|
||||
spreadRadius: glowFactor!,
|
||||
blurRadius: glowFactor! * 2.5,
|
||||
),
|
||||
BoxShadow(
|
||||
offset: const Offset(1, -1),
|
||||
color: glowColor!,
|
||||
spreadRadius: glowFactor!,
|
||||
blurRadius: glowFactor! * 2.5,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
BoxDecoration buildSecondaryDecoration(bool focused) {
|
||||
return BoxDecoration(
|
||||
borderRadius: borderRadius,
|
||||
border: Border.fromBorderSide(
|
||||
!focused ? BorderSide.none : secondaryBorder ?? BorderSide.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<BorderSide>(
|
||||
'primaryBorder',
|
||||
primaryBorder,
|
||||
ifNull: 'No primary border',
|
||||
));
|
||||
properties.add(DiagnosticsProperty<BorderSide>(
|
||||
'secondaryBorder',
|
||||
secondaryBorder,
|
||||
ifNull: 'No secondary border',
|
||||
));
|
||||
properties.add(DiagnosticsProperty<BorderRadius>(
|
||||
'borderRadius',
|
||||
borderRadius,
|
||||
defaultValue: BorderRadius.zero,
|
||||
));
|
||||
properties.add(DoubleProperty('glowFactor', glowFactor, defaultValue: 0.0));
|
||||
properties.add(ColorProperty(
|
||||
'glowColor',
|
||||
glowColor,
|
||||
defaultValue: Colors.transparent,
|
||||
));
|
||||
properties.add(FlagProperty(
|
||||
'renderOutside',
|
||||
value: renderOutside,
|
||||
defaultValue: true,
|
||||
ifFalse: 'renderInside',
|
||||
));
|
||||
}
|
||||
}
|
||||
74
dependencies/fluent_ui-3.12.0/lib/src/styles/mica.dart
vendored
Normal file
74
dependencies/fluent_ui-3.12.0/lib/src/styles/mica.dart
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// Mica is an opaque, dynamic material that incorporates theme
|
||||
/// and desktop wallpaper to paint the background of long-lived
|
||||
/// windows such as apps and settings. You can apply Mica to your
|
||||
/// application backdrop to delight users and create visual hierarchy,
|
||||
/// aiding productivity, by increasing clarity about which window
|
||||
/// is in focus.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Acrylic], a type of Brush that creates a translucent texture
|
||||
/// * <https://docs.microsoft.com/en-us/windows/apps/design/style/mica>
|
||||
class Mica extends StatelessWidget {
|
||||
/// Creates the Mica material.
|
||||
///
|
||||
/// [elevation] must be non-negative.
|
||||
const Mica({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.elevation = 0,
|
||||
this.backgroundColor,
|
||||
this.borderRadius,
|
||||
this.shape = BoxShape.rectangle,
|
||||
}) : assert(elevation >= 0.0),
|
||||
super(key: key);
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget child;
|
||||
|
||||
/// The z-coordinate relative to the parent at which to place this physical
|
||||
/// object.
|
||||
///
|
||||
/// The value is non-negative.
|
||||
final double elevation;
|
||||
|
||||
/// The color to paint the background area with. If null,
|
||||
/// [ThemeData.micaBackgroundColor] is used.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The border radius applied to the area.
|
||||
final BorderRadius? borderRadius;
|
||||
|
||||
/// The box shape applied to the area.
|
||||
/// By default, the value of shape is [BoxShape.rectangle]
|
||||
final BoxShape shape;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasFluentTheme(context));
|
||||
final ThemeData theme = FluentTheme.of(context);
|
||||
final Color boxColor = backgroundColor ?? theme.micaBackgroundColor;
|
||||
final Widget result = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: boxColor,
|
||||
borderRadius: borderRadius,
|
||||
shape: shape,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
if (elevation > 0.0) {
|
||||
return PhysicalModel(
|
||||
color: boxColor,
|
||||
elevation: elevation,
|
||||
borderRadius: borderRadius,
|
||||
shape: shape,
|
||||
child: result,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
187
dependencies/fluent_ui-3.12.0/lib/src/styles/motion/page_transitions.dart
vendored
Normal file
187
dependencies/fluent_ui-3.12.0/lib/src/styles/motion/page_transitions.dart
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Entrance is a combination of a slide up animation and a fade in animation
|
||||
/// for the incoming content. Use page refresh when the user is taken to the top
|
||||
/// of a navigational stack, such as navigating between tabs or left-nav items.
|
||||
///
|
||||
/// This animation is used by default by [NavigationBody] if display mode is top
|
||||
class EntrancePageTransition extends StatelessWidget {
|
||||
/// Creates an entrance page transition
|
||||
const EntrancePageTransition({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.animation,
|
||||
this.vertical = true,
|
||||
this.reverse = false,
|
||||
this.startFrom = 0.25,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The widget to be animated
|
||||
final Widget child;
|
||||
|
||||
/// The animation to drive this transition
|
||||
final Animation<double> animation;
|
||||
|
||||
/// Whether the animation should be done vertically or horizontally
|
||||
final bool vertical;
|
||||
|
||||
/// Whether the animation should be done from the left or from the right
|
||||
final bool reverse;
|
||||
|
||||
/// From where the animation will begin. By default, 0.25 is used.
|
||||
///
|
||||
/// If [reverse] is true, `-startFrom` (negative) is used
|
||||
final double startFrom;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(FlagProperty(
|
||||
'vertical',
|
||||
value: vertical,
|
||||
ifFalse: 'horizontal',
|
||||
defaultValue: true,
|
||||
));
|
||||
properties.add(FlagProperty(
|
||||
vertical ? 'from top' : 'from left',
|
||||
value: reverse,
|
||||
ifTrue: vertical ? 'from bottom' : 'from right',
|
||||
defaultValue: false,
|
||||
));
|
||||
properties.add(PercentProperty(
|
||||
'animationValue',
|
||||
animation.value,
|
||||
ifNull: 'stopped',
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final value = animation.value + (reverse ? -startFrom : startFrom);
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: vertical ? Offset(0, value) : Offset(value, 0),
|
||||
end: Offset.zero,
|
||||
).animate(animation),
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use drill when users navigate deeper into an app, such as
|
||||
/// displaying more information after selecting an item.
|
||||
class DrillInPageTransition extends StatelessWidget {
|
||||
/// Creates a drill in page transition.
|
||||
const DrillInPageTransition({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.animation,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The widget to be animated
|
||||
final Widget child;
|
||||
|
||||
/// The animation to drive this transition
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(PercentProperty(
|
||||
'animationValue',
|
||||
animation.value,
|
||||
ifNull: 'stopped',
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(
|
||||
scale: Tween<double>(begin: 0.88, end: 1.0).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Use horizontal slide to show that sibling pages appear
|
||||
/// next to each other. [NavigationPanel] automatically uses
|
||||
/// this animation for top nav, but if you are building your
|
||||
/// own horizontal navigation experience, then you can implement
|
||||
/// horizontal slide with SlideNavigationTransitionInfo.
|
||||
class HorizontalSlidePageTransition extends StatelessWidget {
|
||||
/// Creates a horizontal slide page transition.
|
||||
const HorizontalSlidePageTransition({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.animation,
|
||||
this.fromLeft = true,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The widget to be animated
|
||||
final Widget child;
|
||||
|
||||
/// The animation to drive this transition
|
||||
final Animation<double> animation;
|
||||
|
||||
/// Whether this animation should be done from the left or not
|
||||
final bool fromLeft;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(PercentProperty(
|
||||
'animationValue',
|
||||
animation.value,
|
||||
ifNull: 'stopped',
|
||||
));
|
||||
properties.add(FlagProperty(
|
||||
'fromLeft',
|
||||
value: fromLeft,
|
||||
defaultValue: true,
|
||||
ifFalse: 'from right',
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final offsetTween = () {
|
||||
if (fromLeft) {
|
||||
return Tween<Offset>(
|
||||
begin: const Offset(-1, 0),
|
||||
end: Offset.zero,
|
||||
);
|
||||
} else {
|
||||
return Tween<Offset>(
|
||||
begin: const Offset(1, 0),
|
||||
end: Offset.zero,
|
||||
);
|
||||
}
|
||||
}();
|
||||
return SlideTransition(
|
||||
position: offsetTween.animate(animation),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// To avoid playing any animation during navigation, use this
|
||||
/// animation.
|
||||
class SuppressPageTransition extends StatelessWidget {
|
||||
const SuppressPageTransition({Key? key, required this.child})
|
||||
: super(key: key);
|
||||
|
||||
/// The widget to be animation
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
631
dependencies/fluent_ui-3.12.0/lib/src/styles/theme.dart
vendored
Normal file
631
dependencies/fluent_ui-3.12.0/lib/src/styles/theme.dart
vendored
Normal file
@@ -0,0 +1,631 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class FluentTheme extends StatelessWidget {
|
||||
/// Applies the given theme [data] to [child].
|
||||
///
|
||||
/// The [data] and [child] arguments must not be null.
|
||||
const FluentTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Specifies the color and typography values for descendant widgets.
|
||||
final ThemeData data;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget child;
|
||||
|
||||
static ThemeData of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<_FluentTheme>()!.data;
|
||||
}
|
||||
|
||||
static ThemeData? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<_FluentTheme>()?.data;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _FluentTheme(
|
||||
data: data,
|
||||
child: IconTheme(
|
||||
data: data.iconTheme,
|
||||
child: AnimatedDefaultTextStyle(
|
||||
style: data.typography.body!,
|
||||
duration: kThemeAnimationDuration,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FluentTheme extends InheritedTheme {
|
||||
const _FluentTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final ThemeData data;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(covariant _FluentTheme oldWidget) =>
|
||||
oldWidget.data != data;
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) {
|
||||
return _FluentTheme(data: data, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
/// An interpolation between two [ThemeData]s.
|
||||
///
|
||||
/// This class specializes the interpolation of [Tween<ThemeData>] to call the
|
||||
/// [ThemeData.lerp] method.
|
||||
///
|
||||
/// See [Tween] for a discussion on how to use interpolation objects.
|
||||
class ThemeDataTween extends Tween<ThemeData> {
|
||||
/// Creates a [ThemeData] tween.
|
||||
///
|
||||
/// The [begin] and [end] properties must be non-null before the tween is
|
||||
/// first used, but the arguments can be null if the values are going to be
|
||||
/// filled in later.
|
||||
ThemeDataTween({ThemeData? begin, ThemeData? end})
|
||||
: super(begin: begin, end: end);
|
||||
|
||||
@override
|
||||
ThemeData lerp(double t) => ThemeData.lerp(begin!, end!, t);
|
||||
}
|
||||
|
||||
/// Animated version of [Theme] which automatically transitions the colors,
|
||||
/// etc, over a given duration whenever the given theme changes.
|
||||
///
|
||||
/// Here's an illustration of what using this widget looks like, using a [curve]
|
||||
/// of [Curves.elasticInOut].
|
||||
/// {@animation 250 266 https://flutter.github.io/assets-for-api-docs/assets/widgets/animated_theme.mp4}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [FluentTheme], which [AnimatedFluentTheme] uses to actually apply the interpolated
|
||||
/// theme.
|
||||
/// * [ThemeData], which describes the actual configuration of a theme.
|
||||
/// * [FluentApp], which includes an [AnimatedFluentTheme] widget configured via
|
||||
/// the [FluentApp.theme] argument.
|
||||
class AnimatedFluentTheme extends ImplicitlyAnimatedWidget {
|
||||
/// Creates an animated theme.
|
||||
///
|
||||
/// By default, the theme transition uses a linear curve. The [data] and
|
||||
/// [child] arguments must not be null.
|
||||
const AnimatedFluentTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
Curve curve = Curves.linear,
|
||||
Duration duration = kThemeAnimationDuration,
|
||||
VoidCallback? onEnd,
|
||||
required this.child,
|
||||
}) : super(key: key, curve: curve, duration: duration, onEnd: onEnd);
|
||||
|
||||
/// Specifies the color and typography values for descendant widgets.
|
||||
final ThemeData data;
|
||||
|
||||
/// The widget below this widget in the tree.
|
||||
///
|
||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
_AnimatedFluentThemeState createState() => _AnimatedFluentThemeState();
|
||||
}
|
||||
|
||||
class _AnimatedFluentThemeState
|
||||
extends AnimatedWidgetBaseState<AnimatedFluentTheme> {
|
||||
ThemeDataTween? _data;
|
||||
|
||||
@override
|
||||
void forEachTween(TweenVisitor<dynamic> visitor) {
|
||||
_data = visitor(_data, widget.data,
|
||||
(dynamic value) => ThemeDataTween(begin: value as ThemeData))!
|
||||
as ThemeDataTween;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FluentTheme(
|
||||
data: _data!.evaluate(animation),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
description.add(DiagnosticsProperty<ThemeDataTween>('data', _data,
|
||||
showName: false, defaultValue: null));
|
||||
}
|
||||
}
|
||||
|
||||
extension BrightnessExtension on Brightness {
|
||||
bool get isLight => this == Brightness.light;
|
||||
bool get isDark => this == Brightness.dark;
|
||||
|
||||
Brightness get opposite => isLight ? Brightness.dark : Brightness.light;
|
||||
}
|
||||
|
||||
const standardCurve = Curves.easeInOut;
|
||||
|
||||
/// Defines the default theme for a [FluentApp] or [FluentTheme].
|
||||
@immutable
|
||||
class ThemeData with Diagnosticable {
|
||||
final Typography typography;
|
||||
|
||||
final AccentColor accentColor;
|
||||
final Color activeColor;
|
||||
final Color inactiveColor;
|
||||
final Color inactiveBackgroundColor;
|
||||
final Color disabledColor;
|
||||
final Color shadowColor;
|
||||
final Color uncheckedColor;
|
||||
final Color checkedColor;
|
||||
final Color borderInputColor;
|
||||
final Color scaffoldBackgroundColor;
|
||||
final Color acrylicBackgroundColor;
|
||||
final Color micaBackgroundColor;
|
||||
final Color menuColor;
|
||||
final Color cardColor;
|
||||
|
||||
final Duration fasterAnimationDuration;
|
||||
final Duration fastAnimationDuration;
|
||||
final Duration mediumAnimationDuration;
|
||||
final Duration slowAnimationDuration;
|
||||
final Curve animationCurve;
|
||||
|
||||
final Brightness brightness;
|
||||
final VisualDensity visualDensity;
|
||||
|
||||
final NavigationPaneThemeData navigationPaneTheme;
|
||||
final BottomNavigationThemeData bottomNavigationTheme;
|
||||
final BottomSheetThemeData bottomSheetTheme;
|
||||
final CheckboxThemeData checkboxTheme;
|
||||
final ChipThemeData chipTheme;
|
||||
final ContentDialogThemeData dialogTheme;
|
||||
final DividerThemeData dividerTheme;
|
||||
final FocusThemeData focusTheme;
|
||||
final IconThemeData iconTheme;
|
||||
final InfoBarThemeData infoBarTheme;
|
||||
final PillButtonBarThemeData pillButtonBarTheme;
|
||||
final RadioButtonThemeData radioButtonTheme;
|
||||
final ScrollbarThemeData scrollbarTheme;
|
||||
final SliderThemeData sliderTheme;
|
||||
final SplitButtonThemeData splitButtonTheme;
|
||||
final SnackbarThemeData snackbarTheme;
|
||||
final ToggleButtonThemeData toggleButtonTheme;
|
||||
final ToggleSwitchThemeData toggleSwitchTheme;
|
||||
final TooltipThemeData tooltipTheme;
|
||||
|
||||
final ButtonThemeData buttonTheme;
|
||||
|
||||
const ThemeData.raw({
|
||||
required this.typography,
|
||||
required this.accentColor,
|
||||
required this.activeColor,
|
||||
required this.inactiveColor,
|
||||
required this.inactiveBackgroundColor,
|
||||
required this.disabledColor,
|
||||
required this.shadowColor,
|
||||
required this.uncheckedColor,
|
||||
required this.checkedColor,
|
||||
required this.borderInputColor,
|
||||
required this.fasterAnimationDuration,
|
||||
required this.fastAnimationDuration,
|
||||
required this.mediumAnimationDuration,
|
||||
required this.slowAnimationDuration,
|
||||
required this.animationCurve,
|
||||
required this.brightness,
|
||||
required this.visualDensity,
|
||||
required this.scaffoldBackgroundColor,
|
||||
required this.acrylicBackgroundColor,
|
||||
required this.micaBackgroundColor,
|
||||
required this.buttonTheme,
|
||||
required this.checkboxTheme,
|
||||
required this.chipTheme,
|
||||
required this.toggleSwitchTheme,
|
||||
required this.bottomNavigationTheme,
|
||||
required this.iconTheme,
|
||||
required this.splitButtonTheme,
|
||||
required this.dialogTheme,
|
||||
required this.tooltipTheme,
|
||||
required this.dividerTheme,
|
||||
required this.navigationPaneTheme,
|
||||
required this.radioButtonTheme,
|
||||
required this.toggleButtonTheme,
|
||||
required this.sliderTheme,
|
||||
required this.infoBarTheme,
|
||||
required this.focusTheme,
|
||||
required this.scrollbarTheme,
|
||||
required this.snackbarTheme,
|
||||
required this.pillButtonBarTheme,
|
||||
required this.bottomSheetTheme,
|
||||
required this.menuColor,
|
||||
required this.cardColor,
|
||||
});
|
||||
|
||||
static ThemeData light() {
|
||||
return ThemeData(brightness: Brightness.light);
|
||||
}
|
||||
|
||||
static ThemeData dark() {
|
||||
return ThemeData(brightness: Brightness.dark);
|
||||
}
|
||||
|
||||
factory ThemeData({
|
||||
Brightness? brightness,
|
||||
VisualDensity? visualDensity,
|
||||
Typography? typography,
|
||||
String? fontFamily,
|
||||
AccentColor? accentColor,
|
||||
Color? activeColor,
|
||||
Color? inactiveColor,
|
||||
Color? inactiveBackgroundColor,
|
||||
Color? disabledColor,
|
||||
Color? scaffoldBackgroundColor,
|
||||
Color? acrylicBackgroundColor,
|
||||
Color? micaBackgroundColor,
|
||||
Color? shadowColor,
|
||||
Color? uncheckedColor,
|
||||
Color? checkedColor,
|
||||
Color? borderInputColor,
|
||||
Color? menuColor,
|
||||
Color? cardColor,
|
||||
Duration? fasterAnimationDuration,
|
||||
Duration? fastAnimationDuration,
|
||||
Duration? mediumAnimationDuration,
|
||||
Duration? slowAnimationDuration,
|
||||
Curve? animationCurve,
|
||||
BottomNavigationThemeData? bottomNavigationTheme,
|
||||
BottomSheetThemeData? bottomSheetTheme,
|
||||
ButtonThemeData? buttonTheme,
|
||||
CheckboxThemeData? checkboxTheme,
|
||||
ChipThemeData? chipTheme,
|
||||
ToggleSwitchThemeData? toggleSwitchTheme,
|
||||
IconThemeData? iconTheme,
|
||||
SplitButtonThemeData? splitButtonTheme,
|
||||
ContentDialogThemeData? dialogTheme,
|
||||
TooltipThemeData? tooltipTheme,
|
||||
DividerThemeData? dividerTheme,
|
||||
NavigationPaneThemeData? navigationPaneTheme,
|
||||
RadioButtonThemeData? radioButtonTheme,
|
||||
ToggleButtonThemeData? toggleButtonTheme,
|
||||
SliderThemeData? sliderTheme,
|
||||
InfoBarThemeData? infoBarTheme,
|
||||
PillButtonBarThemeData? pillButtonBarTheme,
|
||||
FocusThemeData? focusTheme,
|
||||
ScrollbarThemeData? scrollbarTheme,
|
||||
SnackbarThemeData? snackbarTheme,
|
||||
}) {
|
||||
brightness ??= Brightness.light;
|
||||
|
||||
final bool isLight = brightness == Brightness.light;
|
||||
|
||||
visualDensity ??= VisualDensity.adaptivePlatformDensity;
|
||||
fasterAnimationDuration ??= const Duration(milliseconds: 83);
|
||||
fastAnimationDuration ??= const Duration(milliseconds: 167);
|
||||
mediumAnimationDuration ??= const Duration(milliseconds: 250);
|
||||
slowAnimationDuration ??= const Duration(milliseconds: 358);
|
||||
animationCurve ??= standardCurve;
|
||||
accentColor ??= Colors.blue;
|
||||
activeColor ??= Colors.white;
|
||||
inactiveColor ??= isLight ? Colors.black : Colors.white;
|
||||
inactiveBackgroundColor ??=
|
||||
isLight ? const Color(0xFFd6d6d6) : const Color(0xFF292929);
|
||||
disabledColor ??=
|
||||
isLight ? const Color(0xFF838383) : Colors.grey[80].withOpacity(0.6);
|
||||
shadowColor ??= isLight ? Colors.black : Colors.grey[130];
|
||||
scaffoldBackgroundColor ??=
|
||||
isLight ? Colors.white : Colors.white.withOpacity(0.025);
|
||||
acrylicBackgroundColor ??= isLight
|
||||
? const Color.fromARGB(204, 255, 255, 255)
|
||||
: const Color(0x7F1e1e1e);
|
||||
micaBackgroundColor ??=
|
||||
isLight ? const Color(0xFFf3f3f3) : const Color(0xFF202020);
|
||||
uncheckedColor ??= isLight
|
||||
? const Color.fromRGBO(0, 0, 0, 0.6063)
|
||||
: const Color.fromRGBO(255, 255, 255, 0.786);
|
||||
checkedColor ??= isLight ? Colors.white : Colors.black;
|
||||
borderInputColor ??= isLight
|
||||
? const Color.fromRGBO(0, 0, 0, 0.4458)
|
||||
: const Color.fromRGBO(255, 255, 255, 0.5442);
|
||||
menuColor ??= isLight ? const Color(0xFFf9f9f9) : const Color(0xFF2c2c2c);
|
||||
cardColor ??= isLight ? const Color(0xFFf3f3f3) : const Color(0xFF2e2e2e);
|
||||
typography = Typography.fromBrightness(brightness: brightness)
|
||||
.merge(typography)
|
||||
.apply(fontFamily: fontFamily);
|
||||
focusTheme = FocusThemeData.standard(
|
||||
glowColor: accentColor.withOpacity(0.15),
|
||||
primaryBorderColor: inactiveColor,
|
||||
secondaryBorderColor: scaffoldBackgroundColor,
|
||||
).merge(focusTheme);
|
||||
buttonTheme ??= const ButtonThemeData();
|
||||
checkboxTheme ??= const CheckboxThemeData();
|
||||
chipTheme ??= const ChipThemeData();
|
||||
toggleButtonTheme ??= const ToggleButtonThemeData();
|
||||
toggleSwitchTheme ??= const ToggleSwitchThemeData();
|
||||
iconTheme ??= isLight
|
||||
? const IconThemeData(color: Colors.black, size: 18.0)
|
||||
: const IconThemeData(color: Colors.white, size: 18.0);
|
||||
splitButtonTheme ??= const SplitButtonThemeData();
|
||||
dialogTheme ??= const ContentDialogThemeData();
|
||||
tooltipTheme ??= const TooltipThemeData();
|
||||
dividerTheme ??= const DividerThemeData();
|
||||
navigationPaneTheme ??= NavigationPaneThemeData.standard(
|
||||
animationCurve: animationCurve,
|
||||
animationDuration: fastAnimationDuration,
|
||||
backgroundColor: micaBackgroundColor,
|
||||
disabledColor: disabledColor,
|
||||
highlightColor: accentColor.resolveFromReverseBrightness(
|
||||
brightness,
|
||||
level: brightness.isDark ? 2 : 0,
|
||||
),
|
||||
typography: typography,
|
||||
inactiveColor: inactiveColor,
|
||||
);
|
||||
radioButtonTheme ??= const RadioButtonThemeData();
|
||||
sliderTheme ??= const SliderThemeData();
|
||||
infoBarTheme ??= const InfoBarThemeData();
|
||||
pillButtonBarTheme ??= const PillButtonBarThemeData();
|
||||
scrollbarTheme ??= const ScrollbarThemeData();
|
||||
bottomNavigationTheme ??= const BottomNavigationThemeData();
|
||||
snackbarTheme ??= const SnackbarThemeData();
|
||||
bottomSheetTheme ??= const BottomSheetThemeData();
|
||||
return ThemeData.raw(
|
||||
brightness: brightness,
|
||||
visualDensity: visualDensity,
|
||||
fasterAnimationDuration: fasterAnimationDuration,
|
||||
fastAnimationDuration: fastAnimationDuration,
|
||||
mediumAnimationDuration: mediumAnimationDuration,
|
||||
slowAnimationDuration: slowAnimationDuration,
|
||||
animationCurve: animationCurve,
|
||||
accentColor: accentColor,
|
||||
activeColor: activeColor,
|
||||
inactiveColor: inactiveColor,
|
||||
inactiveBackgroundColor: inactiveBackgroundColor,
|
||||
disabledColor: disabledColor,
|
||||
scaffoldBackgroundColor: scaffoldBackgroundColor,
|
||||
acrylicBackgroundColor: acrylicBackgroundColor,
|
||||
micaBackgroundColor: micaBackgroundColor,
|
||||
shadowColor: shadowColor,
|
||||
uncheckedColor: uncheckedColor,
|
||||
checkedColor: checkedColor,
|
||||
borderInputColor: borderInputColor,
|
||||
bottomNavigationTheme: bottomNavigationTheme,
|
||||
buttonTheme: buttonTheme,
|
||||
checkboxTheme: checkboxTheme,
|
||||
chipTheme: chipTheme,
|
||||
dialogTheme: dialogTheme,
|
||||
dividerTheme: dividerTheme,
|
||||
focusTheme: focusTheme,
|
||||
iconTheme: iconTheme,
|
||||
infoBarTheme: infoBarTheme,
|
||||
navigationPaneTheme: navigationPaneTheme,
|
||||
radioButtonTheme: radioButtonTheme,
|
||||
scrollbarTheme: scrollbarTheme,
|
||||
sliderTheme: sliderTheme,
|
||||
splitButtonTheme: splitButtonTheme,
|
||||
toggleButtonTheme: toggleButtonTheme,
|
||||
toggleSwitchTheme: toggleSwitchTheme,
|
||||
tooltipTheme: tooltipTheme,
|
||||
typography: typography,
|
||||
snackbarTheme: snackbarTheme,
|
||||
pillButtonBarTheme: pillButtonBarTheme,
|
||||
bottomSheetTheme: bottomSheetTheme,
|
||||
menuColor: menuColor,
|
||||
cardColor: cardColor,
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData lerp(ThemeData a, ThemeData b, double t) {
|
||||
return ThemeData.raw(
|
||||
brightness: t < 0.5 ? a.brightness : b.brightness,
|
||||
visualDensity: t < 0.5 ? a.visualDensity : b.visualDensity,
|
||||
accentColor: AccentColor.lerp(a.accentColor, b.accentColor, t),
|
||||
typography: Typography.lerp(a.typography, b.typography, t),
|
||||
activeColor: Color.lerp(a.activeColor, b.activeColor, t)!,
|
||||
inactiveColor: Color.lerp(a.inactiveColor, b.inactiveColor, t)!,
|
||||
inactiveBackgroundColor:
|
||||
Color.lerp(a.inactiveBackgroundColor, b.inactiveBackgroundColor, t)!,
|
||||
disabledColor: Color.lerp(a.disabledColor, b.disabledColor, t)!,
|
||||
scaffoldBackgroundColor:
|
||||
Color.lerp(a.scaffoldBackgroundColor, b.scaffoldBackgroundColor, t)!,
|
||||
acrylicBackgroundColor:
|
||||
Color.lerp(a.acrylicBackgroundColor, b.acrylicBackgroundColor, t)!,
|
||||
micaBackgroundColor:
|
||||
Color.lerp(a.micaBackgroundColor, b.micaBackgroundColor, t)!,
|
||||
shadowColor: Color.lerp(a.shadowColor, b.shadowColor, t)!,
|
||||
uncheckedColor: Color.lerp(a.uncheckedColor, b.uncheckedColor, t)!,
|
||||
checkedColor: Color.lerp(a.checkedColor, b.checkedColor, t)!,
|
||||
borderInputColor: Color.lerp(a.borderInputColor, b.borderInputColor, t)!,
|
||||
cardColor: Color.lerp(a.cardColor, b.cardColor, t)!,
|
||||
fasterAnimationDuration:
|
||||
lerpDuration(a.fasterAnimationDuration, b.fasterAnimationDuration, t),
|
||||
fastAnimationDuration:
|
||||
lerpDuration(a.fastAnimationDuration, b.fastAnimationDuration, t),
|
||||
mediumAnimationDuration:
|
||||
lerpDuration(a.mediumAnimationDuration, b.mediumAnimationDuration, t),
|
||||
slowAnimationDuration:
|
||||
lerpDuration(a.slowAnimationDuration, b.slowAnimationDuration, t),
|
||||
animationCurve: t < 0.5 ? a.animationCurve : b.animationCurve,
|
||||
buttonTheme: ButtonThemeData.lerp(a.buttonTheme, b.buttonTheme, t),
|
||||
checkboxTheme:
|
||||
CheckboxThemeData.lerp(a.checkboxTheme, b.checkboxTheme, t),
|
||||
chipTheme: ChipThemeData.lerp(a.chipTheme, b.chipTheme, t),
|
||||
toggleSwitchTheme: ToggleSwitchThemeData.lerp(
|
||||
a.toggleSwitchTheme, b.toggleSwitchTheme, t),
|
||||
iconTheme: IconThemeData.lerp(a.iconTheme, b.iconTheme, t),
|
||||
splitButtonTheme:
|
||||
SplitButtonThemeData.lerp(a.splitButtonTheme, b.splitButtonTheme, t),
|
||||
dialogTheme: ContentDialogThemeData.lerp(a.dialogTheme, b.dialogTheme, t),
|
||||
tooltipTheme: TooltipThemeData.lerp(a.tooltipTheme, b.tooltipTheme, t),
|
||||
dividerTheme: DividerThemeData.lerp(a.dividerTheme, b.dividerTheme, t),
|
||||
navigationPaneTheme: NavigationPaneThemeData.lerp(
|
||||
a.navigationPaneTheme, b.navigationPaneTheme, t),
|
||||
radioButtonTheme:
|
||||
RadioButtonThemeData.lerp(a.radioButtonTheme, b.radioButtonTheme, t),
|
||||
toggleButtonTheme: ToggleButtonThemeData.lerp(
|
||||
a.toggleButtonTheme, b.toggleButtonTheme, t),
|
||||
sliderTheme: SliderThemeData.lerp(a.sliderTheme, b.sliderTheme, t),
|
||||
infoBarTheme: InfoBarThemeData.lerp(a.infoBarTheme, b.infoBarTheme, t),
|
||||
focusTheme: FocusThemeData.lerp(a.focusTheme, b.focusTheme, t),
|
||||
scrollbarTheme:
|
||||
ScrollbarThemeData.lerp(a.scrollbarTheme, b.scrollbarTheme, t),
|
||||
bottomNavigationTheme: BottomNavigationThemeData.lerp(
|
||||
a.bottomNavigationTheme, b.bottomNavigationTheme, t),
|
||||
snackbarTheme:
|
||||
SnackbarThemeData.lerp(a.snackbarTheme, b.snackbarTheme, t),
|
||||
pillButtonBarTheme: PillButtonBarThemeData.lerp(
|
||||
a.pillButtonBarTheme, b.pillButtonBarTheme, t),
|
||||
bottomSheetTheme:
|
||||
BottomSheetThemeData.lerp(a.bottomSheetTheme, b.bottomSheetTheme, t),
|
||||
menuColor: Color.lerp(a.menuColor, b.menuColor, t)!,
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData copyWith({
|
||||
Brightness? brightness,
|
||||
VisualDensity? visualDensity,
|
||||
Typography? typography,
|
||||
AccentColor? accentColor,
|
||||
Color? activeColor,
|
||||
Color? inactiveColor,
|
||||
Color? inactiveBackgroundColor,
|
||||
Color? disabledColor,
|
||||
Color? scaffoldBackgroundColor,
|
||||
Color? acrylicBackgroundColor,
|
||||
Color? micaBackgroundColor,
|
||||
Color? shadowColor,
|
||||
Color? uncheckedColor,
|
||||
Color? checkedColor,
|
||||
Color? borderInputColor,
|
||||
Color? menuColor,
|
||||
Color? cardColor,
|
||||
Duration? fasterAnimationDuration,
|
||||
Duration? fastAnimationDuration,
|
||||
Duration? mediumAnimationDuration,
|
||||
Duration? slowAnimationDuration,
|
||||
Curve? animationCurve,
|
||||
ButtonThemeData? buttonTheme,
|
||||
BottomNavigationThemeData? bottomNavigationTheme,
|
||||
BottomSheetThemeData? bottomSheetTheme,
|
||||
CheckboxThemeData? checkboxTheme,
|
||||
ChipThemeData? chipTheme,
|
||||
ToggleSwitchThemeData? toggleSwitchTheme,
|
||||
IconThemeData? iconTheme,
|
||||
SplitButtonThemeData? splitButtonTheme,
|
||||
ContentDialogThemeData? dialogTheme,
|
||||
TooltipThemeData? tooltipTheme,
|
||||
DividerThemeData? dividerTheme,
|
||||
NavigationPaneThemeData? navigationPaneTheme,
|
||||
RadioButtonThemeData? radioButtonTheme,
|
||||
ToggleButtonThemeData? toggleButtonTheme,
|
||||
SliderThemeData? sliderTheme,
|
||||
InfoBarThemeData? infoBarTheme,
|
||||
PillButtonBarThemeData? pillButtonBarTheme,
|
||||
FocusThemeData? focusTheme,
|
||||
ScrollbarThemeData? scrollbarTheme,
|
||||
SnackbarThemeData? snackbarTheme,
|
||||
}) {
|
||||
return ThemeData.raw(
|
||||
brightness: brightness ?? this.brightness,
|
||||
visualDensity: visualDensity ?? this.visualDensity,
|
||||
typography: typography ?? this.typography,
|
||||
accentColor: accentColor ?? this.accentColor,
|
||||
activeColor: activeColor ?? this.activeColor,
|
||||
inactiveColor: inactiveColor ?? this.inactiveColor,
|
||||
shadowColor: shadowColor ?? this.shadowColor,
|
||||
uncheckedColor: uncheckedColor ?? this.uncheckedColor,
|
||||
checkedColor: checkedColor ?? this.checkedColor,
|
||||
borderInputColor: borderInputColor ?? this.borderInputColor,
|
||||
inactiveBackgroundColor:
|
||||
inactiveBackgroundColor ?? this.inactiveBackgroundColor,
|
||||
disabledColor: disabledColor ?? this.disabledColor,
|
||||
scaffoldBackgroundColor:
|
||||
scaffoldBackgroundColor ?? this.scaffoldBackgroundColor,
|
||||
acrylicBackgroundColor:
|
||||
acrylicBackgroundColor ?? this.acrylicBackgroundColor,
|
||||
micaBackgroundColor: micaBackgroundColor ?? this.micaBackgroundColor,
|
||||
menuColor: menuColor ?? this.menuColor,
|
||||
cardColor: cardColor ?? this.cardColor,
|
||||
fasterAnimationDuration:
|
||||
fasterAnimationDuration ?? this.fasterAnimationDuration,
|
||||
fastAnimationDuration:
|
||||
fastAnimationDuration ?? this.fastAnimationDuration,
|
||||
mediumAnimationDuration:
|
||||
mediumAnimationDuration ?? this.mediumAnimationDuration,
|
||||
slowAnimationDuration:
|
||||
slowAnimationDuration ?? this.slowAnimationDuration,
|
||||
animationCurve: animationCurve ?? this.animationCurve,
|
||||
buttonTheme: this.buttonTheme.merge(buttonTheme),
|
||||
bottomNavigationTheme:
|
||||
this.bottomNavigationTheme.merge(bottomNavigationTheme),
|
||||
bottomSheetTheme: this.bottomSheetTheme.merge(bottomSheetTheme),
|
||||
checkboxTheme: this.checkboxTheme.merge(checkboxTheme),
|
||||
chipTheme: this.chipTheme.merge(chipTheme),
|
||||
dialogTheme: this.dialogTheme.merge(dialogTheme),
|
||||
dividerTheme: this.dividerTheme.merge(dividerTheme),
|
||||
focusTheme: this.focusTheme.merge(focusTheme),
|
||||
iconTheme: this.iconTheme.merge(iconTheme),
|
||||
infoBarTheme: this.infoBarTheme.merge(infoBarTheme),
|
||||
pillButtonBarTheme: this.pillButtonBarTheme.merge(pillButtonBarTheme),
|
||||
navigationPaneTheme: this.navigationPaneTheme.merge(navigationPaneTheme),
|
||||
radioButtonTheme: this.radioButtonTheme.merge(radioButtonTheme),
|
||||
scrollbarTheme: this.scrollbarTheme.merge(scrollbarTheme),
|
||||
sliderTheme: this.sliderTheme.merge(sliderTheme),
|
||||
splitButtonTheme: this.splitButtonTheme.merge(splitButtonTheme),
|
||||
toggleButtonTheme: this.toggleButtonTheme.merge(toggleButtonTheme),
|
||||
toggleSwitchTheme: this.toggleSwitchTheme.merge(toggleSwitchTheme),
|
||||
tooltipTheme: this.tooltipTheme.merge(tooltipTheme),
|
||||
snackbarTheme: this.snackbarTheme.merge(snackbarTheme),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(ColorProperty('accentColor', accentColor))
|
||||
..add(ColorProperty('activeColor', activeColor))
|
||||
..add(ColorProperty('inactiveColor', inactiveColor))
|
||||
..add(ColorProperty('inactiveBackgroundColor', inactiveBackgroundColor))
|
||||
..add(ColorProperty('disabledColor', disabledColor))
|
||||
..add(ColorProperty('shadowColor', shadowColor))
|
||||
..add(ColorProperty('scaffoldBackgroundColor', scaffoldBackgroundColor))
|
||||
..add(ColorProperty('acrylicBackgroundColor', acrylicBackgroundColor))
|
||||
..add(ColorProperty('micaBackgroundColor', micaBackgroundColor))
|
||||
..add(ColorProperty('menuColor', menuColor))
|
||||
..add(ColorProperty('cardColor', cardColor));
|
||||
properties.add(EnumProperty('brightness', brightness));
|
||||
properties.add(DiagnosticsProperty<Duration>(
|
||||
'slowAnimationDuration',
|
||||
slowAnimationDuration,
|
||||
));
|
||||
properties.add(DiagnosticsProperty<Duration>(
|
||||
'mediumAnimationDuration',
|
||||
mediumAnimationDuration,
|
||||
));
|
||||
properties.add(DiagnosticsProperty<Duration>(
|
||||
'fastAnimationDuration',
|
||||
fastAnimationDuration,
|
||||
));
|
||||
properties.add(DiagnosticsProperty<Duration>(
|
||||
'fasterAnimationDuration',
|
||||
fasterAnimationDuration,
|
||||
));
|
||||
properties.add(
|
||||
DiagnosticsProperty<Curve>('animationCurve', animationCurve),
|
||||
);
|
||||
}
|
||||
}
|
||||
241
dependencies/fluent_ui-3.12.0/lib/src/styles/typography.dart
vendored
Normal file
241
dependencies/fluent_ui-3.12.0/lib/src/styles/typography.dart
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// The typography applied to a [ThemeData]. It implements Window's [Type Ramp](https://docs.microsoft.com/en-us/windows/uwp/design/style/typography#type-ramp)
|
||||
///
|
||||
/// | Do | Don't |
|
||||
/// | :-------------------------------------------------- | :-------------------------------------------------------------------------------- |
|
||||
/// | Pick one font for your UI. | Don't mix multiple fonts. |
|
||||
/// | Use [body] for most text | Use "Caption" for primary action or any long strings. |
|
||||
/// | Use "Base" for titles when space is constrained. | Use "Header" or "Subheader" if text needs to wrap. |
|
||||
/// | Keep to 50–60 letters per line for ease of reading. | Less than 20 characters or more than 60 characters per line is difficult to read. |
|
||||
/// | Clip text, and wrap if multiple lines are enabled. | Use ellipses to avoid visual clutter. |
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// For more info, read [Typography](https://docs.microsoft.com/en-us/windows/uwp/design/style/typography)
|
||||
class Typography with Diagnosticable {
|
||||
/// The header style. Use this as the top of the hierarchy
|
||||
///
|
||||
/// Don't use [header] if the text needs to wrap.
|
||||
final TextStyle? display;
|
||||
|
||||
final TextStyle? titleLarge;
|
||||
|
||||
/// The title style.
|
||||
final TextStyle? title;
|
||||
|
||||
/// The subtitle style.
|
||||
final TextStyle? subtitle;
|
||||
|
||||
final TextStyle? bodyLarge;
|
||||
|
||||
/// The base style. Use [base] for titles when space is constrained.
|
||||
final TextStyle? bodyStrong;
|
||||
|
||||
/// The body style. Use [body] for most of the text.
|
||||
final TextStyle? body;
|
||||
|
||||
/// The caption style.
|
||||
///
|
||||
/// Don't use [caption] for primary action or any long strings.
|
||||
final TextStyle? caption;
|
||||
|
||||
/// Creates a new [Typography]. To create the default typography, use [Typography.defaultTypography]
|
||||
const Typography.raw({
|
||||
this.display,
|
||||
this.titleLarge,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.bodyLarge,
|
||||
this.bodyStrong,
|
||||
this.body,
|
||||
this.caption,
|
||||
});
|
||||
|
||||
/// The default typography according to a brightness or color.
|
||||
///
|
||||
/// If [color] is null, [Colors.black] is used if [brightness] is light,
|
||||
/// otherwise [Colors.white] is used. If it's not null, [color] will be used.
|
||||
factory Typography.fromBrightness({
|
||||
Brightness? brightness,
|
||||
Color? color,
|
||||
}) {
|
||||
assert(
|
||||
brightness != null || color != null,
|
||||
'Either brightness or color must be provided',
|
||||
);
|
||||
// If color is null, brightness will not be null
|
||||
color ??=
|
||||
brightness == Brightness.light ? const Color(0xE4000000) : Colors.white;
|
||||
return Typography.raw(
|
||||
display: TextStyle(
|
||||
fontSize: 68,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
fontSize: 40,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
title: TextStyle(
|
||||
fontSize: 28,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
subtitle: TextStyle(
|
||||
fontSize: 20,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
fontSize: 18,
|
||||
color: color,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
bodyStrong: TextStyle(
|
||||
fontSize: 14,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
body: TextStyle(
|
||||
fontSize: 14,
|
||||
color: color,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
caption: TextStyle(
|
||||
fontSize: 12,
|
||||
color: color,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Typography lerp(Typography? a, Typography? b, double t) {
|
||||
return Typography.raw(
|
||||
display: TextStyle.lerp(a?.display, b?.display, t),
|
||||
titleLarge: TextStyle.lerp(a?.titleLarge, b?.titleLarge, t),
|
||||
title: TextStyle.lerp(a?.title, b?.title, t),
|
||||
subtitle: TextStyle.lerp(a?.subtitle, b?.subtitle, t),
|
||||
bodyLarge: TextStyle.lerp(a?.bodyLarge, b?.bodyLarge, t),
|
||||
bodyStrong: TextStyle.lerp(a?.bodyStrong, b?.bodyStrong, t),
|
||||
body: TextStyle.lerp(a?.body, b?.body, t),
|
||||
caption: TextStyle.lerp(a?.caption, b?.caption, t),
|
||||
);
|
||||
}
|
||||
|
||||
/// Copy this with a new [typography]
|
||||
Typography merge(Typography? typography) {
|
||||
if (typography == null) return this;
|
||||
return Typography.raw(
|
||||
display: typography.display ?? display,
|
||||
titleLarge: typography.titleLarge ?? titleLarge,
|
||||
title: typography.title ?? title,
|
||||
subtitle: typography.subtitle ?? subtitle,
|
||||
bodyLarge: typography.bodyLarge ?? bodyLarge,
|
||||
bodyStrong: typography.bodyStrong ?? bodyStrong,
|
||||
body: typography.body ?? body,
|
||||
caption: typography.caption ?? caption,
|
||||
);
|
||||
}
|
||||
|
||||
Typography apply({
|
||||
String? fontFamily,
|
||||
double fontSizeFactor = 1.0,
|
||||
double fontSizeDelta = 0.0,
|
||||
Color? displayColor,
|
||||
TextDecoration? decoration,
|
||||
Color? decorationColor,
|
||||
TextDecorationStyle? decorationStyle,
|
||||
}) {
|
||||
return Typography.raw(
|
||||
display: display?.apply(
|
||||
color: displayColor,
|
||||
decoration: decoration,
|
||||
decorationColor: decorationColor,
|
||||
decorationStyle: decorationStyle,
|
||||
fontFamily: fontFamily,
|
||||
fontSizeFactor: fontSizeFactor,
|
||||
fontSizeDelta: fontSizeDelta,
|
||||
),
|
||||
titleLarge: titleLarge?.apply(
|
||||
color: displayColor,
|
||||
decoration: decoration,
|
||||
decorationColor: decorationColor,
|
||||
decorationStyle: decorationStyle,
|
||||
fontFamily: fontFamily,
|
||||
fontSizeFactor: fontSizeFactor,
|
||||
fontSizeDelta: fontSizeDelta,
|
||||
),
|
||||
title: title?.apply(
|
||||
color: displayColor,
|
||||
decoration: decoration,
|
||||
decorationColor: decorationColor,
|
||||
decorationStyle: decorationStyle,
|
||||
fontFamily: fontFamily,
|
||||
fontSizeFactor: fontSizeFactor,
|
||||
fontSizeDelta: fontSizeDelta,
|
||||
),
|
||||
subtitle: subtitle?.apply(
|
||||
color: displayColor,
|
||||
decoration: decoration,
|
||||
decorationColor: decorationColor,
|
||||
decorationStyle: decorationStyle,
|
||||
fontFamily: fontFamily,
|
||||
fontSizeFactor: fontSizeFactor,
|
||||
fontSizeDelta: fontSizeDelta,
|
||||
),
|
||||
bodyLarge: bodyLarge?.apply(
|
||||
color: displayColor,
|
||||
decoration: decoration,
|
||||
decorationColor: decorationColor,
|
||||
decorationStyle: decorationStyle,
|
||||
fontFamily: fontFamily,
|
||||
fontSizeFactor: fontSizeFactor,
|
||||
fontSizeDelta: fontSizeDelta,
|
||||
),
|
||||
bodyStrong: bodyStrong?.apply(
|
||||
color: displayColor,
|
||||
decoration: decoration,
|
||||
decorationColor: decorationColor,
|
||||
decorationStyle: decorationStyle,
|
||||
fontFamily: fontFamily,
|
||||
fontSizeFactor: fontSizeFactor,
|
||||
fontSizeDelta: fontSizeDelta,
|
||||
),
|
||||
body: body?.apply(
|
||||
color: displayColor,
|
||||
decoration: decoration,
|
||||
decorationColor: decorationColor,
|
||||
decorationStyle: decorationStyle,
|
||||
fontFamily: fontFamily,
|
||||
fontSizeFactor: fontSizeFactor,
|
||||
fontSizeDelta: fontSizeDelta,
|
||||
),
|
||||
caption: caption?.apply(
|
||||
color: displayColor,
|
||||
decoration: decoration,
|
||||
decorationColor: decorationColor,
|
||||
decorationStyle: decorationStyle,
|
||||
fontFamily: fontFamily,
|
||||
fontSizeFactor: fontSizeFactor,
|
||||
fontSizeDelta: fontSizeDelta,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<TextStyle>('header', display));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('titleLarge', titleLarge));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('title', title));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('subtitle', subtitle));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('bodyLarge', bodyLarge));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('bodyStrong', bodyStrong));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('body', body));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('caption', caption));
|
||||
}
|
||||
}
|
||||
123
dependencies/fluent_ui-3.12.0/lib/src/utils.dart
vendored
Normal file
123
dependencies/fluent_ui-3.12.0/lib/src/utils.dart
vendored
Normal file
@@ -0,0 +1,123 @@
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
/// Asserts that the given context has a [FluentTheme] ancestor.
|
||||
///
|
||||
/// To call this function, use the following pattern, typically in the
|
||||
/// relevant Widget's build method:
|
||||
///
|
||||
/// ```dart
|
||||
/// assert(debugCheckHasFluentTheme(context));
|
||||
/// ```
|
||||
///
|
||||
/// Does nothing if asserts are disabled. Always returns true.
|
||||
bool debugCheckHasFluentTheme(BuildContext context, [bool check = true]) {
|
||||
assert(() {
|
||||
if (FluentTheme.maybeOf(context) == null) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('A FluentTheme widget is necessary to draw this layout.'),
|
||||
ErrorHint(
|
||||
'To introduce a FluentTheme widget, you can either directly '
|
||||
'include one, or use a widget that contains FluentTheme itself, '
|
||||
'such as FluentApp',
|
||||
),
|
||||
...context.describeMissingAncestor(expectedAncestorType: FluentTheme),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Asserts that the given context has a [Localizations] ancestor that contains
|
||||
/// a [FluentLocalizations] delegate.
|
||||
///
|
||||
/// Used by many fluent design widgets to make sure that they are
|
||||
/// only used in contexts where they have access to localizations.
|
||||
///
|
||||
/// To call this function, use the following pattern, typically in the
|
||||
/// relevant Widget's build method:
|
||||
///
|
||||
/// ```dart
|
||||
/// assert(debugCheckHasFluentLocalizations(context));
|
||||
/// ```
|
||||
///
|
||||
/// Does nothing if asserts are disabled. Always returns true.
|
||||
bool debugCheckHasFluentLocalizations(BuildContext context) {
|
||||
assert(() {
|
||||
if (Localizations.of<FluentLocalizations>(context, FluentLocalizations) ==
|
||||
null) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('No FluentLocalizations found.'),
|
||||
ErrorDescription(
|
||||
'${context.widget.runtimeType} widgets require FluentLocalizations '
|
||||
'to be provided by a Localizations widget ancestor.',
|
||||
),
|
||||
ErrorDescription(
|
||||
'The fluent library uses Localizations to generate messages, '
|
||||
'labels, and abbreviations.',
|
||||
),
|
||||
ErrorHint(
|
||||
'To introduce a FluentLocalizations, either use a '
|
||||
'FluentApp at the root of your application to include them '
|
||||
'automatically, or add a Localization widget with a '
|
||||
'FluentLocalizations delegate.',
|
||||
),
|
||||
...context.describeMissingAncestor(
|
||||
expectedAncestorType: FluentLocalizations)
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Check if the current screen is 10 foot long or bigger.
|
||||
///
|
||||
/// [width] is the width of the current screen. If not provided,
|
||||
/// [SingletonFlutterWindow.physicalSize] is used
|
||||
bool is10footScreen([double? width]) {
|
||||
width ??= ui.window.physicalSize.width;
|
||||
return width >= 11520;
|
||||
}
|
||||
|
||||
Offset horizontalPositionDependentBox({
|
||||
required Size size,
|
||||
required Size childSize,
|
||||
required Offset target,
|
||||
required bool preferLeft,
|
||||
double verticalOffset = 0.0,
|
||||
double margin = 10.0,
|
||||
}) {
|
||||
// Horizontal DIRECTION
|
||||
final bool fitsLeft =
|
||||
target.dx + verticalOffset + childSize.width <= size.width - margin;
|
||||
final bool fitsRight = target.dx - verticalOffset - childSize.width >= margin;
|
||||
final bool tooltipLeft =
|
||||
preferLeft ? fitsLeft || !fitsRight : !(fitsRight || !fitsLeft);
|
||||
double x;
|
||||
if (tooltipLeft) {
|
||||
x = math.min(target.dx + verticalOffset, size.width - margin);
|
||||
} else {
|
||||
x = math.max(target.dx - verticalOffset - childSize.width, margin);
|
||||
}
|
||||
// Vertical DIRECTION
|
||||
double y;
|
||||
if (size.height - margin * 2.0 < childSize.height) {
|
||||
y = (size.height - childSize.height) / 2.0;
|
||||
} else {
|
||||
final double normalizedTargetY =
|
||||
target.dy.clamp(margin, size.height - margin);
|
||||
final double edge = margin + childSize.height / 2.0;
|
||||
if (normalizedTargetY < edge) {
|
||||
y = margin;
|
||||
} else if (normalizedTargetY > size.height - edge) {
|
||||
y = size.height - margin - childSize.height;
|
||||
} else {
|
||||
y = normalizedTargetY - childSize.height / 2.0;
|
||||
}
|
||||
}
|
||||
return Offset(x, y);
|
||||
}
|
||||
76
dependencies/fluent_ui-3.12.0/lib/src/utils/horizontal_scroll_view.dart
vendored
Normal file
76
dependencies/fluent_ui-3.12.0/lib/src/utils/horizontal_scroll_view.dart
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
/// A specialized kind of [SingleChildScrollView] that only scrolls
|
||||
/// horizontally, and allows the mouse wheel to control scrolling.
|
||||
class HorizontalScrollView extends StatefulWidget {
|
||||
final Widget child;
|
||||
final ScrollPhysics? scrollPhysics;
|
||||
|
||||
/// Whether or not the mouse wheel can be used to scroll
|
||||
/// horizontally. On desktop platforms under a default Flutter
|
||||
/// configuration, this may be the only way to scroll horizontally
|
||||
/// unless the user has a trackpad.
|
||||
final bool mouseWheelScrolls;
|
||||
|
||||
const HorizontalScrollView({
|
||||
Key? key,
|
||||
required this.child,
|
||||
this.scrollPhysics,
|
||||
this.mouseWheelScrolls = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HorizontalScrollViewState createState() => _HorizontalScrollViewState();
|
||||
}
|
||||
|
||||
class _HorizontalScrollViewState extends State<HorizontalScrollView> {
|
||||
late final ScrollController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = ScrollController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerSignal: widget.mouseWheelScrolls
|
||||
? (event) {
|
||||
// Do not capture any other type of mouse pointer signals
|
||||
if (event is PointerScrollEvent) {
|
||||
// Make sure we capture this pointer scroll event so that
|
||||
// any scrollable widgets higher up in the hierarchy do not
|
||||
// handle the event also.
|
||||
GestureBinding.instance.pointerSignalResolver.register(event,
|
||||
(event) {
|
||||
if (event is PointerScrollEvent) {
|
||||
// Use animateTo for a smoother behavior when there are
|
||||
// attempts to scroll beyond the boundaries (it will not
|
||||
// jump beyond the boundaries and then "rebound" like jumpTo
|
||||
// would do if used here).
|
||||
_controller.animateTo(
|
||||
_controller.offset + event.scrollDelta.dy,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.ease);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: widget.scrollPhysics ?? const ClampingScrollPhysics(),
|
||||
controller: _controller,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
66
dependencies/fluent_ui-3.12.0/lib/src/utils/label.dart
vendored
Normal file
66
dependencies/fluent_ui-3.12.0/lib/src/utils/label.dart
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// An info label lets the user know what an element of the ui
|
||||
/// do as a short description of its functionality. It can be
|
||||
/// either rendered above its child or on the side of it.
|
||||
///
|
||||
/// 
|
||||
class InfoLabel extends StatelessWidget {
|
||||
/// Creates an info label.
|
||||
const InfoLabel({
|
||||
Key? key,
|
||||
this.child,
|
||||
required this.label,
|
||||
this.labelStyle,
|
||||
this.isHeader = true,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The text of the label. It'll be styled acorrding to
|
||||
/// [labelStyle]. If this is empty, a blank space will
|
||||
/// be rendered.
|
||||
final String label;
|
||||
|
||||
/// The style of the text. If null, [Typography.body] is used
|
||||
final TextStyle? labelStyle;
|
||||
|
||||
/// The widget to apply the label.
|
||||
final Widget? child;
|
||||
|
||||
/// Whether to render [header] above [child] or on the side of it.
|
||||
final bool isHeader;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(StringProperty('label', label));
|
||||
properties.add(DiagnosticsProperty<TextStyle>('labelStyle', labelStyle));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final labelWidget = Text(
|
||||
label,
|
||||
style: labelStyle ?? FluentTheme.maybeOf(context)?.typography.body,
|
||||
);
|
||||
return Flex(
|
||||
direction: isHeader ? Axis.vertical : Axis.horizontal,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment:
|
||||
isHeader ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (isHeader)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: labelWidget,
|
||||
),
|
||||
if (child != null) Flexible(child: child!),
|
||||
if (!isHeader)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: labelWidget,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
524
dependencies/fluent_ui-3.12.0/lib/src/utils/popup.dart
vendored
Normal file
524
dependencies/fluent_ui-3.12.0/lib/src/utils/popup.dart
vendored
Normal file
@@ -0,0 +1,524 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' as m;
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
class PopUp<T> extends StatefulWidget {
|
||||
const PopUp({
|
||||
Key? key,
|
||||
required this.child,
|
||||
required this.content,
|
||||
this.verticalOffset = 0,
|
||||
this.horizontalOffset = 0,
|
||||
this.placement = FlyoutPlacement.center,
|
||||
this.position = FlyoutPosition.above,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final WidgetBuilder content;
|
||||
final double verticalOffset;
|
||||
final double horizontalOffset;
|
||||
|
||||
final FlyoutPlacement placement;
|
||||
final FlyoutPosition position;
|
||||
|
||||
@override
|
||||
PopUpState<T> createState() => PopUpState<T>();
|
||||
}
|
||||
|
||||
class PopUpState<T> extends State<PopUp<T>> {
|
||||
_PopUpRoute<T>? _dropdownRoute;
|
||||
|
||||
Future<void> openPopup() {
|
||||
assert(_dropdownRoute == null, 'You can NOT open a popup twice');
|
||||
final NavigatorState navigator = Navigator.of(context);
|
||||
final RenderBox itemBox = context.findRenderObject()! as RenderBox;
|
||||
Offset leftTarget = itemBox.localToGlobal(
|
||||
itemBox.size.centerLeft(Offset.zero),
|
||||
ancestor: navigator.context.findRenderObject(),
|
||||
);
|
||||
Offset centerTarget = itemBox.localToGlobal(
|
||||
itemBox.size.center(Offset.zero),
|
||||
ancestor: navigator.context.findRenderObject(),
|
||||
);
|
||||
Offset rightTarget = itemBox.localToGlobal(
|
||||
itemBox.size.centerRight(Offset.zero),
|
||||
ancestor: navigator.context.findRenderObject(),
|
||||
);
|
||||
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
final directionality = Directionality.of(context);
|
||||
|
||||
// The target according to the current directionality
|
||||
final Offset directionalityTarget = () {
|
||||
switch (widget.placement) {
|
||||
case FlyoutPlacement.start:
|
||||
if (directionality == TextDirection.ltr) {
|
||||
return leftTarget;
|
||||
} else {
|
||||
return rightTarget;
|
||||
}
|
||||
case FlyoutPlacement.end:
|
||||
if (directionality == TextDirection.ltr) {
|
||||
return rightTarget;
|
||||
} else {
|
||||
return leftTarget;
|
||||
}
|
||||
case FlyoutPlacement.center:
|
||||
case FlyoutPlacement.full:
|
||||
return centerTarget;
|
||||
}
|
||||
}();
|
||||
|
||||
// The placement according to the current directionality
|
||||
final FlyoutPlacement directionalityPlacement = () {
|
||||
switch (widget.placement) {
|
||||
case FlyoutPlacement.start:
|
||||
if (directionality == TextDirection.rtl) {
|
||||
return FlyoutPlacement.end;
|
||||
}
|
||||
continue next;
|
||||
case FlyoutPlacement.end:
|
||||
if (directionality == TextDirection.rtl) {
|
||||
return FlyoutPlacement.start;
|
||||
}
|
||||
continue next;
|
||||
next:
|
||||
default:
|
||||
return widget.placement;
|
||||
}
|
||||
}();
|
||||
|
||||
final Rect itemRect = directionalityTarget & itemBox.size;
|
||||
_dropdownRoute = _PopUpRoute<T>(
|
||||
target: centerTarget,
|
||||
placementOffset: directionalityTarget,
|
||||
placement: directionalityPlacement,
|
||||
position: widget.position,
|
||||
content: _PopupContentManager(content: widget.content),
|
||||
buttonRect: itemRect,
|
||||
elevation: 4,
|
||||
capturedThemes: InheritedTheme.capture(
|
||||
from: context,
|
||||
to: navigator.context,
|
||||
),
|
||||
transitionAnimationDuration:
|
||||
FluentTheme.of(context).mediumAnimationDuration,
|
||||
verticalOffset: widget.verticalOffset,
|
||||
horizontalOffset: widget.horizontalOffset,
|
||||
barrierLabel: FluentLocalizations.of(context).modalBarrierDismissLabel,
|
||||
);
|
||||
|
||||
return navigator.push(_dropdownRoute!).then((T? newValue) {
|
||||
removePopUpRoute();
|
||||
if (!mounted || newValue == null) return;
|
||||
});
|
||||
}
|
||||
|
||||
bool get isOpen => _dropdownRoute != null;
|
||||
|
||||
void removePopUpRoute() {
|
||||
_dropdownRoute?._dismiss();
|
||||
_dropdownRoute = null;
|
||||
// _lastOrientation = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
removePopUpRoute();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
// Backend below
|
||||
|
||||
class _PopUpScrollBehavior extends ScrollBehavior {
|
||||
const _PopUpScrollBehavior();
|
||||
|
||||
@override
|
||||
TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;
|
||||
|
||||
@override
|
||||
Widget buildViewportChrome(context, child, axisDirection) => child;
|
||||
|
||||
@override
|
||||
ScrollPhysics getScrollPhysics(BuildContext context) =>
|
||||
const ClampingScrollPhysics();
|
||||
}
|
||||
|
||||
class _PopUpMenu<T> extends StatefulWidget {
|
||||
const _PopUpMenu({
|
||||
Key? key,
|
||||
required this.route,
|
||||
required this.buttonRect,
|
||||
required this.constraints,
|
||||
this.dropdownColor,
|
||||
}) : super(key: key);
|
||||
|
||||
final _PopUpRoute<T> route;
|
||||
final Rect buttonRect;
|
||||
final BoxConstraints constraints;
|
||||
final Color? dropdownColor;
|
||||
|
||||
@override
|
||||
_PopUpMenuState<T> createState() => _PopUpMenuState<T>();
|
||||
}
|
||||
|
||||
class _PopUpMenuState<T> extends State<_PopUpMenu<T>> {
|
||||
late CurvedAnimation _fadeOpacity;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fadeOpacity = CurvedAnimation(
|
||||
parent: widget.route.animation!,
|
||||
curve: const Interval(0.0, 0.50),
|
||||
reverseCurve: const Interval(0.75, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FadeTransition(
|
||||
opacity: _fadeOpacity,
|
||||
child: Semantics(
|
||||
scopesRoute: true,
|
||||
namesRoute: true,
|
||||
explicitChildNodes: true,
|
||||
child: m.Material(
|
||||
type: m.MaterialType.transparency,
|
||||
child: ScrollConfiguration(
|
||||
behavior: const _PopUpScrollBehavior(),
|
||||
child: widget.route.content,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PopUpMenuRouteLayout<T> extends SingleChildLayoutDelegate {
|
||||
_PopUpMenuRouteLayout({
|
||||
required this.buttonRect,
|
||||
required this.route,
|
||||
required this.textDirection,
|
||||
required this.target,
|
||||
required this.verticalOffset,
|
||||
required this.horizontalOffset,
|
||||
required this.placementOffset,
|
||||
required this.placement,
|
||||
required this.position,
|
||||
required this.screenSize,
|
||||
});
|
||||
|
||||
final Rect buttonRect;
|
||||
final _PopUpRoute<T> route;
|
||||
final TextDirection? textDirection;
|
||||
final Offset target;
|
||||
final double verticalOffset;
|
||||
final double horizontalOffset;
|
||||
final Offset placementOffset;
|
||||
final FlyoutPlacement placement;
|
||||
final FlyoutPosition position;
|
||||
final Size screenSize;
|
||||
|
||||
@override
|
||||
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
||||
return BoxConstraints(
|
||||
maxWidth: constraints.maxWidth,
|
||||
maxHeight: screenSize.height -
|
||||
target.dy -
|
||||
verticalOffset -
|
||||
buttonRect.height -
|
||||
10,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Offset getPositionForChild(Size size, Size childSize) {
|
||||
final defaultOffset = position == FlyoutPosition.side
|
||||
? horizontalPositionDependentBox(
|
||||
size: size,
|
||||
childSize: childSize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
margin: horizontalOffset,
|
||||
preferLeft: placement == FlyoutPlacement.end,
|
||||
)
|
||||
: positionDependentBox(
|
||||
size: size,
|
||||
childSize: childSize,
|
||||
target: target,
|
||||
verticalOffset: verticalOffset,
|
||||
preferBelow: position == FlyoutPosition.below,
|
||||
margin: horizontalOffset,
|
||||
);
|
||||
if (position == FlyoutPosition.side) {
|
||||
return Offset(defaultOffset.dx, defaultOffset.dy);
|
||||
}
|
||||
switch (placement) {
|
||||
case FlyoutPlacement.start:
|
||||
return Offset(placementOffset.dx, defaultOffset.dy);
|
||||
case FlyoutPlacement.end:
|
||||
return Offset(placementOffset.dx - childSize.width, defaultOffset.dy);
|
||||
case FlyoutPlacement.full:
|
||||
return Offset.zero;
|
||||
case FlyoutPlacement.center:
|
||||
return defaultOffset;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRelayout(_PopUpMenuRouteLayout<T> oldDelegate) {
|
||||
return oldDelegate.target == target ||
|
||||
oldDelegate.placementOffset == placementOffset ||
|
||||
buttonRect != oldDelegate.buttonRect;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is _PopUpMenuRouteLayout<T> &&
|
||||
other.buttonRect == buttonRect &&
|
||||
other.route == route &&
|
||||
other.textDirection == textDirection &&
|
||||
other.target == target &&
|
||||
other.verticalOffset == verticalOffset &&
|
||||
other.horizontalOffset == horizontalOffset &&
|
||||
other.placementOffset == placementOffset &&
|
||||
other.placement == placement &&
|
||||
other.position == position &&
|
||||
other.screenSize == screenSize;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return buttonRect.hashCode ^
|
||||
route.hashCode ^
|
||||
textDirection.hashCode ^
|
||||
target.hashCode ^
|
||||
verticalOffset.hashCode ^
|
||||
horizontalOffset.hashCode ^
|
||||
placementOffset.hashCode ^
|
||||
placement.hashCode ^
|
||||
position.hashCode ^
|
||||
screenSize.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class _PopUpRoute<T> extends PopupRoute<T> {
|
||||
_PopUpRoute({
|
||||
required this.content,
|
||||
required this.buttonRect,
|
||||
required this.target,
|
||||
required this.placementOffset,
|
||||
required this.placement,
|
||||
this.elevation = 8,
|
||||
required this.capturedThemes,
|
||||
required this.transitionAnimationDuration,
|
||||
this.barrierLabel,
|
||||
required this.verticalOffset,
|
||||
required this.horizontalOffset,
|
||||
required this.position,
|
||||
});
|
||||
|
||||
final Widget content;
|
||||
final Rect buttonRect;
|
||||
final int elevation;
|
||||
final CapturedThemes capturedThemes;
|
||||
final double verticalOffset;
|
||||
final double horizontalOffset;
|
||||
|
||||
final Duration transitionAnimationDuration;
|
||||
|
||||
final Offset target;
|
||||
final Offset placementOffset;
|
||||
final FlyoutPlacement placement;
|
||||
final FlyoutPosition position;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => transitionAnimationDuration;
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => true;
|
||||
|
||||
@override
|
||||
Color? get barrierColor => null;
|
||||
|
||||
@override
|
||||
final String? barrierLabel;
|
||||
|
||||
@override
|
||||
Widget buildPage(context, animation, secondaryAnimation) {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final page = _PopUpRoutePage<T>(
|
||||
target: target,
|
||||
placementOffset: placementOffset,
|
||||
placement: placement,
|
||||
route: this,
|
||||
constraints: constraints,
|
||||
content: content,
|
||||
buttonRect: buttonRect,
|
||||
elevation: elevation,
|
||||
capturedThemes: capturedThemes,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontalOffset: horizontalOffset,
|
||||
position: position,
|
||||
);
|
||||
return page;
|
||||
});
|
||||
}
|
||||
|
||||
void _dismiss() {
|
||||
if (isActive) {
|
||||
navigator?.removeRoute(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PopUpRoutePage<T> extends StatelessWidget {
|
||||
const _PopUpRoutePage({
|
||||
Key? key,
|
||||
required this.route,
|
||||
required this.constraints,
|
||||
required this.content,
|
||||
required this.buttonRect,
|
||||
this.elevation = 8,
|
||||
required this.capturedThemes,
|
||||
required this.verticalOffset,
|
||||
required this.horizontalOffset,
|
||||
this.style,
|
||||
required this.target,
|
||||
required this.placement,
|
||||
required this.placementOffset,
|
||||
required this.position,
|
||||
}) : super(key: key);
|
||||
|
||||
final _PopUpRoute<T> route;
|
||||
final BoxConstraints constraints;
|
||||
final Widget content;
|
||||
final Rect buttonRect;
|
||||
final int elevation;
|
||||
final CapturedThemes capturedThemes;
|
||||
final TextStyle? style;
|
||||
final double verticalOffset;
|
||||
final double horizontalOffset;
|
||||
final Offset target;
|
||||
final Offset placementOffset;
|
||||
final FlyoutPlacement placement;
|
||||
final FlyoutPosition position;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasDirectionality(context));
|
||||
|
||||
final TextDirection? textDirection = Directionality.maybeOf(context);
|
||||
final Widget menu = _PopUpMenu<T>(
|
||||
route: route,
|
||||
buttonRect: buttonRect,
|
||||
constraints: constraints,
|
||||
);
|
||||
|
||||
return MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
removeBottom: true,
|
||||
removeLeft: true,
|
||||
removeRight: true,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final mediaQuery = MediaQuery.of(context);
|
||||
return SizedBox(
|
||||
height: mediaQuery.size.height,
|
||||
width: mediaQuery.size.width,
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: _PopUpMenuRouteLayout<T>(
|
||||
target: target,
|
||||
placement: placement,
|
||||
position: position,
|
||||
placementOffset: placementOffset,
|
||||
buttonRect: buttonRect,
|
||||
route: route,
|
||||
textDirection: textDirection,
|
||||
verticalOffset: verticalOffset,
|
||||
horizontalOffset: horizontalOffset,
|
||||
screenSize: mediaQuery.size,
|
||||
),
|
||||
child: capturedThemes.wrap(menu),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PopupContentManager extends StatefulWidget {
|
||||
const _PopupContentManager({
|
||||
Key? key,
|
||||
required this.content,
|
||||
}) : super(key: key);
|
||||
|
||||
final WidgetBuilder content;
|
||||
|
||||
@override
|
||||
State<_PopupContentManager> createState() => __PopupContentManagerState();
|
||||
}
|
||||
|
||||
class __PopupContentManagerState extends State<_PopupContentManager> {
|
||||
final GlobalKey key = GlobalKey();
|
||||
|
||||
Size size = Size.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
final context = key.currentContext;
|
||||
if (context == null) return;
|
||||
final RenderBox box = context.findRenderObject() as RenderBox;
|
||||
setState(() => size = box.size);
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KeyedSubtree(
|
||||
key: key,
|
||||
child: PopupContentSizeInfo(
|
||||
size: size,
|
||||
child: widget.content(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PopupContentSizeInfo extends InheritedWidget {
|
||||
const PopupContentSizeInfo({
|
||||
Key? key,
|
||||
required Widget child,
|
||||
required this.size,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final Size size;
|
||||
|
||||
static PopupContentSizeInfo of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<PopupContentSizeInfo>()!;
|
||||
}
|
||||
|
||||
static PopupContentSizeInfo? maybeOf(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType<PopupContentSizeInfo>();
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(PopupContentSizeInfo oldWidget) {
|
||||
return oldWidget.size != size;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user