mirror of
https://github.com/Auties00/Reboot-Launcher.git
synced 2026-01-13 03:02:22 +01:00
Made build portable
This commit is contained in:
45
dependencies/bitsdojo_window-0.1.5/CHANGELOG.md
vendored
Normal file
45
dependencies/bitsdojo_window-0.1.5/CHANGELOG.md
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
## 0.1.5
|
||||||
|
- Runs on Windows 7
|
||||||
|
## 0.1.4
|
||||||
|
- Updated win32 to 3.0.0
|
||||||
|
## 0.1.3
|
||||||
|
- Updated ffi to 2.0.0
|
||||||
|
## 0.1.2
|
||||||
|
- Flutter 3.0 support
|
||||||
|
## 0.1.1+1
|
||||||
|
- Added Linux usage instructions
|
||||||
|
## 0.1.1
|
||||||
|
- Linux support now stable
|
||||||
|
## 0.1.0+1
|
||||||
|
- Fix gtk library name on Linux
|
||||||
|
## 0.1.0
|
||||||
|
- Added null safety support
|
||||||
|
## 0.0.9
|
||||||
|
- Linux support added
|
||||||
|
## 0.0.8
|
||||||
|
- Added macOS readme instructions
|
||||||
|
## 0.0.7
|
||||||
|
- macOS support added
|
||||||
|
## 0.0.6
|
||||||
|
- Works with latest Flutter version (master channel)
|
||||||
|
## 0.0.5
|
||||||
|
- Works with latest Flutter version (dev channel)
|
||||||
|
## 0.0.4
|
||||||
|
- Better integration with other plugins
|
||||||
|
## 0.0.3
|
||||||
|
- Using dpi-aware values for title bar and buttons dimensions
|
||||||
|
- Dynamically calculating default button padding instead of fixed one
|
||||||
|
## 0.0.2
|
||||||
|
- Added video tutorial link
|
||||||
|
## 0.0.1
|
||||||
|
|
||||||
|
* Initial release
|
||||||
|
- Custom window frame - remove standard Windows titlebar and buttons
|
||||||
|
- Hide window on startup
|
||||||
|
- Show/hide window
|
||||||
|
- Minimize/Maximize/Restore/Close window
|
||||||
|
- Move window using Flutter widget
|
||||||
|
- Set window size, minimum size and maximum size
|
||||||
|
- Set window position
|
||||||
|
- Set window alignment on screen (center/topLeft/topRight/bottomLeft/bottomRight)
|
||||||
|
- Set window title
|
||||||
21
dependencies/bitsdojo_window-0.1.5/LICENSE
vendored
Normal file
21
dependencies/bitsdojo_window-0.1.5/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-2021 Bogdan Hobeanu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
262
dependencies/bitsdojo_window-0.1.5/README.md
vendored
Normal file
262
dependencies/bitsdojo_window-0.1.5/README.md
vendored
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
# bitsdojo_window
|
||||||
|
|
||||||
|
A [Flutter package](https://pub.dev/packages/bitsdojo_window) that makes it easy to customize and work with your Flutter desktop app window **on Windows, macOS and Linux**.
|
||||||
|
|
||||||
|
Watch the tutorial to get started. Click the image below to watch the video:
|
||||||
|
|
||||||
|
[](https://www.youtube.com/watch?v=bee2AHQpGK4 "Click to open")
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/bitsdojo/bitsdojo_window/master/resources/screenshot.png">
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
|
||||||
|
- Custom window frame - remove standard Windows/macOS/Linux titlebar and buttons
|
||||||
|
- Hide window on startup
|
||||||
|
- Show/hide window
|
||||||
|
- Move window using Flutter widget
|
||||||
|
- Minimize/Maximize/Restore/Close window
|
||||||
|
- Set window size, minimum size and maximum size
|
||||||
|
- Set window position
|
||||||
|
- Set window alignment on screen (center/topLeft/topRight/bottomLeft/bottomRight)
|
||||||
|
- Set window title
|
||||||
|
|
||||||
|
# Getting Started
|
||||||
|
|
||||||
|
Install the package using `pubspec.yaml`
|
||||||
|
|
||||||
|
# For Windows apps
|
||||||
|
|
||||||
|
Inside your application folder, go to `windows\runner\main.cpp` and add these two lines at the beginning of the file:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||||
|
auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP);
|
||||||
|
```
|
||||||
|
|
||||||
|
# For macOS apps
|
||||||
|
|
||||||
|
Inside your application folder, go to `macos\runner\MainFlutterWindow.swift` and add this line after the one saying `import FlutterMacOS` :
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import FlutterMacOS
|
||||||
|
import bitsdojo_window_macos // Add this line
|
||||||
|
```
|
||||||
|
|
||||||
|
Then change this line from:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class MainFlutterWindow: NSWindow {
|
||||||
|
```
|
||||||
|
|
||||||
|
to this:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class MainFlutterWindow: BitsdojoWindow {
|
||||||
|
```
|
||||||
|
|
||||||
|
After changing `NSWindow` to `BitsdojoWindow` add these lines below the line you changed:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
override func bitsdojo_window_configure() -> UInt {
|
||||||
|
return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Your code should now look like this:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
class MainFlutterWindow: BitsdojoWindow {
|
||||||
|
|
||||||
|
override func bitsdojo_window_configure() -> UInt {
|
||||||
|
return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP
|
||||||
|
}
|
||||||
|
|
||||||
|
override func awakeFromNib() {
|
||||||
|
... //rest of your code
|
||||||
|
```
|
||||||
|
#
|
||||||
|
|
||||||
|
If you don't want to use a custom frame and prefer the standard window titlebar and buttons, you can remove the `BDW_CUSTOM_FRAME` flag from the code above.
|
||||||
|
|
||||||
|
If you don't want to hide the window on startup, you can remove the `BDW_HIDE_ON_STARTUP` flag from the code above.
|
||||||
|
|
||||||
|
# For Linux apps
|
||||||
|
|
||||||
|
Inside your application folder, go to `linux\my_application.cc` and add this line at the beginning of the file:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
||||||
|
```
|
||||||
|
Then look for these two lines:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
|
```
|
||||||
|
and change them to this:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto bdw = bitsdojo_window_from(window); // <--- add this line
|
||||||
|
bdw->setCustomFrame(true); // <-- add this line
|
||||||
|
//gtk_window_set_default_size(window, 1280, 720); // <-- comment this line
|
||||||
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, we commented the line calling `gtk_window_set_default_size` and added these two lines before `gtk_widget_show(GTK_WIDGET(window));`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto bdw = bitsdojo_window_from(window);
|
||||||
|
bdw->setCustomFrame(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
# Flutter app integration
|
||||||
|
|
||||||
|
Now go to `lib\main.dart` and add this code in the `main` function right after `runApp(MyApp());` :
|
||||||
|
|
||||||
|
```dart
|
||||||
|
void main() {
|
||||||
|
runApp(MyApp());
|
||||||
|
|
||||||
|
// Add this code below
|
||||||
|
|
||||||
|
doWhenWindowReady(() {
|
||||||
|
const initialSize = Size(600, 450);
|
||||||
|
appWindow.minSize = initialSize;
|
||||||
|
appWindow.size = initialSize;
|
||||||
|
appWindow.alignment = Alignment.center;
|
||||||
|
appWindow.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
This will set an initial size and a minimum size for your application window, center it on the screen and show it on the screen.
|
||||||
|
|
||||||
|
You can find examples in the `example` folder.
|
||||||
|
|
||||||
|
Here is an example that displays this window:
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand</summary>
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const MyApp());
|
||||||
|
doWhenWindowReady(() {
|
||||||
|
final win = appWindow;
|
||||||
|
const initialSize = Size(600, 450);
|
||||||
|
win.minSize = initialSize;
|
||||||
|
win.size = initialSize;
|
||||||
|
win.alignment = Alignment.center;
|
||||||
|
win.title = "Custom window with Flutter";
|
||||||
|
win.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const borderColor = Color(0xFF805306);
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: Scaffold(
|
||||||
|
body: WindowBorder(
|
||||||
|
color: borderColor,
|
||||||
|
width: 1,
|
||||||
|
child: Row(
|
||||||
|
children: const [LeftSide(), RightSide()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebarColor = Color(0xFFF6A00C);
|
||||||
|
|
||||||
|
class LeftSide extends StatelessWidget {
|
||||||
|
const LeftSide({Key? key}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SizedBox(
|
||||||
|
width: 200,
|
||||||
|
child: Container(
|
||||||
|
color: sidebarColor,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
WindowTitleBarBox(child: MoveWindow()),
|
||||||
|
Expanded(child: Container())
|
||||||
|
],
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backgroundStartColor = Color(0xFFFFD500);
|
||||||
|
const backgroundEndColor = Color(0xFFF6A00C);
|
||||||
|
|
||||||
|
class RightSide extends StatelessWidget {
|
||||||
|
const RightSide({Key? key}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [backgroundStartColor, backgroundEndColor],
|
||||||
|
stops: [0.0, 1.0]),
|
||||||
|
),
|
||||||
|
child: Column(children: [
|
||||||
|
WindowTitleBarBox(
|
||||||
|
child: Row(
|
||||||
|
children: [Expanded(child: MoveWindow()), const WindowButtons()],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final buttonColors = WindowButtonColors(
|
||||||
|
iconNormal: const Color(0xFF805306),
|
||||||
|
mouseOver: const Color(0xFFF6A00C),
|
||||||
|
mouseDown: const Color(0xFF805306),
|
||||||
|
iconMouseOver: const Color(0xFF805306),
|
||||||
|
iconMouseDown: const Color(0xFFFFD500));
|
||||||
|
|
||||||
|
final closeButtonColors = WindowButtonColors(
|
||||||
|
mouseOver: const Color(0xFFD32F2F),
|
||||||
|
mouseDown: const Color(0xFFB71C1C),
|
||||||
|
iconNormal: const Color(0xFF805306),
|
||||||
|
iconMouseOver: Colors.white);
|
||||||
|
|
||||||
|
class WindowButtons extends StatelessWidget {
|
||||||
|
const WindowButtons({Key? key}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
MinimizeWindowButton(colors: buttonColors),
|
||||||
|
MaximizeWindowButton(colors: buttonColors),
|
||||||
|
CloseWindowButton(colors: closeButtonColors),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
|
#
|
||||||
|
# ❤️ **Sponsors - friends helping this package**
|
||||||
|
|
||||||
|
I am developing this package in my spare time and any help is appreciated.
|
||||||
|
If you want to help you can [become a sponsor](https://github.com/sponsors/bitsdojo).
|
||||||
|
|
||||||
|
🙏 Thank you!
|
||||||
|
|
||||||
|
Want to help? [Become a sponsor](https://github.com/sponsors/bitsdojo)
|
||||||
5
dependencies/bitsdojo_window-0.1.5/lib/bitsdojo_window.dart
vendored
Normal file
5
dependencies/bitsdojo_window-0.1.5/lib/bitsdojo_window.dart
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export 'src/widgets/window_border.dart';
|
||||||
|
export 'src/widgets/window_button.dart';
|
||||||
|
export 'src/widgets/window_caption.dart';
|
||||||
|
export 'src/icons/icons.dart';
|
||||||
|
export 'src/app_window.dart';
|
||||||
43
dependencies/bitsdojo_window-0.1.5/lib/src/app_window.dart
vendored
Normal file
43
dependencies/bitsdojo_window-0.1.5/lib/src/app_window.dart
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:bitsdojo_window_platform_interface/bitsdojo_window_platform_interface.dart';
|
||||||
|
import 'package:bitsdojo_window_platform_interface/method_channel_bitsdojo_window.dart';
|
||||||
|
import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart';
|
||||||
|
import 'package:bitsdojo_window_macos/bitsdojo_window_macos.dart';
|
||||||
|
import 'package:bitsdojo_window_linux/bitsdojo_window_linux.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
bool _platformInstanceNeedsInit = true;
|
||||||
|
|
||||||
|
void initPlatformInstance() {
|
||||||
|
if (!kIsWeb) {
|
||||||
|
if (BitsdojoWindowPlatform.instance is MethodChannelBitsdojoWindow) {
|
||||||
|
if (Platform.isWindows) {
|
||||||
|
BitsdojoWindowPlatform.instance = BitsdojoWindowWindows();
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
BitsdojoWindowPlatform.instance = BitsdojoWindowMacOS();
|
||||||
|
} else if (Platform.isLinux) {
|
||||||
|
BitsdojoWindowPlatform.instance = BitsdojoWindowLinux();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BitsdojoWindowPlatform.instance = BitsdojoWindowPlatformNotImplemented();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BitsdojoWindowPlatform get _platform {
|
||||||
|
var needsInit = _platformInstanceNeedsInit;
|
||||||
|
if (needsInit) {
|
||||||
|
initPlatformInstance();
|
||||||
|
_platformInstanceNeedsInit = false;
|
||||||
|
}
|
||||||
|
return BitsdojoWindowPlatform.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void doWhenWindowReady(VoidCallback callback) {
|
||||||
|
_platform.doWhenWindowReady(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopWindow get appWindow {
|
||||||
|
return _platform.appWindow;
|
||||||
|
}
|
||||||
114
dependencies/bitsdojo_window-0.1.5/lib/src/icons/icons.dart
vendored
Normal file
114
dependencies/bitsdojo_window-0.1.5/lib/src/icons/icons.dart
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
// Switched to CustomPaint icons by https://github.com/esDotDev
|
||||||
|
|
||||||
|
/// Close
|
||||||
|
class CloseIcon extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
CloseIcon({Key? key, required this.color}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: Stack(children: [
|
||||||
|
// Use rotated containers instead of a painter because it renders slightly crisper than a painter for some reason.
|
||||||
|
Transform.rotate(
|
||||||
|
angle: pi * .25,
|
||||||
|
child:
|
||||||
|
Center(child: Container(width: 14, height: 1, color: color))),
|
||||||
|
Transform.rotate(
|
||||||
|
angle: pi * -.25,
|
||||||
|
child:
|
||||||
|
Center(child: Container(width: 14, height: 1, color: color))),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maximize
|
||||||
|
class MaximizeIcon extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
MaximizeIcon({Key? key, required this.color}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _AlignedPaint(_MaximizePainter(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MaximizePainter extends _IconPainter {
|
||||||
|
_MaximizePainter(Color color) : super(color);
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
Paint p = getPaint(color);
|
||||||
|
canvas.drawRect(Rect.fromLTRB(0, 0, size.width - 1, size.height - 1), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore
|
||||||
|
class RestoreIcon extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
RestoreIcon({
|
||||||
|
Key? key,
|
||||||
|
required this.color,
|
||||||
|
}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _AlignedPaint(_RestorePainter(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RestorePainter extends _IconPainter {
|
||||||
|
_RestorePainter(Color color) : super(color);
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
Paint p = getPaint(color);
|
||||||
|
canvas.drawRect(Rect.fromLTRB(0, 2, size.width - 2, size.height), p);
|
||||||
|
canvas.drawLine(Offset(2, 2), Offset(2, 0), p);
|
||||||
|
canvas.drawLine(Offset(2, 0), Offset(size.width, 0), p);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(size.width, 0), Offset(size.width, size.height - 2), p);
|
||||||
|
canvas.drawLine(Offset(size.width, size.height - 2),
|
||||||
|
Offset(size.width - 2, size.height - 2), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimize
|
||||||
|
class MinimizeIcon extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
MinimizeIcon({Key? key, required this.color}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _AlignedPaint(_MinimizePainter(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MinimizePainter extends _IconPainter {
|
||||||
|
_MinimizePainter(Color color) : super(color);
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
Paint p = getPaint(color);
|
||||||
|
canvas.drawLine(
|
||||||
|
Offset(0, size.height / 2), Offset(size.width, size.height / 2), p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helpers
|
||||||
|
abstract class _IconPainter extends CustomPainter {
|
||||||
|
_IconPainter(this.color);
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlignedPaint extends StatelessWidget {
|
||||||
|
const _AlignedPaint(this.painter, {Key? key}) : super(key: key);
|
||||||
|
final CustomPainter painter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: CustomPaint(size: Size(10, 10), painter: painter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Paint getPaint(Color color, [bool isAntiAlias = false]) => Paint()
|
||||||
|
..color = color
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..isAntiAlias = isAntiAlias
|
||||||
|
..strokeWidth = 1;
|
||||||
71
dependencies/bitsdojo_window-0.1.5/lib/src/widgets/mouse_state_builder.dart
vendored
Normal file
71
dependencies/bitsdojo_window-0.1.5/lib/src/widgets/mouse_state_builder.dart
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
typedef MouseStateBuilderCB = Widget Function(
|
||||||
|
BuildContext context, MouseState mouseState);
|
||||||
|
|
||||||
|
class MouseState {
|
||||||
|
bool isMouseOver = false;
|
||||||
|
bool isMouseDown = false;
|
||||||
|
MouseState();
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return "isMouseDown: ${this.isMouseDown} - isMouseOver: ${this.isMouseOver}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
T? _ambiguate<T>(T? value) => value;
|
||||||
|
|
||||||
|
class MouseStateBuilder extends StatefulWidget {
|
||||||
|
final MouseStateBuilderCB builder;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
MouseStateBuilder({Key? key, required this.builder, this.onPressed})
|
||||||
|
: super(key: key);
|
||||||
|
@override
|
||||||
|
_MouseStateBuilderState createState() => _MouseStateBuilderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MouseStateBuilderState extends State<MouseStateBuilder> {
|
||||||
|
late MouseState _mouseState;
|
||||||
|
_MouseStateBuilderState() {
|
||||||
|
_mouseState = MouseState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
onEnter: (event) {
|
||||||
|
setState(() {
|
||||||
|
_mouseState.isMouseOver = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onExit: (event) {
|
||||||
|
setState(() {
|
||||||
|
_mouseState.isMouseOver = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: GestureDetector(
|
||||||
|
onTapDown: (_) {
|
||||||
|
setState(() {
|
||||||
|
_mouseState.isMouseDown = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onTapCancel: () {
|
||||||
|
setState(() {
|
||||||
|
_mouseState.isMouseDown = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_mouseState.isMouseDown = false;
|
||||||
|
_mouseState.isMouseOver = false;
|
||||||
|
});
|
||||||
|
_ambiguate(WidgetsBinding.instance)!.addPostFrameCallback((_) {
|
||||||
|
if (widget.onPressed != null) {
|
||||||
|
widget.onPressed!();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onTapUp: (_) {},
|
||||||
|
child: widget.builder(context, _mouseState)));
|
||||||
|
}
|
||||||
|
}
|
||||||
49
dependencies/bitsdojo_window-0.1.5/lib/src/widgets/window_border.dart
vendored
Normal file
49
dependencies/bitsdojo_window-0.1.5/lib/src/widgets/window_border.dart
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import 'package:bitsdojo_window_windows/bitsdojo_window_windows.dart'
|
||||||
|
show WinDesktopWindow;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import '../app_window.dart';
|
||||||
|
|
||||||
|
class WindowBorder extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
final Color color;
|
||||||
|
final double? width;
|
||||||
|
|
||||||
|
WindowBorder({Key? key, required this.child, required this.color, this.width})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool isWindowsApp =
|
||||||
|
(!kIsWeb) && (defaultTargetPlatform == TargetPlatform.windows);
|
||||||
|
bool isLinuxApp =
|
||||||
|
(!kIsWeb) && (defaultTargetPlatform == TargetPlatform.linux);
|
||||||
|
|
||||||
|
// Only show border on Windows and Linux
|
||||||
|
if (!(isWindowsApp || isLinuxApp)) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
var borderWidth = width ?? 1;
|
||||||
|
var topBorderWidth = width ?? 1;
|
||||||
|
|
||||||
|
if (appWindow is WinDesktopWindow) {
|
||||||
|
appWindow as WinDesktopWindow..setWindowCutOnMaximize(borderWidth.ceil());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWindowsApp) {
|
||||||
|
topBorderWidth += 1 / appWindow.scaleFactor;
|
||||||
|
}
|
||||||
|
final topBorderSide = BorderSide(color: this.color, width: topBorderWidth);
|
||||||
|
final borderSide = BorderSide(color: this.color, width: borderWidth);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
child: child,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: topBorderSide,
|
||||||
|
left: borderSide,
|
||||||
|
right: borderSide,
|
||||||
|
bottom: borderSide)));
|
||||||
|
}
|
||||||
|
}
|
||||||
203
dependencies/bitsdojo_window-0.1.5/lib/src/widgets/window_button.dart
vendored
Normal file
203
dependencies/bitsdojo_window-0.1.5/lib/src/widgets/window_button.dart
vendored
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import './mouse_state_builder.dart';
|
||||||
|
import '../icons/icons.dart';
|
||||||
|
import '../app_window.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
typedef WindowButtonIconBuilder = Widget Function(
|
||||||
|
WindowButtonContext buttonContext);
|
||||||
|
typedef WindowButtonBuilder = Widget Function(
|
||||||
|
WindowButtonContext buttonContext, Widget icon);
|
||||||
|
|
||||||
|
class WindowButtonContext {
|
||||||
|
BuildContext context;
|
||||||
|
MouseState mouseState;
|
||||||
|
Color? backgroundColor;
|
||||||
|
Color iconColor;
|
||||||
|
WindowButtonContext(
|
||||||
|
{required this.context,
|
||||||
|
required this.mouseState,
|
||||||
|
this.backgroundColor,
|
||||||
|
required this.iconColor});
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindowButtonColors {
|
||||||
|
late Color normal;
|
||||||
|
late Color mouseOver;
|
||||||
|
late Color mouseDown;
|
||||||
|
late Color iconNormal;
|
||||||
|
late Color iconMouseOver;
|
||||||
|
late Color iconMouseDown;
|
||||||
|
WindowButtonColors(
|
||||||
|
{Color? normal,
|
||||||
|
Color? mouseOver,
|
||||||
|
Color? mouseDown,
|
||||||
|
Color? iconNormal,
|
||||||
|
Color? iconMouseOver,
|
||||||
|
Color? iconMouseDown}) {
|
||||||
|
this.normal = normal ?? _defaultButtonColors.normal;
|
||||||
|
this.mouseOver = mouseOver ?? _defaultButtonColors.mouseOver;
|
||||||
|
this.mouseDown = mouseDown ?? _defaultButtonColors.mouseDown;
|
||||||
|
this.iconNormal = iconNormal ?? _defaultButtonColors.iconNormal;
|
||||||
|
this.iconMouseOver = iconMouseOver ?? _defaultButtonColors.iconMouseOver;
|
||||||
|
this.iconMouseDown = iconMouseDown ?? _defaultButtonColors.iconMouseDown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final _defaultButtonColors = WindowButtonColors(
|
||||||
|
normal: Colors.transparent,
|
||||||
|
iconNormal: Color(0xFF805306),
|
||||||
|
mouseOver: Color(0xFF404040),
|
||||||
|
mouseDown: Color(0xFF202020),
|
||||||
|
iconMouseOver: Color(0xFFFFFFFF),
|
||||||
|
iconMouseDown: Color(0xFFF0F0F0));
|
||||||
|
|
||||||
|
class WindowButton extends StatelessWidget {
|
||||||
|
final WindowButtonBuilder? builder;
|
||||||
|
final WindowButtonIconBuilder? iconBuilder;
|
||||||
|
late final WindowButtonColors colors;
|
||||||
|
final bool animate;
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
|
WindowButton(
|
||||||
|
{Key? key,
|
||||||
|
WindowButtonColors? colors,
|
||||||
|
this.builder,
|
||||||
|
@required this.iconBuilder,
|
||||||
|
this.padding,
|
||||||
|
this.onPressed,
|
||||||
|
this.animate = false})
|
||||||
|
: super(key: key) {
|
||||||
|
this.colors = colors ?? _defaultButtonColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getBackgroundColor(MouseState mouseState) {
|
||||||
|
if (mouseState.isMouseDown) return colors.mouseDown;
|
||||||
|
if (mouseState.isMouseOver) return colors.mouseOver;
|
||||||
|
return colors.normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getIconColor(MouseState mouseState) {
|
||||||
|
if (mouseState.isMouseDown) return colors.iconMouseDown;
|
||||||
|
if (mouseState.isMouseOver) return colors.iconMouseOver;
|
||||||
|
return colors.iconNormal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return Container();
|
||||||
|
} else {
|
||||||
|
// Don't show button on macOS
|
||||||
|
if (Platform.isMacOS) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final buttonSize = appWindow.titleBarButtonSize;
|
||||||
|
return MouseStateBuilder(
|
||||||
|
builder: (context, mouseState) {
|
||||||
|
WindowButtonContext buttonContext = WindowButtonContext(
|
||||||
|
mouseState: mouseState,
|
||||||
|
context: context,
|
||||||
|
backgroundColor: getBackgroundColor(mouseState),
|
||||||
|
iconColor: getIconColor(mouseState));
|
||||||
|
|
||||||
|
var icon = (this.iconBuilder != null)
|
||||||
|
? this.iconBuilder!(buttonContext)
|
||||||
|
: Container();
|
||||||
|
double borderSize = appWindow.borderSize;
|
||||||
|
double defaultPadding =
|
||||||
|
(appWindow.titleBarHeight - borderSize) / 3 - (borderSize / 2);
|
||||||
|
// Used when buttonContext.backgroundColor is null, allowing the AnimatedContainer to fade-out smoothly.
|
||||||
|
var fadeOutColor =
|
||||||
|
getBackgroundColor(MouseState()..isMouseOver = true).withOpacity(0);
|
||||||
|
var padding = this.padding ?? EdgeInsets.zero;
|
||||||
|
var animationMs =
|
||||||
|
mouseState.isMouseOver ? (animate ? 100 : 0) : (animate ? 200 : 0);
|
||||||
|
Widget iconWithPadding = Padding(padding: padding, child: icon);
|
||||||
|
iconWithPadding = AnimatedContainer(
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
duration: Duration(milliseconds: animationMs),
|
||||||
|
color: buttonContext.backgroundColor ?? fadeOutColor,
|
||||||
|
child: iconWithPadding);
|
||||||
|
var button = (this.builder != null)
|
||||||
|
? this.builder!(buttonContext, icon)
|
||||||
|
: iconWithPadding;
|
||||||
|
return SizedBox(
|
||||||
|
width: 48, height: 48, child: button);
|
||||||
|
},
|
||||||
|
onPressed: () {
|
||||||
|
if (this.onPressed != null) this.onPressed!();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MinimizeWindowButton extends WindowButton {
|
||||||
|
MinimizeWindowButton(
|
||||||
|
{Key? key,
|
||||||
|
WindowButtonColors? colors,
|
||||||
|
VoidCallback? onPressed,
|
||||||
|
bool? animate})
|
||||||
|
: super(
|
||||||
|
key: key,
|
||||||
|
colors: colors,
|
||||||
|
animate: animate ?? false,
|
||||||
|
iconBuilder: (buttonContext) =>
|
||||||
|
MinimizeIcon(color: buttonContext.iconColor),
|
||||||
|
onPressed: onPressed ?? () => appWindow.minimize());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MaximizeWindowButton extends WindowButton {
|
||||||
|
MaximizeWindowButton(
|
||||||
|
{Key? key,
|
||||||
|
WindowButtonColors? colors,
|
||||||
|
VoidCallback? onPressed,
|
||||||
|
bool? animate})
|
||||||
|
: super(
|
||||||
|
key: key,
|
||||||
|
colors: colors,
|
||||||
|
animate: animate ?? false,
|
||||||
|
iconBuilder: (buttonContext) =>
|
||||||
|
MaximizeIcon(color: buttonContext.iconColor),
|
||||||
|
onPressed: onPressed ?? () => appWindow.maximizeOrRestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
class RestoreWindowButton extends WindowButton {
|
||||||
|
RestoreWindowButton(
|
||||||
|
{Key? key,
|
||||||
|
WindowButtonColors? colors,
|
||||||
|
VoidCallback? onPressed,
|
||||||
|
bool? animate})
|
||||||
|
: super(
|
||||||
|
key: key,
|
||||||
|
colors: colors,
|
||||||
|
animate: animate ?? false,
|
||||||
|
iconBuilder: (buttonContext) =>
|
||||||
|
RestoreIcon(color: buttonContext.iconColor),
|
||||||
|
onPressed: onPressed ?? () => appWindow.maximizeOrRestore());
|
||||||
|
}
|
||||||
|
|
||||||
|
final _defaultCloseButtonColors = WindowButtonColors(
|
||||||
|
mouseOver: Color(0xFFD32F2F),
|
||||||
|
mouseDown: Color(0xFFB71C1C),
|
||||||
|
iconNormal: Color(0xFF805306),
|
||||||
|
iconMouseOver: Color(0xFFFFFFFF));
|
||||||
|
|
||||||
|
class CloseWindowButton extends WindowButton {
|
||||||
|
CloseWindowButton(
|
||||||
|
{Key? key,
|
||||||
|
WindowButtonColors? colors,
|
||||||
|
VoidCallback? onPressed,
|
||||||
|
bool? animate})
|
||||||
|
: super(
|
||||||
|
key: key,
|
||||||
|
colors: colors ?? _defaultCloseButtonColors,
|
||||||
|
animate: animate ?? false,
|
||||||
|
iconBuilder: (buttonContext) =>
|
||||||
|
CloseIcon(color: buttonContext.iconColor),
|
||||||
|
onPressed: onPressed ?? () => appWindow.close());
|
||||||
|
}
|
||||||
48
dependencies/bitsdojo_window-0.1.5/lib/src/widgets/window_caption.dart
vendored
Normal file
48
dependencies/bitsdojo_window-0.1.5/lib/src/widgets/window_caption.dart
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import '../app_window.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
|
||||||
|
class _MoveWindow extends StatelessWidget {
|
||||||
|
_MoveWindow({Key? key, this.child, this.onDoubleTap}) : super(key: key);
|
||||||
|
final Widget? child;
|
||||||
|
final VoidCallback? onDoubleTap;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onPanStart: (details) {
|
||||||
|
appWindow.startDragging();
|
||||||
|
},
|
||||||
|
onDoubleTap: this.onDoubleTap ?? () => appWindow.maximizeOrRestore(),
|
||||||
|
child: this.child ?? Container());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MoveWindow extends StatelessWidget {
|
||||||
|
final Widget? child;
|
||||||
|
final VoidCallback? onDoubleTap;
|
||||||
|
MoveWindow({Key? key, this.child, this.onDoubleTap}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (child == null) return _MoveWindow(onDoubleTap: this.onDoubleTap);
|
||||||
|
return _MoveWindow(
|
||||||
|
onDoubleTap: this.onDoubleTap,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [Expanded(child: this.child!)]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class WindowTitleBarBox extends StatelessWidget {
|
||||||
|
final Widget? child;
|
||||||
|
WindowTitleBarBox({Key? key, this.child}) : super(key: key);
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (kIsWeb) {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
final titlebarHeight = appWindow.titleBarHeight;
|
||||||
|
return SizedBox(height: titlebarHeight, child: this.child ?? Container());
|
||||||
|
}
|
||||||
|
}
|
||||||
31
dependencies/bitsdojo_window-0.1.5/pubspec.yaml
vendored
Normal file
31
dependencies/bitsdojo_window-0.1.5/pubspec.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: bitsdojo_window
|
||||||
|
description: A package to help with creating custom windows with Flutter desktop (custom border, titlebar and minimize/maximize/close buttons) and common desktop window operations (show/hide/position on screen) for Windows and macOS
|
||||||
|
version: 0.1.5
|
||||||
|
homepage: https://www.bitsdojo.com
|
||||||
|
repository: https://github.com/bitsdojo/bitsdojo_window
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=2.17.0 <3.0.0"
|
||||||
|
flutter: ">=1.20.0"
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
plugin:
|
||||||
|
platforms:
|
||||||
|
windows:
|
||||||
|
default_package: bitsdojo_window_windows
|
||||||
|
macos:
|
||||||
|
default_package: bitsdojo_window_macos
|
||||||
|
linux:
|
||||||
|
default_package: bitsdojo_window_linux
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
bitsdojo_window_platform_interface: ^0.1.2
|
||||||
|
#path: ../bitsdojo_window_platform_interface
|
||||||
|
bitsdojo_window_windows: ^0.1.5
|
||||||
|
#path: ../bitsdojo_window_windows
|
||||||
|
bitsdojo_window_macos: ^0.1.3
|
||||||
|
#path: ../bitsdojo_window_macos
|
||||||
|
bitsdojo_window_linux: ^0.1.3
|
||||||
|
#path: ../bitsdojo_window_linux
|
||||||
897
dependencies/fluent_ui-3.12.0/CHANGELOG.md
vendored
Normal file
897
dependencies/fluent_ui-3.12.0/CHANGELOG.md
vendored
Normal file
@@ -0,0 +1,897 @@
|
|||||||
|
Date format: DD/MM/YYYY
|
||||||
|
|
||||||
|
## [3.12.0] - Flutter 3.0 - [13/05/2022]
|
||||||
|
|
||||||
|
- Add support for Flutter 3.0 (Fixes [#186](https://github.com/bdlukaa/fluent_ui/issues/186), [#327](https://github.com/bdlukaa/fluent_ui/issues/327))
|
||||||
|
|
||||||
|
## [3.11.1] - [30/04/2022]
|
||||||
|
|
||||||
|
- Reworked `DropDownButton` ([#297](https://github.com/bdlukaa/fluent_ui/pull/297)):
|
||||||
|
- `DropDownButton` now uses `Flyout` and `MenuFlyout` to display the menu
|
||||||
|
- Added scrolling features and style to `MenuFlyout`
|
||||||
|
- `MenuFlyout` content height is now properly calculated (Fixes [#210](https://github.com/bdlukaa/fluent_ui/issues/210))
|
||||||
|
- `DropDownButtonItem` is deprecated. `MenuFlyoutItem` should be used instead
|
||||||
|
- Added `DropDownButton.buttonBuilder`, which is able to style the button as you wish. `DropDownButton.buttonStyle` is now deprecated
|
||||||
|
```dart
|
||||||
|
DropDownButton(
|
||||||
|
items: [...],
|
||||||
|
// onOpen should be called to open the flyout. If onOpen is null, it means the button
|
||||||
|
// should be disabled
|
||||||
|
buttonBuilder: (context, onOpen) {
|
||||||
|
return Button(
|
||||||
|
...,
|
||||||
|
onPressed: onOpen,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
- `TextButton` now uses `textButtonStyle` instead of `outlinedButtonStyle`
|
||||||
|
- Add `TextFormBox.decoration` ([#312](https://github.com/bdlukaa/fluent_ui/pull/312))
|
||||||
|
|
||||||
|
## [3.11.0] - Menu Flyouts - [23/04/2022]
|
||||||
|
|
||||||
|
- Implemented `MenuFlyout` ([#266](https://github.com/bdlukaa/fluent_ui/pull/266))
|
||||||
|
- Implemented `FlyoutPosition`, which controls where the flyout will be opened according to the child. It can be `above`, `below` or `side`
|
||||||
|
- `FlyoutOpenMode.longHover`, which makes possible to open the flyout when the user performs a long hover
|
||||||
|
- Added `Flyout.onOpen` and `Flyout.onClose`. Some convenience callbacks that are called when the flyout is opened or closed, respectively
|
||||||
|
- Implement `PopupContentSizeInfo`, which provides the information about the content size
|
||||||
|
- Implemented `MenuFlyoutItem`, `MenuFlyoutSeparator` and `MenuFlyoutSubItem`. They are used inside `MenuFlyout` to render the menu items
|
||||||
|
- `horizontalPositionDependentBox` is now globally available for use as a top function
|
||||||
|
- Implemented overflow popup on `NavigationView` for top mode ([#277](https://github.com/bdlukaa/fluent_ui/pull/277))
|
||||||
|
- `InfoBadge` now is correctly positioned on top mode ([#296](https://github.com/bdlukaa/fluent_ui/pull/296))
|
||||||
|
|
||||||
|
## [3.10.3] - [15/04/2022]
|
||||||
|
|
||||||
|
- Do not use duplicated `Scrollbar`s ([#279](https://github.com/bdlukaa/fluent_ui/pull/279/))
|
||||||
|
- Allow custom height on `NavigationPane` header. ([#260](https://github.com/bdlukaa/fluent_ui/pull/260/))
|
||||||
|
- Allow to define the minimal tab width ([#282](https://github.com/bdlukaa/fluent_ui/pull/282/))
|
||||||
|
- Allow applying custom leading Widget to NavigationPane ([#288](https://github.com/bdlukaa/fluent_ui/pull/288/))
|
||||||
|
- `TextFormBox.expands` now works properly ([#291]](https://github.com/bdlukaa/fluent_ui/pull/291))
|
||||||
|
- Focus on `TextBox` is no longer duplicated ([#290](https://github.com/bdlukaa/fluent_ui/pull/290))
|
||||||
|
|
||||||
|
## [3.10.2] - [09/04/2022]
|
||||||
|
|
||||||
|
- `NavigationView` without pane no longer throws error ([#276](https://github.com/bdlukaa/fluent_ui/issues/276))
|
||||||
|
|
||||||
|
## [3.10.1] - [06/04/2022]
|
||||||
|
|
||||||
|
- Fix overflow behavior for `TreeViewItem` ([#270](https://github.com/bdlukaa/fluent_ui/pull/270))
|
||||||
|
- Do not animate sticky indicators when parent is updated ([#273](https://github.com/bdlukaa/fluent_ui/pull/273))
|
||||||
|
- Add Arabic(ar) localization ([#268](https://github.com/bdlukaa/fluent_ui/pull/268))
|
||||||
|
|
||||||
|
## [3.10.0] - Localization, Indicators, CommandBar and Flyouts - [02/04/2022]
|
||||||
|
|
||||||
|
- Improves `icons.dart` formatting and its generation ([#215](https://github.com/bdlukaa/fluent_ui/pull/215))
|
||||||
|
- Use correct color on `FilledButton` when disabled ([209](https://github.com/bdlukaa/fluent_ui/pull/209))
|
||||||
|
- Built-in support for new languages ([#216](https://github.com/bdlukaa/fluent_ui/pull/216)):
|
||||||
|
- English
|
||||||
|
- Spanish (reviewed by [@henry2man](https://github.com/henry2man))
|
||||||
|
- French (reviewed by [@WinXaito](https://github.com/WinXaito))
|
||||||
|
- Brazilian Portuguese (reviewed by [@bdlukaa](https://github.com/bdlukaa))
|
||||||
|
- Russian (reviewed by [@raitonoberu](https://github.com/raitonoberu))
|
||||||
|
- German (reviewed by [@larsb24](https://github.com/larsb24))
|
||||||
|
- Hindi (reviewed by [@alexmercerind](https://github.com/alexmercerind))
|
||||||
|
- Simplified Chinese (reviewed by [@zacksleo](https://github.com/zacksleo))
|
||||||
|
- Add `useInheritedMediaQuery` property to `FluentApp` ([#211](https://github.com/bdlukaa/fluent_ui/pull/211))
|
||||||
|
- `TreeView` updates ([#255](https://github.com/bdlukaa/fluent_ui/pull/225)):
|
||||||
|
- Optional vertical scrolling by setting `shrinkWrap` to `false`
|
||||||
|
- TreeViewItem now has a custom primary key (`value` field)
|
||||||
|
- Added `onSelectionChanged` callback, called when the selection is changed
|
||||||
|
- Account for enabled on pressing states ([#233](https://github.com/bdlukaa/fluent_ui/pull/233))
|
||||||
|
- Implement `CommandBar` ([#232](https://github.com/bdlukaa/fluent_ui/pull/232))
|
||||||
|
- Add `DynamicOverflow` layout widget, for one-run horizontal or vertical layout with an overflow widget
|
||||||
|
- Add `HorizontalScrollView` helper widget, with mouse wheel horizontal scrolling
|
||||||
|
- Long `content` widget no longer overflow in `ContentDialog` ([#242](https://github.com/bdlukaa/fluent_ui/issues/242))
|
||||||
|
- Content state is no longer lost when the pane display mode is changed ([#250](https://github.com/bdlukaa/fluent_ui/pull/250))
|
||||||
|
- **BREAKING** Update indicators ([#248](https://github.com/bdlukaa/fluent_ui/pull/248)):
|
||||||
|
- Added `InheritedNavigationView`
|
||||||
|
- Updated sticky indicator to match the latest Win 11 UI ([#173](https://github.com/bdlukaa/fluent_ui/issues/173))
|
||||||
|
- **BREAKING** Renamed `NavigationPane.indicatorBuilder` to `NavigationPane.indicator`
|
||||||
|
- **BREAKING** Indicators are no longer built with functions
|
||||||
|
Before:
|
||||||
|
```dart
|
||||||
|
indicatorBuilder: ({
|
||||||
|
required BuildContext context,
|
||||||
|
required NavigationPane pane,
|
||||||
|
required Axis axis,
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
if (pane.selected == null) return child;
|
||||||
|
assert(debugCheckHasFluentTheme(context));
|
||||||
|
final theme = NavigationPaneTheme.of(context);
|
||||||
|
|
||||||
|
final left = theme.iconPadding?.left ?? theme.labelPadding?.left ?? 0;
|
||||||
|
final right = theme.labelPadding?.right ?? theme.iconPadding?.right ?? 0;
|
||||||
|
|
||||||
|
return StickyNavigationIndicator(
|
||||||
|
index: pane.selected!,
|
||||||
|
pane: pane,
|
||||||
|
child: child,
|
||||||
|
color: theme.highlightColor,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
axis: axis,
|
||||||
|
topPadding: EdgeInsets.only(left: left, right: right),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now:
|
||||||
|
```dart
|
||||||
|
indicator: StickyNavigationIndicator(
|
||||||
|
color: Colors.blue.lighter, // optional
|
||||||
|
),
|
||||||
|
```
|
||||||
|
- `initiallyExpanded` property on `Expander` works properly ([#252](https://github.com/bdlukaa/fluent_ui/pull/252))
|
||||||
|
- **BREAKING** Flyout changes:
|
||||||
|
- Removed `Flyout.contentWidth` and added `FlyoutContent.constraints`. Now the content will be automatically sized and layed out according to the placement
|
||||||
|
- Added `Flyout.placement` which takes a `FlyoutPlacement`
|
||||||
|
- Added `Flyout.openMode` which takes a `FlyoutOpenMode`
|
||||||
|
- `Flyout.controller` is no longer required. If not provided, a local controller is created to handle the `Flyout.openMode` settings
|
||||||
|
- **Breaking** `FlyoutController.open` is now a function
|
||||||
|
- Added `FlyoutController.isOpen`, `FlyoutController.isClosed`, `FlyoutController.close()`, `FlyoutController.open()` and `FlyoutController.toggle()`
|
||||||
|
- **Breaking** Removed `Popup.contentHeight`
|
||||||
|
- **BREAKING** Updated typography ([#261](https://github.com/bdlukaa/fluent_ui/pull/261)):
|
||||||
|
- Renamed `Typography.standart` to `Typography.fromBrightness`
|
||||||
|
- Renamed `Typography` constructor to `Typography.raw`
|
||||||
|
- Default color for dark mode is now `const Color(0xE4000000)`
|
||||||
|
- Updated default font sizes for `display`, `titleLarge`, `title` and `subtitle`
|
||||||
|
- `TabWidthBehavior.sizeToContent` now works properly ([#218](https://github.com/bdlukaa/fluent_ui/issues/218))
|
||||||
|
|
||||||
|
## [3.9.1] - Input Update - [25/02/2022]
|
||||||
|
|
||||||
|
- `TextBox` updates: ([#179](https://github.com/bdlukaa/fluent_ui/pull/179))
|
||||||
|
- Correctly apply the `style` property
|
||||||
|
- Correctly apply `decoration` to the background
|
||||||
|
- Added `foregroundDecoration` and `highlightColor` property. They can not be specified at the same time
|
||||||
|
- **BREAKING** replaced `maxLengthEnforeced` with `maxLengthEnforcement`
|
||||||
|
- Expose more propertied to `TextFormBox`
|
||||||
|
- `AutoSuggestBox` updates:
|
||||||
|
- Improved fidelity of the suggestions overlay expose more customization properties ([#174](https://github.com/bdlukaa/fluent_ui/issues/174))
|
||||||
|
- When a suggestion is picked, the overlay is automatically closed and the text box is unfocused
|
||||||
|
- Clear button now only shows when the text box is focused
|
||||||
|
- Add directionality support ([#184](https://github.com/bdlukaa/fluent_ui/pull/184))
|
||||||
|
- Correctly apply elevation for `DropDownButton` overlay ([#182](https://github.com/bdlukaa/fluent_ui/issues/182))
|
||||||
|
- Show app bar even if `NavigationPane` is not provided on `NavigationView` ([#187](https://github.com/bdlukaa/fluent_ui/issues/187))
|
||||||
|
- Ensure `NavigationAppBar.actions` are rendered on the top of the other widgets ([#177](https://github.com/bdlukaa/fluent_ui/issues/177))
|
||||||
|
- All Form widgets now have the same height by default
|
||||||
|
- Only show one scrollbar on `ComboBox` overlay
|
||||||
|
- Fix opened pane opacity
|
||||||
|
- Added `menuColor` for theme, which is now used by dropdown button, auto suggest box, tooltip and content dialog
|
||||||
|
- Added `Card` and `cardColor` for theme
|
||||||
|
- Update fluent text controls and added support for `SelectableText` ([#196](https://github.com/bdlukaa/fluent_ui/pull/196))
|
||||||
|
|
||||||
|
## [3.9.0] - Fidelity - [10/02/2022]
|
||||||
|
|
||||||
|
- **BREAKING** Renamed `standartCurve` to `standardCurve`
|
||||||
|
- **BREAKING** Completly rework `DropDownButton`
|
||||||
|
- **BREAKING** Removed `CheckboxThemeData.thirdStateIcon`
|
||||||
|
|
||||||
|
Currently, there isn't a fluent icon that is close to the native icon. A local widget *`_ThirdStateDash`* is used
|
||||||
|
- Do not override material `Theme` on `FluentApp` ([#155](https://github.com/bdlukaa/fluent_ui/pull/154))
|
||||||
|
- Slider thumb now doesn't change inner size if hovered while disabled
|
||||||
|
- Uniform foreground color on `Checkbox`
|
||||||
|
- Updated `FilledButton` Style
|
||||||
|
- `ToggleButton` and `FilledButton` now share the same style
|
||||||
|
- `ScaffoldPage.scrollable` and `ScaffoldPage.withPadding`
|
||||||
|
- Ensure we use `Typography.body` as the default text style on `BaseButton` ([#120](https://github.com/bdlukaa/fluent_ui/issues/160))
|
||||||
|
- Update `ButtonThemeData.uncheckedInputColor`
|
||||||
|
|
||||||
|
## [3.8.0] - Flutter Favorite - [03/02/2022]
|
||||||
|
|
||||||
|
- Tests ([#142](https://github.com/bdlukaa/fluent_ui/pull/142))
|
||||||
|
- Added Material Theme to Fluent Theme Builder ([#133](https://github.com/bdlukaa/fluent_ui/issues/133))
|
||||||
|
- Add more customization options to PaneItem ([#111](https://github.com/bdlukaa/fluent_ui/issues/111), [#144](https://github.com/bdlukaa/fluent_ui/issues/144))
|
||||||
|
- `NavigationView` updates **BREAKING**:
|
||||||
|
- Properly add item key to `PaneItem` in top mode ([#143](https://github.com/bdlukaa/fluent_ui/issues/143))
|
||||||
|
- Items bounds and positions are fetched when the item list is scrolled as well to prevent misalignment
|
||||||
|
- Added the helper functions `NavigationIndicator.end` and `NavigationIndicator.sticky`
|
||||||
|
- Use `Curves.easeIn` for sticky navigation indicator by default
|
||||||
|
- Use the correct accent color for navigation indicators by default
|
||||||
|
- `EntrancePageTransition` is now the correct page transition used when display mode is top
|
||||||
|
- Apply correct press effect for `PaneItem` when display mode is top
|
||||||
|
- **BREAKING** Removed `NavigationPane.defaultNavigationIndicator`
|
||||||
|
- **BREAKING** Replaced `offsets` and `sizes` with `pane` in `NavigationPane`
|
||||||
|
|
||||||
|
Before:
|
||||||
|
```dart
|
||||||
|
pane: NavigationPane(
|
||||||
|
indicatorBuilder: ({
|
||||||
|
required BuildContext context,
|
||||||
|
/// The navigation pane corresponding to this indicator
|
||||||
|
required NavigationPane pane,
|
||||||
|
/// Corresponds to the current display mode. If top, Axis.vertical
|
||||||
|
/// is passed, otherwise Axis.vertical
|
||||||
|
Axis? axis,
|
||||||
|
/// Corresponds to the pane itself as a widget. The indicator is
|
||||||
|
/// rendered over the whole pane.
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
if (pane.selected == null) return child;
|
||||||
|
assert(debugCheckHasFluentTheme(context));
|
||||||
|
final theme = NavigationPaneThemeData.of(context);
|
||||||
|
|
||||||
|
axis??= Axis.horizontal;
|
||||||
|
|
||||||
|
return EndNavigationIndicator(
|
||||||
|
index: pane.selected,
|
||||||
|
offsets: () => pane.effectiveItems.getPaneItemsOffsets (pane.paneKey),
|
||||||
|
sizes: pane.effectiveItems.getPaneItemsSizes,
|
||||||
|
child: child,
|
||||||
|
color: theme.highlightColor,
|
||||||
|
curve: theme.animationCurve ?? Curves.linear,
|
||||||
|
axis: axis,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
Now:
|
||||||
|
```dart
|
||||||
|
pane: NavigationPane(
|
||||||
|
indicatorBuilder: ({
|
||||||
|
required BuildContext context,
|
||||||
|
/// The navigation pane corresponding to this indicator
|
||||||
|
required NavigationPane pane,
|
||||||
|
/// Corresponds to the current display mode. If top, Axis.vertical
|
||||||
|
/// is passed, otherwise Axis.vertical
|
||||||
|
required Axis axis,
|
||||||
|
/// Corresponds to the pane itself as a widget. The indicator is
|
||||||
|
/// rendered over the whole pane.
|
||||||
|
required Widget child,
|
||||||
|
}) {
|
||||||
|
if (pane.selected == null) return child;
|
||||||
|
assert(debugCheckHasFluentTheme(context));
|
||||||
|
final theme = NavigationPaneThemeData.of(context);
|
||||||
|
|
||||||
|
return EndNavigationIndicator(
|
||||||
|
index: pane.selected,
|
||||||
|
pane: pane,
|
||||||
|
child: child,
|
||||||
|
color: theme.highlightColor,
|
||||||
|
curve: theme.animationCurve ?? Curves.linear,
|
||||||
|
axis: axis,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
## [3.7.0] - Breaking changes - [21/01/2022]
|
||||||
|
|
||||||
|
- AutoSuggestBox: ([#130](https://github.com/bdlukaa/fluent_ui/pull/130))
|
||||||
|
- It gets opened automatically when it gets focus
|
||||||
|
- When an item is tapped, the cursor is positioned correctly at the end of the text
|
||||||
|
- **BREAKING** Now it's not possible to assign a type to `AutoSuggestBox`:
|
||||||
|
Before:
|
||||||
|
```dart
|
||||||
|
AutoSuggestBox<String>(...),
|
||||||
|
```
|
||||||
|
Now:
|
||||||
|
```dart
|
||||||
|
AutoSuggestBox(...),
|
||||||
|
```
|
||||||
|
- Added TextFormBox witch integrates with the Form widget. It has the ability to be validated and to show an error message.
|
||||||
|
- New FluentIcons gallery showcase in example project ([#123](https://github.com/bdlukaa/fluent_ui/issues/123))
|
||||||
|
- Updated FluentIcons as per 30/12/2021
|
||||||
|
- **BREAKING** Renamed `FluentIcons.close` to `FluentIcons.chrome_close`
|
||||||
|
- Fixed rounded corners on the ComboBox widget
|
||||||
|
- Fixed missing padding before close button on `TabView` ([#122](https://github.com/bdlukaa/fluent_ui/issues/122))
|
||||||
|
- Readded tab minimal size for `equal` and `sizeToContent` tab width behaviours ([#122](https://github.com/bdlukaa/fluent_ui/issues/122))
|
||||||
|
- `TabView`'s close button now uses `SmallIconButton`
|
||||||
|
- If a tab is partially off the view, it's scrolled until it's visible
|
||||||
|
- Fix `IconButton`'s icon size
|
||||||
|
- Update `OutlinedButton`, `FilledButton` and `TextButton` styles
|
||||||
|
- Implement lazy tree view ([#139](https://github.com/bdlukaa/fluent_ui/pull/139))
|
||||||
|
|
||||||
|
## [3.6.0] - TabView Update - [25/12/2021]
|
||||||
|
|
||||||
|
- Implement `TreeView` ([#120](https://github.com/bdlukaa/fluent_ui/pull/120))
|
||||||
|
- Fix `Tooltip.useMousePosition`
|
||||||
|
- Fix `Slider` and `RatingBar` ([#116](https://github.com/bdlukaa/fluent_ui/issues/116))
|
||||||
|
- Fix scroll buttons when there are too many tabs in `TabView` ([#92](https://github.com/bdlukaa/fluent_ui/issues/92))
|
||||||
|
- Fix button style on tab in `TabView` ([#90](https://github.com/bdlukaa/fluent_ui/issues/90))
|
||||||
|
- Added *Close on middle click* on tabs in `TabView` ([#91](https://github.com/bdlukaa/fluent_ui/issues/91))
|
||||||
|
- Added `newTabLabel`, `closeTabLabel`, `scrollTabBackward`, `scrollTabForward` to `FluentLocalizations`
|
||||||
|
- Fix `TabView`'s text when it's too long. Now it's clipped when overflow and line doesn't break
|
||||||
|
- Added `TabView.closeButtonVisibility`. Defaults to `CloseButtonVisibilityMode.always`
|
||||||
|
- Updated selected tab paint
|
||||||
|
- Added `TabView.tabWidthBehavior`. Defaults to `TabWidthBehavior.equal`
|
||||||
|
- Added `TabView.header` and `TabView.footer`
|
||||||
|
- `Slider`'s mouse cursor is now [MouseCursor.defer]
|
||||||
|
- Added `SmallIconButton`, which makes an [IconButton] small if wrapped. It's used by `TextBox`
|
||||||
|
- Added `ButtonStyle.iconSize`
|
||||||
|
- **BREAKING** `AutoSuggestBox` updates:
|
||||||
|
- Added `FluentLocalizations.noResultsFoundLabel`. "No results found" is the default text
|
||||||
|
- Removed `itemBuilder`, `sorter`, `noResultsFound`, `textBoxBuilder`, `defaultNoResultsFound` and `defaultTextBoxBuilder`
|
||||||
|
- Added `onChanged`, `trailingIcon`, `clearButtonEnabled` and `placeholder`
|
||||||
|
- `controller` is now nullable. If null, an internal controller is creted
|
||||||
|
|
||||||
|
## [3.5.2] - [17/12/2021]
|
||||||
|
|
||||||
|
- **BREAKING** Removed `ThemeData.inputMouseCursor`
|
||||||
|
- **BREAKING** Removed `cursor` from `DatePicker`, `TimePicker`, `ButtonStyle`, `CheckboxThemeData`, `RadioButtonThemeData`, `SliderThemeData`, `ToggleSwitchThemeData`, `NavigationPaneThemeData`
|
||||||
|
- Scrollbar is not longer shown if `PaneDisplayMode` is `top`
|
||||||
|
- If open the compact pane, it's not always a overlay
|
||||||
|
- Added `triggerMode` and `enableFeedback` to `Tooltip`.
|
||||||
|
- Added `Tooltip.dismissAllToolTips`
|
||||||
|
|
||||||
|
## [3.5.1] - [15/12/2021]
|
||||||
|
|
||||||
|
- Update inputs colors
|
||||||
|
- `Expander` now properly disposes its resources
|
||||||
|
- Add the `borderRadius` and `shape` attributes to the `Mica` widget
|
||||||
|
- Implement `DropDownButton` ([#85](https://github.com/bdlukaa/fluent_ui/issues/85))
|
||||||
|
|
||||||
|
## [3.5.0] - Flutter 2.8 - [09/12/2021]
|
||||||
|
|
||||||
|
- **BREAKING** Minimal Flutter version is now 2.8
|
||||||
|
- `NavigationAppBar.backgroundColor` is now applied correctly. ([#100](https://github.com/bdlukaa/fluent_ui/issues/100))
|
||||||
|
- ComboBox's Popup Acrylic can now be disabled if wrapped in a `DisableAcrylic` ([#105](https://github.com/bdlukaa/fluent_ui/issues/105))
|
||||||
|
- `NavigationPane` width can now be customizable ([#99](https://github.com/bdlukaa/fluent_ui/issues/99))
|
||||||
|
- Implement `PaneItemAction` for `NavigationPane` ([#104](https://github.com/bdlukaa/fluent_ui/issues/104))
|
||||||
|
|
||||||
|
## [3.4.1] - [08/11/2021]
|
||||||
|
|
||||||
|
- `ContentDialog` constraints can now be customizable ([#86](https://github.com/bdlukaa/fluent_ui/issues/86))
|
||||||
|
- Add possibility to disable acrylic by wrapping it in a `DisableAcrylic` ([#89](https://github.com/bdlukaa/fluent_ui/issues/89))
|
||||||
|
- Fix `onReaorder null exception` ([#88](https://github.com/bdlukaa/fluent_ui/issues/88))
|
||||||
|
- Implement `InfoBadge`
|
||||||
|
- Implement `Expander` ([#85](https://github.com/bdlukaa/fluent_ui/issues/85))
|
||||||
|
- Default `inputMouseCursor` is now `MouseCursor.defer`
|
||||||
|
- `NavigationView.contentShape` is now rendered at the foreground
|
||||||
|
|
||||||
|
## [3.4.0] - Flexibility - [22/10/2021]
|
||||||
|
|
||||||
|
- `ProgressRing` now spins on the correct direction ([#83](https://github.com/bdlukaa/fluent_ui/issues/83))
|
||||||
|
- Added the `backwards` property to `ProgressRing`
|
||||||
|
- `FluentApp.builder` now works as expected ([#84](https://github.com/bdlukaa/fluent_ui/issues/84))
|
||||||
|
- Implemented `NavigationPane.customPane`, which now gives you the ability to create custom panes for `NavigationView`
|
||||||
|
- **BREAKING** `sizes`, `offsets` and `index` parameters from `NavigationIndicatorBuilder` were replaced by `pane`
|
||||||
|
|
||||||
|
## [3.3.0] - [12/10/2021]
|
||||||
|
|
||||||
|
- Back button now isn't forced when using top navigation mode ([#74](https://github.com/bdlukaa/fluent_ui/issues/74))
|
||||||
|
- `PilButtonBar` now accept 2 items ([#66](https://github.com/bdlukaa/fluent_ui/issues/66))
|
||||||
|
- Added builder variant to `NavigationBody`.
|
||||||
|
- Fixed content bug when `AppBar` was not supplied too `NavigationView`
|
||||||
|
|
||||||
|
## [3.2.0] - Flutter 2.5.0 - [15/09/2021]
|
||||||
|
|
||||||
|
- Added missing parameters in `_FluentTextSelectionControls` methods ([#67](https://github.com/bdlukaa/fluent_ui/issues/67))
|
||||||
|
- Min Flutter version is now 2.5.0
|
||||||
|
- **EXAMPLE APP** Updated the url strategy on web.
|
||||||
|
- **EXAMPLE APP** Upgraded dependencies
|
||||||
|
- Format code according to flutter_lints
|
||||||
|
|
||||||
|
## [3.1.0] - Texts and Fixes - [25/08/2021]
|
||||||
|
|
||||||
|
- Updated Typography:
|
||||||
|
- **BREAKING** Renamed `header` -> `display`
|
||||||
|
- **BREAKING** Renamed `subHeader` -> `titleLarge`
|
||||||
|
- **BREAKING** Renamed `base` -> `bodyStrong`
|
||||||
|
- Added `bodyLarge`
|
||||||
|
- Updated font size and weight for most of the text styles
|
||||||
|
- Update `SplitButton` design
|
||||||
|
- Update `IconButton` design
|
||||||
|
- Fixed `ToggleSwitch` not showing expanded thumb mode when dragging
|
||||||
|
- **BREAKING** Remove `CheckboxListTile`, `RadioListTile` and `SwitchListTile`. Use the respective widget with the `content` property
|
||||||
|
|
||||||
|
## [3.0.0] - Windows 11 - [24/08/2021]
|
||||||
|
|
||||||
|
- Update `ToggleButton` design.
|
||||||
|
- Update `Button` design.
|
||||||
|
- Update `RadioButton` design.
|
||||||
|
- Update `ContentDialog` design.
|
||||||
|
- Update `NavigationView` design:
|
||||||
|
- **BREAKING:** Acryic is not used anymore. Consequently, `useAcrylic` method was removed.
|
||||||
|
- Implemented `Mica`, used by the new `NavigationView`
|
||||||
|
- Added support for horizontal tooltips. Set `Tooltip.displayHorizontally` to true to enable it.
|
||||||
|
- Updated Acrylic to support the web
|
||||||
|
- Update `Checkbox` design
|
||||||
|
- Update `ToggleSwitch` design
|
||||||
|
- Update `Scrollbar` design
|
||||||
|
- Update `Slider` design
|
||||||
|
- Update `InfoBar` design
|
||||||
|
- Update pickers design (`Combobox`, `DatePicker` and `TimePicker`)
|
||||||
|
|
||||||
|
## [2.2.1] - [26/06/2021]
|
||||||
|
|
||||||
|
- Implement Fluent Selection Controls for `TextBox` ([#49](https://github.com/bdlukaa/fluent_ui/pull/49))
|
||||||
|
- `Tooltip` is now displayed when focused ([#45](https://github.com/bdlukaa/fluent_ui/issues/45))
|
||||||
|
- AppBar is now displayed when minimal pane is open.
|
||||||
|
- AppBar's animation now follows the pane animation
|
||||||
|
|
||||||
|
## [2.2.0] - BREAKING CHANGES - [25/06/2021]
|
||||||
|
|
||||||
|
- **BREAKING:** Material `Icons` are not used anymore. Use `FluentIcons` instead.
|
||||||
|
- **BREAKING:** Reworked the `Acrylic` widget implementation ([#47](https://github.com/bdlukaa/fluent_ui/pull/47))
|
||||||
|
- **BREAKING:** Removed the `useAcrylic` property from `NavigationView`. Acrylic is now used by default.
|
||||||
|
- `PaneDisplayMode.compact` has now a width of 40, not 50.
|
||||||
|
- Removed `SizeTransition` from `TabView`.
|
||||||
|
|
||||||
|
## [2.1.1] - [03/06/2021]
|
||||||
|
|
||||||
|
- Option to set a default font family on the theme data (`ThemeData.fontFamily`)
|
||||||
|
- `indicatorBuilder` is correctly applied to the automatic display mode in `NavigationView`
|
||||||
|
- An overlay is open when the toggle button is pressed on the compact display mode ([#43](https://github.com/bdlukaa/fluent_ui/issues/43))
|
||||||
|
|
||||||
|
## [2.1.0] - Mobile Update - [01/06/2021]
|
||||||
|
|
||||||
|
- Implemented `BottomNavigation`
|
||||||
|
- Implemented `BottomSheet`
|
||||||
|
- Implemented `Chip`
|
||||||
|
- Implemented `Snackbar`
|
||||||
|
- Implemented `PillButtonBar`
|
||||||
|
- New buttons variations:
|
||||||
|
- `FillButton`
|
||||||
|
- `OutlinedButton`
|
||||||
|
- `TextButton`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- `PaneItem`s' `build` method is now overridable. You can know customize how the items in `NavigationView` should look like by overriding the method.
|
||||||
|
- Fixed bug that navigation indicator was not showing on the first frame
|
||||||
|
- Fixed minimal tooltip not updating when closed the overlay
|
||||||
|
- **EXAMPLE APP:** Navigation indicator is now configurable on the `Settings` page
|
||||||
|
|
||||||
|
## [2.0.3] - [28/05/2021]
|
||||||
|
|
||||||
|
- Correctly apply items positions to pane indicators, regardless of external factors, such as navigation view app bar ([#41](https://github.com/bdlukaa/fluent_ui/issues/41))
|
||||||
|
- Improved `NavigationIndicator`s performance
|
||||||
|
|
||||||
|
## [2.0.2] - [23/05/2021]
|
||||||
|
|
||||||
|
- **BREAKING CHANGES:** Reworked the theme api ([#39](https://github.com/bdlukaa/fluent_ui/pull/39)):
|
||||||
|
|
||||||
|
- Removed the theme extension (`context.theme`). Use `FluentTheme.of(context)` instead
|
||||||
|
- `ButtonState` is now a class that can receive a value. It now allows lerping between values, making `AnimatedFluentTheme` possible.
|
||||||
|
|
||||||
|
Here's an example of how to migrate your code:
|
||||||
|
|
||||||
|
_Before_: `cursor: (_) => SystemMouseCursors.click,`\
|
||||||
|
_Now_: `cursor: ButtonState.all(SystemMouseCursors.click),`
|
||||||
|
|
||||||
|
- All theme datas and `AccentColor` have now a lerp method, in order to make `AnimatedFluentTheme` possible.
|
||||||
|
- Implemented `AnimatedFluentTheme`, in order to replace `AnimateContainer`s all around the library
|
||||||
|
- Dedicated theme for each theme data ([#37](https://github.com/bdlukaa/fluent_ui/issues/37)):
|
||||||
|
- IconTheme
|
||||||
|
- ButtonTheme
|
||||||
|
- RadioButtonTheme
|
||||||
|
- CheckboxTheme
|
||||||
|
- FocusTheme
|
||||||
|
- SplitButtonTheme
|
||||||
|
- ToggleButtonTheme
|
||||||
|
- ToggleSwitchTheme
|
||||||
|
- NavigationPaneTheme
|
||||||
|
- InfoBarTheme
|
||||||
|
- TooltipTheme
|
||||||
|
- DividerTheme
|
||||||
|
- ScrollbarTheme
|
||||||
|
- `DividerThemeData` now has `verticalMargin` and `horizontalMargin` instead of an axis callback.
|
||||||
|
- Updated button colors.
|
||||||
|
- Removed `animationDuration` and `animationCurve` from theme datas (except from `NavigationPaneThemeData`).
|
||||||
|
- Renamed `copyWith` to `merge` on theme datas (except from `ThemeData`)
|
||||||
|
- Fixed typo `standart` -> `standard`
|
||||||
|
- Implement `AnimatedAcrylic`
|
||||||
|
|
||||||
|
## [2.0.1] - [21/05/2021]
|
||||||
|
|
||||||
|
- Minimal flutter version is now 2.2
|
||||||
|
- Implement `FluentScrollBehavior`, that automatically adds a scrollbar into listviews ([#35](https://github.com/bdlukaa/fluent_ui/pull/35))
|
||||||
|
- Reworked the inputs api ([#38](https://github.com/bdlukaa/fluent_ui/pull/38)):
|
||||||
|
- A input can have multiple states. Now, if the widget is focused and pressed at the same time, it doesn't lose its focused border.
|
||||||
|
- Now, the focus is not requested twice when the button is pressed, only once. This fixes a bug introduced in a previous version that combo boxes items we're not being focused.
|
||||||
|
- Semantics (acessibility) is now applied on all inputs
|
||||||
|
|
||||||
|
## [2.0.0] - [20/05/2021]
|
||||||
|
|
||||||
|
- New way to disable the acrylic blur effect. Just wrap the acrylic widget in a `NoAcrylicBlurEffect` to have it disabled.
|
||||||
|
- Reworked the Navigation Panel from scratch ([#31](https://github.com/bdlukaa/fluent_ui/pull/31)):
|
||||||
|
- The legacy `NavigationPanel` and `Scaffold` were removed. Use `NavigationView` and `ScaffoldPage` instead
|
||||||
|
- Implemented open, compact, top and minimal display modes.
|
||||||
|
- Custom Selected Indicators
|
||||||
|
- Implemented fluent localizations ([#30](https://github.com/bdlukaa/fluent_ui/issues/30))
|
||||||
|
|
||||||
|
## [1.10.1] - [05/05/2021]
|
||||||
|
|
||||||
|
- **FIX** Reworked the combo box widget to improve fidelity. ([#25](https://github.com/bdlukaa/fluent_ui/pull/25))
|
||||||
|
- **FIX** Improved `HoverButton` focus management.
|
||||||
|
- **FIX** Reworked the tooltip widget. Now, if any mouse is connected, the tooltip is displaying according to the pointer position, not to the child's. ([#26](https://github.com/bdlukaa/fluent_ui/pull/26))
|
||||||
|
- **FIX** TabView is now scrollable if the size of the tabs overflow the width
|
||||||
|
|
||||||
|
## [1.10.0] - **BREAKING CHANGES** - [03/05/2021]
|
||||||
|
|
||||||
|
- **BREAKING** `InfoHeader` was renamed to `InfoLabel`. You can now set if the label will be rendered above the child or on the side of it.
|
||||||
|
- **FIX** Fixed `RadioButton` inner color overlaping the border.
|
||||||
|
- **NEW** `ThemeData.inputMouseCursor`
|
||||||
|
- **FIDELITY** Switch thumb is now draggable. (Fixes [#22](https://github.com/bdlukaa/fluent_ui/issues/22))
|
||||||
|
- **EXAMPLE** Reworked the example app inputs page
|
||||||
|
|
||||||
|
## [1.9.4] - [02/05/2021]
|
||||||
|
|
||||||
|
- **FIX** `CheckboxListTile`, `SwitchListTile` and `RadioListTile` now doesn't focus its leading widget.
|
||||||
|
- **FIX** `TabView` is now not scrollable
|
||||||
|
- **FIX** Fixed `Acrylic` blur effect being disabled by default.
|
||||||
|
- **FIDELITY** Improved `ContentDialog` transition fidelity
|
||||||
|
- **FIX** Fixed `FocusBorder` for some widgets. It was affecting layout when it shouldn't
|
||||||
|
- **FIX** `RatingBar` and `Slider` weren't working due to `FocusBorder`
|
||||||
|
- **NEW** | **FIDELITY** New `Slider` thumb
|
||||||
|
|
||||||
|
## [1.9.3] - [01/05/2021]
|
||||||
|
|
||||||
|
- **NEW** `FocusBorder.renderOutside`. With this property, you can control if the FocusBorder will be rendered over the widget or outside of it.
|
||||||
|
- **FIX** Fixed `RadioButton`s border when focused
|
||||||
|
- **FIX** `Color.resolve` now doesn't throw a stack overflow error.
|
||||||
|
- **BREAKING** Removed `Color.resolveFromBrightness`. This is only available on `AccentColor`
|
||||||
|
- **EXAMPLE APP** Hability to change the app accent color
|
||||||
|
- **NEW** `darkest` and `lightest` colors variants in `AccentColor`
|
||||||
|
- **FIX** Fixed `InfoBar`'s error icon. It now uses `Icons.cancel_outlined` instead of `Icons.close`
|
||||||
|
- **NEW** `NavigationPanel` now has a `Scrollbar` and the `bottom` property is now properly styled if selected
|
||||||
|
|
||||||
|
## [1.9.2] - [30/04/2021]
|
||||||
|
|
||||||
|
- **FIX** `TabView` tabs can now be reordered (Fixes [#10](https://github.com/bdlukaa/fluent_ui/issues/10))
|
||||||
|
- **FIDELITY** If a new `Tab` is added, its now animated
|
||||||
|
- **FIX** `FocusBorder` now doesn't change the size of the widgets
|
||||||
|
- **BREAKING** `buttonCursor`, `uncheckedInputColor` and `checkedInputColor` are now moved to `ButtonThemeData` as static functions.
|
||||||
|
|
||||||
|
## [1.9.1] - [29/04/2021]
|
||||||
|
|
||||||
|
- **FIX** Fixed diagnostic tree. (Fixes [#17](https://github.com/bdlukaa/fluent_ui/issues/17))
|
||||||
|
- **FIX** | **FIDELITY** `TappableListTile` now changes its color when focused instead of having a border
|
||||||
|
- **FIDELITY** Improved `Acrylic`'s blur effect fidelity
|
||||||
|
- **FIX** `Acrylic`'s elevation was being applying margin
|
||||||
|
- **NEW** `ThemeData.shadowColor`, which is now used by `Acrylic`
|
||||||
|
- **NEW** You can now globally disable the acrylic blur effect by changing `Acrylic.acrylicEnabled`
|
||||||
|
|
||||||
|
## [1.9.0] - **BREAKING CHANGES** - Theme Update - [29/04/2021]
|
||||||
|
|
||||||
|
The whole theme implementation was reworked on this change.
|
||||||
|
|
||||||
|
- **BREAKING** Renamed `Theme` to `FluentTheme`
|
||||||
|
- **BREAKING** All the properties in `FluentTheme` now can't be null
|
||||||
|
- **BREAKING** Renamed all the `Style` occurrences to `ThemeData`
|
||||||
|
- **BREAKING** `ThemeData.accentColor` is now an `AccentColor`
|
||||||
|
- **FIX** When providing a custom style to a tooltip, it's now correctly applied to `ThemeData.tooltipStyle`
|
||||||
|
- **FIX** `debugCheckHasFluentTheme` has now a better error message
|
||||||
|
- **FIX** `FluentApp` now doesn't throw an error if no `theme` is provided
|
||||||
|
- **FIX** Reworked `Scrollbar` to improve fidelity.
|
||||||
|
- **NEW** Color extension methods: `Color.toAccentColor` and `Color.basedOnLuminance`
|
||||||
|
- **NEW** `Button.builder`
|
||||||
|
|
||||||
|
## [1.8.1] - [16/04/2021]
|
||||||
|
|
||||||
|
- **NEW** In `TabView`, it's now possible use the following shortcuts if `TabView.shortcutsEnabled` is `true` (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/tab-view#closing-a-tab)):
|
||||||
|
1. `Ctrl + F4` or `Ctrl + W` to close the current tab
|
||||||
|
2. `Ctrl + T` to create a new tab
|
||||||
|
3. `1-8` to navigate to a tab with the pressed number
|
||||||
|
4. `9` to navigate to the last tab and navigate to the last tab
|
||||||
|
- **NEW** `IconButton.autofocus`, `ToggleButton.autofocus`
|
||||||
|
- **BREAKING** Renamed all the `semanticsLabel` to `semanticLabel`
|
||||||
|
|
||||||
|
## [1.8.0] - Color Update - [14/04/2021]
|
||||||
|
|
||||||
|
- **NEW** Web version hosted at https://bdlukaa.github.io/fluent_ui
|
||||||
|
- **NEW** Colors showcase page in example app
|
||||||
|
- **NEW** Info Colors:
|
||||||
|
- `Colors.warningPrimaryColor`
|
||||||
|
- `Colors.warningSecondaryColor`
|
||||||
|
- `Colors.errorPrimaryColor`
|
||||||
|
- `Colors.errorSecondaryColor`
|
||||||
|
- `Colors.successPrimaryColor`
|
||||||
|
- `Colors.successSecondaryColor`
|
||||||
|
- **FIX** Reworked all the accent colors (`Colors.accentColors`) with `darkest`, `dark`, `normal`, `light` and `lighter`
|
||||||
|
- **BREAKING** `Colors.blue` is now an `AccentColor`
|
||||||
|
|
||||||
|
## [1.7.6] - [13/04/2021]
|
||||||
|
|
||||||
|
- **NEW** `Checkbox.autofocus`
|
||||||
|
- **BREAKING** `Button` refactor:
|
||||||
|
- Removed `Button.icon` and `Button.trailingIcon`
|
||||||
|
- Renamed `Button.text` to `Button.child`
|
||||||
|
- You can now disable the acrylic backdrop effect by setting `enabled` to false
|
||||||
|
- **NEW** `NavigationPanelBody.animationCurve` and `NavigationPanelBody.animationDuration`
|
||||||
|
|
||||||
|
## [1.7.5] - [13/04/2021]
|
||||||
|
|
||||||
|
- **NEW** `Scrollbar` and `ScrollbarStyle`
|
||||||
|
- Reworked `FluentApp` to not depend of material anymore.
|
||||||
|
|
||||||
|
## [1.7.4] - [10/04/2021]
|
||||||
|
|
||||||
|
- **FIX** Updated `Icon` widget to use Flutter's default icon widget
|
||||||
|
- **NEW** Documentation
|
||||||
|
|
||||||
|
## [1.7.3] - [07/04/2021]
|
||||||
|
|
||||||
|
- **FIX** Improved `ListTile` sizing ([#Spacing](https://docs.microsoft.com/en-us/windows/uwp/design/style/spacing))
|
||||||
|
- **NEW** `FocusStyle` and support for glow focus
|
||||||
|
- **NEW** `RatingBar.starSpacing`
|
||||||
|
|
||||||
|
## [1.7.2] - [06/04/2021]
|
||||||
|
|
||||||
|
- **FIX** Animation when using `NavigationPanelBody` now works as expected
|
||||||
|
- **NEW** `CheckboxListTile`, `SwitchListTile` and `RadioListTile`
|
||||||
|
- **FIX** It's now not possible to focus a disabled `TextBox`
|
||||||
|
|
||||||
|
## [1.7.1] - [06/04/2021]
|
||||||
|
|
||||||
|
- **FIX** The mouse cursor in a disabled input is now `basic` instead of `forbidden`
|
||||||
|
- **FIX** `NavigationPanelBody` now doesn't use a `IndexedStack` under the hood because it was interfering in the focus scope
|
||||||
|
- **FIX** The color of the focus now is the `Style.inactiveColor`
|
||||||
|
- **FIX** `RadioButton`'s cursor was not being applied correctly
|
||||||
|
- **NEW** `Button.toggle`
|
||||||
|
- **FIX** The state provided by `HoverButton` was being `focused` when it shouldn't be
|
||||||
|
- **FIX** TimePicker showing wrong minute count. It should start from 00 and end in 59
|
||||||
|
- **NEW** `TimePicker.minuteIncrement`
|
||||||
|
|
||||||
|
## [1.7.0] - Focus Update - [05/04/2021]
|
||||||
|
|
||||||
|
- **FIXED** Fixed the possibility to give a elevation lower than 0 in `Acrylic`
|
||||||
|
- **NEW** It's now possible to change the rating of `RatingBar` using the keyboard arrows
|
||||||
|
- **NEW** Now it's possible to navigate using the keyboard with all focusable widgets
|
||||||
|
|
||||||
|
## [1.6.0] - BREAKING CHANGES - [03/04/2021]
|
||||||
|
|
||||||
|
- Added the missing `Diagnostics`
|
||||||
|
- Updated all the screenshots
|
||||||
|
- **BREAKING CHANGE** Uses the material icon library now
|
||||||
|
|
||||||
|
**DEVELOPER NOTE** This was a hard choice, but the material icon library is a robust, bigger library. It contains all the icons the previous library has, and a few many more.
|
||||||
|
|
||||||
|
## [1.5.0] - [02/04/2021]
|
||||||
|
|
||||||
|
- Added `Diagnostics` to many widgets
|
||||||
|
- **NEW** `AutoSuggestBox` (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/auto-suggest-box))
|
||||||
|
- **NEW** `Flyout` and `FlyoutContent` (Folllows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/dialogs-and-flyouts/flyouts))
|
||||||
|
- **FIXED** Popup was being shown off-screen.
|
||||||
|
|
||||||
|
**DEVELOPER NOTE** The solution for this was to make it act like a tooltip: only show the popup above or under the `child`. This was a hard choice, but the only viable option that would work on small screens/devices. This also made `Flyout` easier to implement. This should be changed when multi-window support is available.
|
||||||
|
|
||||||
|
- **FIXED** `DatePicker` incorrectly changing hour
|
||||||
|
- **NEW** `Colors.accentColors`
|
||||||
|
- Documentation about [system_theme](https://pub.dev/packages/system_theme)
|
||||||
|
- **BREAKING** Removed `Pivot` because it's deprecated
|
||||||
|
|
||||||
|
## [1.4.1] - Pickers Update - [30/03/2021]
|
||||||
|
|
||||||
|
- **NEW** `Style.fasterAnimationDuration`
|
||||||
|
- **FIX** `ComboBox` press effect
|
||||||
|
- **NEW** `TimePicker` (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/time-picker))
|
||||||
|
- **NEW** `DatePicker` (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/date-picker))
|
||||||
|
|
||||||
|
## [1.4.0] - [28/03/2021]
|
||||||
|
|
||||||
|
- **NEW** `InfoHeader`
|
||||||
|
- **NEW** `ComboBox` (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/checkbox))
|
||||||
|
- **NEW** `TappableListTile`
|
||||||
|
- **BREAKING** Removed `DropdownButton` and `Button.dropdown`
|
||||||
|
|
||||||
|
## [1.3.4] - [28/03/2021]
|
||||||
|
|
||||||
|
- **NEW** Vertical Slider
|
||||||
|
|
||||||
|
## [1.3.3] - [25/03/2021]
|
||||||
|
|
||||||
|
- **NEW** Indeterminate `ProgressRing` ([@raitonoberu](https://github.com/raitonoberu))
|
||||||
|
- **NEW** `ListTile`
|
||||||
|
- **DIAGNOSTICS** Provide `Diagnostics` support to:
|
||||||
|
- `Style`
|
||||||
|
- `NavigationPanelStyle`
|
||||||
|
- `TooltipStyle`
|
||||||
|
|
||||||
|
## [1.3.2] - Accessibility update - [24/03/2021]
|
||||||
|
|
||||||
|
This version provides the fix for [#5](https://github.com/bdlukaa/fluent_ui/issues/5)
|
||||||
|
|
||||||
|
- `Theme.of` can't be null anymore. Use `Theme.maybeOf` for such
|
||||||
|
- **NEW** `Style.inactiveBackgroundColor`
|
||||||
|
- **BREAKING** Replaced `color`, `border`, `borderRadius` from `IconButtonStyle` to `decoration`
|
||||||
|
- **DIAGNOSTICS** Provide `Diagnostics` support to the following classes:
|
||||||
|
- ButtonStyle
|
||||||
|
- Checkbox
|
||||||
|
- CheckboxStyle
|
||||||
|
- IconButtonStyle
|
||||||
|
- RadioButtonStyle
|
||||||
|
- RatingBar
|
||||||
|
- SplitButtonStyle
|
||||||
|
- ToggleButton
|
||||||
|
- ToggleButtonStyle
|
||||||
|
- ToggleSwitch
|
||||||
|
- ToggleSwitchStyle
|
||||||
|
- Slider
|
||||||
|
- SliderStyle
|
||||||
|
- Typography
|
||||||
|
- Divider
|
||||||
|
- DividerStyle
|
||||||
|
- Provide accessibility support to the following widgets:
|
||||||
|
- Button
|
||||||
|
- Checkbox
|
||||||
|
- IconButton
|
||||||
|
- RadioButton
|
||||||
|
- RatingBar
|
||||||
|
- Slider
|
||||||
|
- ToggleButton
|
||||||
|
- ToggleSwitch
|
||||||
|
- TabView
|
||||||
|
|
||||||
|
## [1.3.1] - [23/03/2021]
|
||||||
|
|
||||||
|
- **FIX** `IconButtonStyle`'s `iconStyle` now works properly
|
||||||
|
- Improved `TabView` icon styling
|
||||||
|
- **NEW** Indeterminate `ProgressBar` ([@raitonoberu](https://github.com/raitonoberu))
|
||||||
|
|
||||||
|
## [1.3.0] - [22/03/2021]
|
||||||
|
|
||||||
|
- **NEW** Determinate `ProgressBar` and `ProgressRing`
|
||||||
|
- **NEW** `TabView` ([#TabView](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/tab-view))
|
||||||
|
|
||||||
|
## [1.2.5] - [21/03/2021]
|
||||||
|
|
||||||
|
- **FIX** Fixed `InfoBar`'s overflow
|
||||||
|
|
||||||
|
## [1.2.4] - [21/03/2021]
|
||||||
|
|
||||||
|
- **BREAKING** `RadioButton`'s `selected` property was renamed to `checked` to match a single pattern between all the other widgets.
|
||||||
|
|
||||||
|
## [1.2.3] - [19/03/2021]
|
||||||
|
|
||||||
|
- **NEW** | **EXAMPLE APP** `Settings` screen
|
||||||
|
- Improved theme changing
|
||||||
|
- **FIX** `FluentApp` doesn't lose its state anymore, possibiliting hot relaod.
|
||||||
|
- **NEW** `showDialog` rework:
|
||||||
|
- `showDialog` now can return data. (Fixes [#2](https://github.com/bdlukaa/fluent_ui/issues/2))
|
||||||
|
- `showDialog.transitionBuilder`
|
||||||
|
- `showDialog.useRootNavigator`
|
||||||
|
- `showDialog.routeSettings`
|
||||||
|
- It's no longer necessary to have the fluent theme to display dialogs using this function.
|
||||||
|
|
||||||
|
## [1.2.2] - [17/03/2021]
|
||||||
|
|
||||||
|
- **BREAKING** Removed `_regular` from the name of the icons.
|
||||||
|
- **NEW** `InfoBar` (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/infobar))
|
||||||
|
|
||||||
|
## [1.2.1] - [16/03/2021]
|
||||||
|
|
||||||
|
- **NEW** `Divider`
|
||||||
|
|
||||||
|
## [1.2.0] - Timing and easing - Page transitioning - [15/03/2021]
|
||||||
|
|
||||||
|
- **FIDELITY** Improved `ToggleButton` fidelity
|
||||||
|
- **NEW** `NavigationPanelBody`
|
||||||
|
- **NEW** Page transitions
|
||||||
|
- `EntrancePageTransition` ([#PageRefresh](https://docs.microsoft.com/en-us/windows/uwp/design/motion/page-transitions#page-refresh))
|
||||||
|
- `DrillInPageTransition` ([#Drill](https://docs.microsoft.com/en-us/windows/uwp/design/motion/page-transitions#drill))
|
||||||
|
- `HorizontalSlidePageTransition` ([#HorizontalSlide](https://docs.microsoft.com/en-us/windows/uwp/design/motion/page-transitions#horizontal-slide))
|
||||||
|
- `SuppressPageTransition` ([#Supress](https://docs.microsoft.com/en-us/windows/uwp/design/motion/page-transitions#suppress))
|
||||||
|
- Add timing and easing to style. (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/motion/timing-and-easing))
|
||||||
|
- **NEW** `Style.fastAnimationDuration` (Defaults to 150ms)
|
||||||
|
- **NEW** `Style.mediumAnimationDuration` (Defaults to 300ms)
|
||||||
|
- **NEW** `Style.slowAnimationDuration` (Defaults to 500ms)
|
||||||
|
- Default `animationCurve` is now `Curves.easeInOut` (standard) instead of `Curves.linear`
|
||||||
|
- **BREAKING** Removed `Style.animationDuration`
|
||||||
|
- Refactored Navigation Panel
|
||||||
|
|
||||||
|
## [1.1.0] - Fidelity update - [14/03/2021]
|
||||||
|
|
||||||
|
- **BREAKING** Removed `Card` widget. Use `Acrylic` instead
|
||||||
|
- **NEW** `Acrylic` widget ([#Acrylic](https://docs.microsoft.com/en-us/windows/uwp/design/style/acrylic))
|
||||||
|
- **NEW** **NAVIGATION PANEL** `bottom` property
|
||||||
|
- **FIDELITY** Improved the corder radius of some widgets (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/style/rounded-corner#page-or-app-wide-cornerradius-changes))
|
||||||
|
- **FIX** **FIDELITY** Dark theme hovering color
|
||||||
|
- Improved documentation
|
||||||
|
|
||||||
|
## [1.0.2] - Typography update - [11/03/2021]
|
||||||
|
|
||||||
|
- **NEW** Typography
|
||||||
|
- Migrated all the widgets to use typography
|
||||||
|
- **NEW** Tooltip
|
||||||
|
- **NEW** Dark theme
|
||||||
|
- **FIX** Disabled button press effect if disabled
|
||||||
|
- **FIX** Grey color resulting in green color
|
||||||
|
|
||||||
|
## [1.0.1+1] - [09/03/2021]
|
||||||
|
|
||||||
|
- **NEW** Screenshots
|
||||||
|
|
||||||
|
## [1.0.1] - [07/03/2021]
|
||||||
|
|
||||||
|
- **FIX** `NavigationPanel` navigation index
|
||||||
|
- **FIX** `Slider`'s inactive color
|
||||||
|
- **FIDELITY** Scale animation of button press
|
||||||
|
- **FIDELITY** Improved `Slider` label fidelity
|
||||||
|
- **NEW** Split Button
|
||||||
|
|
||||||
|
## [1.0.0] - [05/03/2021]
|
||||||
|
|
||||||
|
- **NEW** Null-safety
|
||||||
|
- **NEW** New Icons Library
|
||||||
|
- **NEW** `NavigationPanelSectionHeader` and `NavigationPanelTileSeparator`
|
||||||
|
- **BREAKING** Removed `Snackbar`
|
||||||
|
|
||||||
|
## [0.0.9] - [03/03/2021]
|
||||||
|
|
||||||
|
- Export the icons library
|
||||||
|
- **NEW** `TextBox`
|
||||||
|
|
||||||
|
## [0.0.8] - [01/03/2021]
|
||||||
|
|
||||||
|
- **NEW** `ContentDialog` 🎉
|
||||||
|
- **NEW** `RatingControl` 🎉
|
||||||
|
- **NEW** `NavigationPanel` 🎉
|
||||||
|
- Improved `Button` fidelity
|
||||||
|
|
||||||
|
## [0.0.7] - [28/02/2021]
|
||||||
|
|
||||||
|
- **NEW** `Slider` 🎉
|
||||||
|
- Use physical model for elevation instead of box shadows
|
||||||
|
- Improved TODO
|
||||||
|
|
||||||
|
## [0.0.6] - [27/02/2021]
|
||||||
|
|
||||||
|
- **FIXED** Button now detect pressing
|
||||||
|
- **FIXED** `ToggleSwitch` default thumb is now animated
|
||||||
|
- **FIXED** Improved `ToggleSwitch` fidelity
|
||||||
|
**FIXED** Darker color for button press.
|
||||||
|
- **NEW** **THEMING**
|
||||||
|
- `Style.activeColor`
|
||||||
|
- `Style.inactiveColor`
|
||||||
|
- `Style.disabledColor`
|
||||||
|
- `Style.animationDuration`
|
||||||
|
- `Style.animationCurve`
|
||||||
|
|
||||||
|
## [0.0.5] - [27/02/2021]
|
||||||
|
|
||||||
|
- `ToggleSwitch` is now stable 🎉
|
||||||
|
- **NEW** `DefaultToggleSwitchThumb`
|
||||||
|
- **NEW** `ToggleButton`
|
||||||
|
- New toast lib: [fl_toast](https://pub.dev/packages/fl_toast)
|
||||||
|
- Screenshot on the readme. (Fixes [#1](https://github.com/bdlukaa/fluent_ui/issues/1))
|
||||||
|
|
||||||
|
## [0.0.4] - [22/02/2021]
|
||||||
|
|
||||||
|
- New fluent icons library: [fluentui_icons](https://pub.dev/packages/fluentui_icons)
|
||||||
|
- Re-made checkbox with more fidelity
|
||||||
|
- Refactored the following widgets to follow the theme accent color:
|
||||||
|
- `Checkbox`
|
||||||
|
- `ToggleSwitch`
|
||||||
|
- `RadioButton`
|
||||||
|
- Added accent colors to widget. Use [this](https://docs.microsoft.com/en-us/windows/uwp/design/style/images/color/windows-controls.svg) as a base
|
||||||
|
|
||||||
|
## [0.0.3] - Theming update - [21/02/2021]
|
||||||
|
|
||||||
|
- **HIGHLIGHT** A whole new [documentation](https://github.com/bdlukaa/fluent_ui/wiki)
|
||||||
|
- Scaffold now works as expected.
|
||||||
|
- Improved theming checking
|
||||||
|
- **NEW**
|
||||||
|
- `null` (thirdstate) design on `Checkbox`. (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/checkbox))
|
||||||
|
- Now you can use the `Decoration` to style the inputs
|
||||||
|
- **BREAKING**:
|
||||||
|
- Removed `Button.action`
|
||||||
|
- Removed `Button.compound`
|
||||||
|
- Removed `Button.primary`
|
||||||
|
- Removed `Button.contextual`
|
||||||
|
- Removed `AppBar`
|
||||||
|
- Now the default theme uses accent color instead of a predefined color (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/style/color#accent-color))
|
||||||
|
- **FIXED**:
|
||||||
|
- `ToggleSwitch` can NOT receive null values
|
||||||
|
|
||||||
|
## [0.0.2] - [18/02/2021]
|
||||||
|
|
||||||
|
- The whole library was rewritten following [this](https://docs.microsoft.com/en-us/windows/uwp/design/)
|
||||||
|
- Tooltip's background color is now opaque (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/tooltips))
|
||||||
|
- Dropdown button now works as expected
|
||||||
|
- **FIXED**:
|
||||||
|
- Snackbar now is dismissed even if pressing or hovering
|
||||||
|
- Margin is no longer used as part of the clickable button
|
||||||
|
- **BREAKING**:
|
||||||
|
- Renamed `Toggle` to `ToggleSwitch` (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/toggles))
|
||||||
|
- Removed `BottomNavigationBar`. It's recommended to use top navigation (pivots)
|
||||||
|
- Removed `IconButton.menu`
|
||||||
|
- **NEW**:
|
||||||
|
- NavigationPanel (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/layout/page-layout#left-nav))
|
||||||
|
- Windows project on example
|
||||||
|
- RadioButton (Follows [this](https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/radio-button))
|
||||||
|
|
||||||
|
## [0.0.1]
|
||||||
|
|
||||||
|
- Initial release
|
||||||
11
dependencies/fluent_ui-3.12.0/LICENSE
vendored
Normal file
11
dependencies/fluent_ui-3.12.0/LICENSE
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Copyright 2020 Bruno D'Luka
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
1807
dependencies/fluent_ui-3.12.0/README.md
vendored
Normal file
1807
dependencies/fluent_ui-3.12.0/README.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5
dependencies/fluent_ui-3.12.0/analysis_options.yaml
vendored
Normal file
5
dependencies/fluent_ui-3.12.0/analysis_options.yaml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
rules:
|
||||||
|
library_private_types_in_public_api: false
|
||||||
BIN
dependencies/fluent_ui-3.12.0/assets/AcrylicNoise.png
vendored
Normal file
BIN
dependencies/fluent_ui-3.12.0/assets/AcrylicNoise.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
7664
dependencies/fluent_ui-3.12.0/coverage/lcov.info
vendored
Normal file
7664
dependencies/fluent_ui-3.12.0/coverage/lcov.info
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
dependencies/fluent_ui-3.12.0/fonts/FluentIcons.ttf
vendored
Normal file
BIN
dependencies/fluent_ui-3.12.0/fonts/FluentIcons.ttf
vendored
Normal file
Binary file not shown.
BIN
dependencies/fluent_ui-3.12.0/images/example-showcase.png
vendored
Normal file
BIN
dependencies/fluent_ui-3.12.0/images/example-showcase.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
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();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user