diff --git a/CHANGELOG.md b/CHANGELOG.md index 45aed06..57ffe52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +v0.0.12 / 2023-01-03 +=================== +Widgets: + * Rebuilt Checkbox, RadioButton, Switch and Slider based on CustomPainter + * Added a ContextMenu widget + * Added new Animations to some components + * Simplified ZenitIconButton + * Added ZenitCheckboxListTile and ZenitRadioButtonListTile and improved ZenitSwitchListTile + * Add hover effect to components + * New naming scheme for buttons (Primary, Secondary, Text and "Button" - made to be customized) + * Added ZenitSection (should be used instead of Card) + * Added Zenit(Window)Toolbar and ZenitWindowButtons + * Added ZenitDialog widget + +Theme: + * Adjusted the theme colours + * Added copyWith to every theme class of zenit widgets + * new extension on Color to add some features + * Changed the default font to Inter + * Added a custom text theme more suited for desktop applications + * Changed FAB to be Rectangular and reduced it's size + * Add slight outline to most components + +Windowing: + * Added HandyWindow and YaruWindow + * Added basic window management functionality + v0.0.11 / 2023-07-19 =================== diff --git a/example/assets/dahliaos_banner_dark.png b/example/assets/dahliaos_banner_dark.png new file mode 100644 index 0000000..11e100d Binary files /dev/null and b/example/assets/dahliaos_banner_dark.png differ diff --git a/example/assets/dahliaos_banner_light.png b/example/assets/dahliaos_banner_light.png new file mode 100644 index 0000000..07f5f9c Binary files /dev/null and b/example/assets/dahliaos_banner_light.png differ diff --git a/example/lib/main.dart b/example/lib/main.dart index b07adec..e18fe07 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,24 +1,25 @@ -import 'package:flutter/material.dart'; import 'package:zenit_ui/zenit_ui.dart'; import 'package:zenit_ui_example/pages.dart'; void main() async { // Check for repainting //debugRepaintRainbowEnabled = true; - - runApp(const MyApp()); + await ZenitWindowTitlebar.ensureInitialized(); + WidgetsFlutterBinding.ensureInitialized(); + runApp(const ExampleApp()); } -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); +class ExampleApp extends StatefulWidget { + const ExampleApp({Key? key}) : super(key: key); @override - State createState() => _MyAppState(); + State createState() => _ExampleAppState(); } -class _MyAppState extends State { +class _ExampleAppState extends State { int themeMode = 0; Set themeModes = {ThemeMode.system, ThemeMode.light, ThemeMode.dark}; + @override Widget build(BuildContext context) { return MaterialApp( @@ -27,6 +28,17 @@ class _MyAppState extends State { theme: createZenitTheme(brightness: Brightness.light), darkTheme: createZenitTheme(brightness: Brightness.dark), home: ZenitNavigationLayout( + titlebar: const ZenitWindowTitlebar(), + sidebarWidth: 280, + sidebarToolbar: const ZenitToolbar( + title: ZenitWindowTitle(fallback: "ZenitUI Example"), + backgroundColor: Colors.transparent, + ), + pageToolbarBuilder: (context, index) => ZenitToolbar( + height: 48, + title: examplePages[index].titleBuilder(context), + backgroundColor: Colors.transparent, + ), length: examplePages.length, destinationBuilder: (context, index, selected) => ZenitLayoutTile( title: examplePages[index].titleBuilder(context), @@ -34,10 +46,10 @@ class _MyAppState extends State { selected: selected, ), pageBuilder: (context, index) => examplePages[index].pageBuilder(context), - globalFloatingActionButton: FloatingActionButton.extended( + globalFloatingActionButton: FloatingActionButton( onPressed: () => setState(() => (themeMode < themeModes.length - 1) ? themeMode++ : themeMode = 0), - label: Text(resolveThemeName()), - icon: Icon(resolveThemeIcon()), + tooltip: resolveThemeName(), + child: Icon(resolveThemeIcon()), ), ), ); diff --git a/example/lib/pages.dart b/example/lib/pages.dart index 75862ee..61d6fb7 100644 --- a/example/lib/pages.dart +++ b/example/lib/pages.dart @@ -1,38 +1,54 @@ -import 'package:flutter/material.dart'; import 'package:zenit_ui/zenit_ui.dart'; +import 'package:zenit_ui_example/pages/button.dart'; import 'package:zenit_ui_example/pages/checkbox.dart'; +import 'package:zenit_ui_example/pages/color_scheme.dart'; +import 'package:zenit_ui_example/pages/context_menu.dart'; +import 'package:zenit_ui_example/pages/dialog.dart'; import 'package:zenit_ui_example/pages/icon_button.dart'; import 'package:zenit_ui_example/pages/list_titles.dart'; import 'package:zenit_ui_example/pages/radio_button.dart'; +import 'package:zenit_ui_example/pages/slider.dart'; +import 'package:zenit_ui_example/pages/sub_pages.dart'; import 'package:zenit_ui_example/pages/switch.dart'; import 'package:zenit_ui_example/pages/tab_view.dart'; import 'package:zenit_ui_example/pages/text.dart'; -import 'package:zenit_ui_example/pages/zenit_components.dart'; +import 'package:zenit_ui_example/pages/text_field.dart'; +import 'package:zenit_ui_example/pages/welcome.dart'; final examplePages = [ ZenitLayoutItem( titleBuilder: (context) => const Text("Welcome"), - pageBuilder: (context) => const ZenitComponentsExample(), + pageBuilder: (context) => const ZenitWelcome(), iconBuilder: (context, selected) => Icon(selected ? Icons.info_rounded : Icons.info_outline_rounded), ), ZenitLayoutItem( - titleBuilder: (context) => const Text("ZenitSwitch"), + titleBuilder: (context) => const Text("Button"), + pageBuilder: (context) => const ZenitButtonExample(), + iconBuilder: (context, selected) => Icon(selected ? Icons.touch_app_rounded : Icons.touch_app_outlined), + ), + ZenitLayoutItem( + titleBuilder: (context) => const Text("Switch"), pageBuilder: (context) => const ZenitSwitchExample(), iconBuilder: (context, selected) => Icon(selected ? Icons.toggle_on_rounded : Icons.toggle_off_rounded), ), ZenitLayoutItem( - titleBuilder: (context) => const Text("ZenitCheckbox"), + titleBuilder: (context) => const Text("Checkbox"), pageBuilder: (context) => const ZenitCheckboxExample(), iconBuilder: (context, selected) => Icon(selected ? Icons.check_box_rounded : Icons.check_box_outlined), ), ZenitLayoutItem( - titleBuilder: (context) => const Text("ZenitRadioButton"), + titleBuilder: (context) => const Text("RadioButton"), pageBuilder: (context) => const ZenitRadioButtonExample(), iconBuilder: (context, selected) => Icon(selected ? Icons.radio_button_checked_rounded : Icons.radio_button_off_rounded), ), ZenitLayoutItem( - titleBuilder: (context) => const Text("ZenitIconButton"), + titleBuilder: (context) => const Text("TextField"), + pageBuilder: (context) => const ZenitTextFieldExample(), + iconBuilder: (context, selected) => const Icon(Icons.text_format_rounded), + ), + ZenitLayoutItem( + titleBuilder: (context) => const Text("IconButton"), pageBuilder: (context) => const ZenitIconButtonExample(), iconBuilder: (context, selected) => Icon(selected ? Icons.emoji_emotions_rounded : Icons.emoji_emotions_outlined), @@ -40,7 +56,15 @@ final examplePages = [ ZenitLayoutItem( titleBuilder: (context) => const Text("Tab View"), pageBuilder: (context) => const ZenitTabViewExample(), - iconBuilder: (context, selected) => Icon(selected ? Icons.tab_rounded : Icons.tab_unselected_rounded), + iconBuilder: (context, selected) => Transform.flip( + flipX: true, + child: Icon(selected ? Icons.tab_rounded : Icons.tab_unselected_rounded), + ), + ), + ZenitLayoutItem( + titleBuilder: (context) => const Text("Context Menu"), + pageBuilder: (context) => const ZenitContextMenuExample(), + iconBuilder: (context, selected) => Icon(selected ? Icons.menu_open_rounded : Icons.menu_rounded), ), ZenitLayoutItem( titleBuilder: (context) => const Text("List Tiles"), @@ -53,4 +77,25 @@ final examplePages = [ iconBuilder: (context, selected) => Icon(selected ? Icons.text_fields_rounded : Icons.text_fields_outlined), ), + ZenitLayoutItem( + titleBuilder: (context) => const Text("Colors"), + pageBuilder: (context) => const ColorSchemeExamplePage(), + iconBuilder: (context, selected) => Icon(selected ? Icons.color_lens_rounded : Icons.color_lens_outlined), + ), + ZenitLayoutItem( + titleBuilder: (context) => const Text("Sub pages"), + pageBuilder: (context) => const SubPagesExample(), + iconBuilder: (context, selected) => const Icon(Icons.subtitles_rounded), + ), + ZenitLayoutItem( + titleBuilder: (context) => const Text("Dialog"), + pageBuilder: (context) => const DialogExample(), + iconBuilder: (context, selected) => const Icon(Icons.smart_display_outlined), + ), + ZenitLayoutItem( + titleBuilder: (context) => const Text("Slider"), + pageBuilder: (context) => const SliderExample(), + iconBuilder: (contex, selected) => + Icon(selected ? Icons.linear_scale_rounded : Icons.linear_scale_outlined), + ) ]; diff --git a/example/lib/pages/button.dart b/example/lib/pages/button.dart new file mode 100644 index 0000000..e3977ba --- /dev/null +++ b/example/lib/pages/button.dart @@ -0,0 +1,116 @@ +import 'package:gap/gap.dart'; +import 'package:zenit_ui/zenit_ui.dart'; +import 'package:zenit_ui_example/title.dart'; + +class ZenitButtonExample extends StatefulWidget { + const ZenitButtonExample({super.key}); + + @override + State createState() => _ZenitButtonExampleState(); +} + +bool val = true; + +class _ZenitButtonExampleState extends State { + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Gap(8), + const ExampleTitle("ZenitButton"), + const Gap(24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 160, + child: ZenitPrimaryButton( + onPressed: () => print("PrimaryButton was clicked"), + child: const Text("PrimaryButton"), + ), + ), + const Gap(12), + SizedBox( + width: 160, + child: ZenitSecondaryButton( + onPressed: () => print("FilledButton was clicked"), + child: const Text("SecondaryButton"), + ), + ), + ], + ), + const Gap(12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 160, + child: ZenitTextButton( + onPressed: () => print("FilledButton was clicked"), + child: const Text("TextButton"), + ), + ), + ], + ), + const Gap(24), + const ExampleSubtitle("Custom ListTiles with ZenitButtons"), + const Gap(8), + SizedBox( + width: MediaQuery.of(context).size.width * 0.4 + 256, + child: ZenitSection( + child: Column( + children: [ + ListTile( + title: const Text( + "A ListTile with a ZenitPrimaryButton", + ), + subtitle: const Text( + "This is the subtitle", + ), + trailing: SizedBox( + width: 160, + child: ZenitPrimaryButton( + onPressed: () => print("Button pressed"), + child: const Text("Primary Button"), + ), + ), + ), + ListTile( + title: const Text( + "A ListTile with a ZenitSecondaryButton", + ), + subtitle: const Text( + "This is the subtitle", + ), + trailing: SizedBox( + width: 160, + child: ZenitSecondaryButton( + onPressed: () => print("Button pressed"), + child: const Text("Secondary Button"), + ), + ), + ), + ListTile( + title: const Text( + "A ListTile with a ZenitTextButton", + ), + subtitle: const Text( + "This is the subtitle", + ), + trailing: SizedBox( + width: 160, + child: ZenitTextButton( + onPressed: () => print("Button pressed"), + child: const Text("Text Button"), + ), + ), + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/example/lib/pages/checkbox.dart b/example/lib/pages/checkbox.dart index a047ae4..28d2962 100644 --- a/example/lib/pages/checkbox.dart +++ b/example/lib/pages/checkbox.dart @@ -1,6 +1,6 @@ -import 'package:flutter/cupertino.dart'; import 'package:gap/gap.dart'; import 'package:zenit_ui/zenit_ui.dart'; +import 'package:zenit_ui_example/title.dart'; class ZenitCheckboxExample extends StatefulWidget { const ZenitCheckboxExample({super.key}); @@ -15,9 +15,14 @@ class _ZenitCheckboxExampleState extends State { final List _checkboxValues = [true, null, false]; @override Widget build(BuildContext context) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Column( + const Gap(8), + const ExampleTitle("ZenitCheckbox"), + const Gap(24), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ for (int i = 0; i < _checkboxValues.length; i++) ...[ ZenitCheckbox( @@ -29,7 +34,38 @@ class _ZenitCheckboxExampleState extends State { ] ], ), + const Gap(24), + const ExampleSubtitle("ZenitCheckboxListTile"), + const Gap(8), + SizedBox( + width: MediaQuery.of(context).size.width * 0.4 + 256, + child: ZenitSection( + child: Column( + children: [ + for (int i = 0; i < _checkboxValues.length; i++) ...[ + ZenitCheckboxListTile( + title: const Text("ZenitCheckboxListTile"), + subtitle: Text("ZenitCheckboxListTile is ${resolve(_checkboxValues[i])}"), + value: _checkboxValues[i], + onChanged: (value) => setState(() => _checkboxValues[i] = value), + tristate: true, + ) + ], + ], + ), + ), + ), ], ); } + + String resolve(bool? val) { + if (val == null) { + return "intermedate"; + } else if (val) { + return "checked"; + } else { + return "unchecked"; + } + } } diff --git a/example/lib/pages/color_scheme.dart b/example/lib/pages/color_scheme.dart new file mode 100644 index 0000000..2980f0b --- /dev/null +++ b/example/lib/pages/color_scheme.dart @@ -0,0 +1,66 @@ +import 'package:zenit_ui/zenit_ui.dart'; + +class ColorSchemeExamplePage extends StatelessWidget { + const ColorSchemeExamplePage({super.key}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return SingleChildScrollView( + child: Column( + children: [ + // Background + colorSchemeDisplay(cs.background, "Background"), + colorSchemeDisplay(cs.onBackground, "On Background"), + // Surface + colorSchemeDisplay(cs.surface, "Surface"), + colorSchemeDisplay(cs.onSurface, "On Surface"), + colorSchemeDisplay(cs.inverseSurface, "Inverse Surface"), + colorSchemeDisplay(cs.surfaceTint, "Surface Tint"), + // Primary + colorSchemeDisplay(cs.primary, "Primary"), + colorSchemeDisplay(cs.onPrimary, "On Primary"), + colorSchemeDisplay(cs.inversePrimary, "Inverse Primary"), + // Secondary + colorSchemeDisplay(cs.secondary, "Secondary"), + colorSchemeDisplay(cs.onSecondary, "On Secondary"), + colorSchemeDisplay(cs.secondaryContainer, "Secondary Container"), + colorSchemeDisplay(cs.onSecondaryContainer, "On Secondary Container"), + // Teriary + colorSchemeDisplay(cs.tertiary, "Tertiary"), + colorSchemeDisplay(cs.onTertiary, "On Tertiary"), + // Error + colorSchemeDisplay(cs.error, "Error"), + colorSchemeDisplay(cs.onError, "On Error"), + colorSchemeDisplay(cs.errorContainer, "Error Container"), + colorSchemeDisplay(cs.onErrorContainer, "On Error Container"), + // Outline + colorSchemeDisplay(cs.outline, "Outline"), + colorSchemeDisplay(cs.outlineVariant, "Outline Variant"), + // Scrim + colorSchemeDisplay(cs.scrim, "Scrim"), + // Shadow + colorSchemeDisplay(cs.shadow, "Shadow"), + ], + ), + ); + } +} + +SizedBox colorSchemeDisplay(Color color, String label) { + return SizedBox( + height: 64, + child: ZenitSection( + color: color, + child: Center( + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + color: color.computeLuminance() > 0.3 ? Colors.black : Colors.white, + ), + ), + ), + ), + ); +} diff --git a/example/lib/pages/context_menu.dart b/example/lib/pages/context_menu.dart new file mode 100644 index 0000000..a5b2dc2 --- /dev/null +++ b/example/lib/pages/context_menu.dart @@ -0,0 +1,108 @@ +import 'package:flutter/services.dart'; +import 'package:zenit_ui/zenit_ui.dart'; + +class ZenitContextMenuExample extends StatelessWidget { + const ZenitContextMenuExample({super.key}); + + @override + Widget build(BuildContext context) { + return ZenitContextMenuRegion( + contextMenu: ZenitContextMenuData( + entries: [ + ZenitContextMenuItem( + leading: const Icon(Icons.arrow_back_rounded), + label: "Backward", + onPressed: () { + print("back"); + }, + shortcut: + const SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true), + ), + ZenitContextMenuItem( + leading: const Icon(Icons.arrow_forward_rounded), + label: ("Forward"), + onPressed: () {}, + shortcut: + const SingleActivator(LogicalKeyboardKey.arrowRight, alt: true), + ), + ZenitContextMenuItem( + leading: const Icon(Icons.replay_rounded), + label: ("Reload"), + onPressed: () {}, + shortcut: + const SingleActivator(LogicalKeyboardKey.keyR, control: true), + ), + const ZenitContextMenuDivider(), + ZenitContextMenuItem( + leading: const Icon(Icons.add_rounded), + label: ("Add something"), + onPressed: () {}, + shortcut: + const SingleActivator(LogicalKeyboardKey.keyA, control: true), + ), + ZenitContextMenuItem( + leading: const FlutterLogo(), + label: ("Uhh Flutter Logo"), + onPressed: () {}, + shortcut: + const SingleActivator(LogicalKeyboardKey.keyF, control: true), + ), + const ZenitContextMenuDivider(), + ZenitContextMenuItem( + leading: const Icon(Icons.copy_rounded), + label: ("Copy text"), + onPressed: () {}, + shortcut: + const SingleActivator(LogicalKeyboardKey.keyC, control: true), + ), + ZenitContextMenuItem( + leading: const Icon(Icons.cut_rounded), + label: ("Cut text"), + onPressed: () {}, + shortcut: + const SingleActivator(LogicalKeyboardKey.keyX, control: true), + ), + ZenitContextMenuItem( + leading: const Icon(Icons.print_rounded), + label: ("Print"), + onPressed: () => print("Print"), + shortcut: + const SingleActivator(LogicalKeyboardKey.keyP, control: true), + ), + ZenitNestedContextMenuItem( + children: [ + ZenitContextMenuItem( + leading: const Icon(Icons.copy_rounded), + label: ("Copy text"), + onPressed: () {}, + shortcut: const SingleActivator(LogicalKeyboardKey.keyC, + control: true), + ), + ZenitContextMenuItem( + leading: const Icon(Icons.cut_rounded), + label: ("Cut text"), + onPressed: () {}, + shortcut: const SingleActivator(LogicalKeyboardKey.keyX, + control: true), + ), + ZenitContextMenuItem( + leading: const Icon(Icons.print_rounded), + label: ("Print"), + onPressed: () {}, + ), + ], + label: "Nested", + ), + ], + ), + child: const SizedBox.expand( + child: DecoratedBox( + decoration: BoxDecoration(), + child: Center( + child: Text("Right click anywhere!"), + ), + ), + ), + ); + } +} diff --git a/example/lib/pages/dialog.dart b/example/lib/pages/dialog.dart new file mode 100644 index 0000000..79b0aa0 --- /dev/null +++ b/example/lib/pages/dialog.dart @@ -0,0 +1,45 @@ +import 'package:zenit_ui/zenit_ui.dart'; + +class DialogExample extends StatelessWidget { + const DialogExample({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: ZenitPrimaryButton( + child: const Text("Show dialog"), + onPressed: () { + showDialog( + context: context, + builder: (context) => ZenitDialog( + title: const Text("ZenitDialog Widget"), + icon: const FlutterLogo(), + content: const Text( + "Dialog content\nThis dialog is for demo purposes only.\nIt says it saves something but it actually doesn't.\nThose buttons are only there for show"), + actions: [ + SizedBox( + width: 100, + child: ZenitTextButton( + child: const Text("Cancel"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + SizedBox( + width: 100, + child: ZenitPrimaryButton( + child: const Text("Save"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/example/lib/pages/icon_button.dart b/example/lib/pages/icon_button.dart index f2e00dc..04c4a24 100644 --- a/example/lib/pages/icon_button.dart +++ b/example/lib/pages/icon_button.dart @@ -1,28 +1,35 @@ -import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:zenit_ui/zenit_ui.dart'; +import 'package:zenit_ui_example/title.dart'; class ZenitIconButtonExample extends StatelessWidget { const ZenitIconButtonExample({super.key}); @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, + return Column( children: [ - ZenitIconButton( - icon: Icons.remove_red_eye, - onPressed: () {}, - ), - const Gap(16), - ZenitIconButton( - icon: Icons.add, - onPressed: () {}, - ), - const Gap(16), - ZenitIconButton( - icon: Icons.close, - onPressed: () {}, + const Gap(8), + const ExampleTitle("ZenitIconButton"), + const Gap(24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ZenitIconButton( + icon: Icons.remove_red_eye, + onPressed: () {}, + ), + const Gap(16), + ZenitIconButton( + icon: Icons.add, + onPressed: () {}, + ), + const Gap(16), + ZenitIconButton( + icon: Icons.close, + onPressed: () {}, + ), + ], ), ], ); diff --git a/example/lib/pages/list_titles.dart b/example/lib/pages/list_titles.dart index 655eb10..a98d32e 100644 --- a/example/lib/pages/list_titles.dart +++ b/example/lib/pages/list_titles.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:zenit_ui/zenit_ui.dart'; class ZenitListTilesExample extends StatefulWidget { @@ -9,7 +8,7 @@ class ZenitListTilesExample extends StatefulWidget { } class _ZenitListTilesExampleState extends State { - bool value = false; + bool? value = false; @override Widget build(BuildContext context) { final textStyle = Theme.of(context).textTheme.bodyLarge; @@ -21,16 +20,24 @@ class _ZenitListTilesExampleState extends State { ), defaultListTile(), switchListTile(), + checkboxListTile(), + radioButtonListTile(), buttonListTile(), const Divider(), Padding( padding: const EdgeInsets.all(8.0), - child: Text("ListTiles in cards", style: textStyle), + child: Text("ListTiles in ZenitSections", style: textStyle), ), - Card( + ZenitSection( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: [defaultListTile(), switchListTile(), buttonListTile()], + children: [ + defaultListTile(), + switchListTile(), + checkboxListTile(), + radioButtonListTile(), + buttonListTile() + ], ), ), ], @@ -45,7 +52,7 @@ class _ZenitListTilesExampleState extends State { subtitle: const Text( "This is the subtitle", ), - trailing: ZenitFilledButton( + trailing: ZenitSecondaryButton( onPressed: () => print("Button pressed"), child: const Text("Button"), ), @@ -60,11 +67,38 @@ class _ZenitListTilesExampleState extends State { subtitle: const Text( "This is the subtitle", ), - value: value, + value: value ?? false, onChanged: (val) => setState(() => value = val), ); } + ZenitRadioButtonListTile radioButtonListTile() { + return ZenitRadioButtonListTile( + title: const Text( + "ZenitRadioButtonListTile", + ), + subtitle: const Text( + "This is the subtitle", + ), + value: value, + groupValue: true, + onChanged: (val) => setState(() => value = !val!), + ); + } + + ZenitCheckboxListTile checkboxListTile() { + return ZenitCheckboxListTile( + title: const Text( + "ZenitCheckboxListTile", + ), + subtitle: const Text( + "This is the subtitle", + ), + value: value, + onChanged: (val) => setState(() => value = val!), + ); + } + ListTile defaultListTile() { return const ListTile( title: Text( diff --git a/example/lib/pages/radio_button.dart b/example/lib/pages/radio_button.dart index 1d85033..ff14126 100644 --- a/example/lib/pages/radio_button.dart +++ b/example/lib/pages/radio_button.dart @@ -1,12 +1,13 @@ -import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:zenit_ui/zenit_ui.dart'; +import 'package:zenit_ui_example/title.dart'; class ZenitRadioButtonExample extends StatefulWidget { const ZenitRadioButtonExample({super.key}); @override - State createState() => _ZenitRadioButtonExampleState(); + State createState() => + _ZenitRadioButtonExampleState(); } bool val = false; @@ -16,50 +17,45 @@ class _ZenitRadioButtonExampleState extends State { @override Widget build(BuildContext context) { - return Row( + return Column( children: [ - Column( + const Gap(8), + const ExampleTitle("ZenitRadioButton"), + const Gap(24), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - ZenitRadioButton( - value: 0, - groupValue: groupValue, - onChanged: (value) => setState(() => groupValue = value!), - ), - const Gap(16), - ZenitRadioButton( - value: 1, - groupValue: groupValue, - onChanged: (value) => setState(() => groupValue = value!), - ), - const Gap(16), - ZenitRadioButton( - value: 2, - groupValue: groupValue, - onChanged: (value) => setState(() => groupValue = value!), - ), + for (double i = 0; i < 3; i++) ...[ + ZenitRadioButton( + value: i, + groupValue: groupValue, + onChanged: (value) => setState(() => groupValue = value!), + ), + const Gap(16), + ], ], ), const Gap(24), - Column( - children: [ - ZenitRadioButton( - value: 3, - groupValue: groupValue, - onChanged: (value) => setState(() => groupValue = value!), - ), - const Gap(16), - ZenitRadioButton( - value: 4, - groupValue: groupValue, - onChanged: (value) => setState(() => groupValue = value!), + const ExampleSubtitle("ZenitRadioButtonListTile"), + const Gap(8), + SizedBox( + width: MediaQuery.of(context).size.width * 0.4 + 256, + child: ZenitSection( + child: Column( + children: [ + for (double d = 0; d < 3; d++) ...[ + ZenitRadioButtonListTile( + title: const Text("ZenitRadioButtonListTile"), + subtitle: Text( + "ZenitRadioButtonListTile is ${d == groupValue ? "selected" : "not selected"}"), + value: d, + groupValue: groupValue, + onChanged: (value) => setState(() => groupValue = value!), + ), + ], + ], ), - const Gap(16), - ZenitRadioButton( - value: 5, - groupValue: groupValue, - onChanged: (value) => setState(() => groupValue = value!), - ), - ], + ), ), ], ); diff --git a/example/lib/pages/slider.dart b/example/lib/pages/slider.dart new file mode 100644 index 0000000..8476d07 --- /dev/null +++ b/example/lib/pages/slider.dart @@ -0,0 +1,29 @@ +import 'package:zenit_ui/zenit_ui.dart'; + +class SliderExample extends StatefulWidget { + const SliderExample({super.key}); + + @override + State createState() => _SliderExampleState(); +} + +class _SliderExampleState extends State { + double value = 0.5; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: ZenitSlider( + onChanged: (val) { + setState(() { + value = val; + }); + }, + value: value, + ), + ), + ); + } +} diff --git a/example/lib/pages/sub_pages.dart b/example/lib/pages/sub_pages.dart new file mode 100644 index 0000000..9f1307f --- /dev/null +++ b/example/lib/pages/sub_pages.dart @@ -0,0 +1,57 @@ +import 'package:zenit_ui/zenit_ui.dart'; + +class SubPagesExample extends StatelessWidget { + const SubPagesExample({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: ZenitPrimaryButton( + child: const Text("Go to sub page"), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const _SubPage(), + ), + ); + }, + ), + ); + } +} + +class _SubPage extends StatelessWidget { + const _SubPage(); + + @override + Widget build(BuildContext context) { + return Center( + child: ZenitPrimaryButton( + child: const Text("Go to sub sub page"), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const _SubSubPage(), + ), + ); + }, + ), + ); + } +} + +class _SubSubPage extends StatelessWidget { + const _SubSubPage(); + + @override + Widget build(BuildContext context) { + return Center( + child: ZenitPrimaryButton( + child: const Text("Go back"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + } +} diff --git a/example/lib/pages/switch.dart b/example/lib/pages/switch.dart index 821b5aa..6f25042 100644 --- a/example/lib/pages/switch.dart +++ b/example/lib/pages/switch.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:zenit_ui/zenit_ui.dart'; +import 'package:zenit_ui_example/title.dart'; class ZenitSwitchExample extends StatefulWidget { const ZenitSwitchExample({super.key}); @@ -9,14 +9,19 @@ class ZenitSwitchExample extends StatefulWidget { State createState() => _ZenitSwitchExampleState(); } -bool val = false; +bool val = true; class _ZenitSwitchExampleState extends State { @override Widget build(BuildContext context) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Column( + const Gap(8), + const ExampleTitle("ZenitSwitch"), + const Gap(24), + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ ZenitSwitch( value: val, @@ -27,31 +32,27 @@ class _ZenitSwitchExampleState extends State { value: !val, onChanged: (value) => setState(() => val = !val), ), - const Gap(16), - ZenitSwitch( - value: val, - onChanged: (value) => setState(() => val = !val), - ), ], ), const Gap(24), - Column( - children: [ - ZenitSwitch( - value: !val, - onChanged: (value) => setState(() => val = !val), + const ExampleSubtitle("ZenitSwitchListTile"), + const Gap(8), + SizedBox( + width: MediaQuery.of(context).size.width * 0.4 + 256, + child: ZenitSection( + child: Column( + children: [ + for (int i = 0; i < 2; i++) ...[ + ZenitSwitchListTile( + title: const Text("ZenitSwitchListTile"), + subtitle: Text("ZenitSwitchListTile is ${val ? "on" : "off"}"), + value: i.isEven ? val : !val, + onChanged: (value) => setState(() => val = !val), + ), + ] + ], ), - const Gap(16), - ZenitSwitch( - value: val, - onChanged: (value) => setState(() => val = !val), - ), - const Gap(16), - ZenitSwitch( - value: !val, - onChanged: (value) => setState(() => val = !val), - ), - ], + ), ), ], ); diff --git a/example/lib/pages/tab_view.dart b/example/lib/pages/tab_view.dart index db910e9..f5b30dd 100644 --- a/example/lib/pages/tab_view.dart +++ b/example/lib/pages/tab_view.dart @@ -1,7 +1,8 @@ import 'dart:math'; -import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:zenit_ui/zenit_ui.dart'; +import 'package:zenit_ui_example/title.dart'; class ZenitTabViewExample extends StatefulWidget { const ZenitTabViewExample({super.key}); @@ -35,6 +36,9 @@ class _ZenitTabViewExampleState extends State { Widget build(BuildContext context) { return Column( children: [ + const Gap(8), + const ExampleTitle("ZenitTabBar"), + const Gap(24), ZenitTabBar( selectedIndex: _selectedIndex, tabs: tabs, diff --git a/example/lib/pages/text.dart b/example/lib/pages/text.dart index 8ed4ddf..09fa696 100644 --- a/example/lib/pages/text.dart +++ b/example/lib/pages/text.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; +import 'package:zenit_ui/zenit_ui.dart'; class TextExamplePage extends StatelessWidget { const TextExamplePage({super.key}); @@ -21,8 +21,8 @@ class TextExamplePage extends StatelessWidget { ); } - Card textStyleTitle(TextTheme textTheme) { - return Card( + ZenitSection textStyleTitle(TextTheme textTheme) { + return ZenitSection( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -49,8 +49,8 @@ class TextExamplePage extends StatelessWidget { ); } - Card textStyleLabel(TextTheme textTheme) { - return Card( + ZenitSection textStyleLabel(TextTheme textTheme) { + return ZenitSection( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -77,8 +77,8 @@ class TextExamplePage extends StatelessWidget { ); } - Card textStyleHeadline(TextTheme textTheme) { - return Card( + ZenitSection textStyleHeadline(TextTheme textTheme) { + return ZenitSection( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -105,8 +105,8 @@ class TextExamplePage extends StatelessWidget { ); } - Card textStyleBody(TextTheme textTheme) { - return Card( + ZenitSection textStyleBody(TextTheme textTheme) { + return ZenitSection( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -133,8 +133,8 @@ class TextExamplePage extends StatelessWidget { ); } - Card textStyleDisplay(TextTheme textTheme) { - return Card( + ZenitSection textStyleDisplay(TextTheme textTheme) { + return ZenitSection( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( diff --git a/example/lib/pages/text_field.dart b/example/lib/pages/text_field.dart new file mode 100644 index 0000000..6c0252f --- /dev/null +++ b/example/lib/pages/text_field.dart @@ -0,0 +1,41 @@ +import 'package:gap/gap.dart'; +import 'package:zenit_ui/zenit_ui.dart'; +import 'package:zenit_ui_example/title.dart'; + +class ZenitTextFieldExample extends StatefulWidget { + const ZenitTextFieldExample({super.key}); + + @override + State createState() => _ZenitTextFieldExampleState(); +} + +class _ZenitTextFieldExampleState extends State { + String text = ""; + final TextEditingController controller = TextEditingController(); + @override + Widget build(BuildContext context) { + return Center( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Gap(8), + const ExampleTitle("ZenitTextField"), + const Gap(24), + SizedBox( + width: 256, + child: ZenitTextField( + hint: "Text Field! You can type here", + controller: controller, + onChanged: (value) => setState(() => text = controller.text), + ), + ), + const Gap(8), + Text( + "Your Text is here:\n $text", + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/welcome.dart b/example/lib/pages/welcome.dart new file mode 100644 index 0000000..8fabce2 --- /dev/null +++ b/example/lib/pages/welcome.dart @@ -0,0 +1,93 @@ +import 'package:gap/gap.dart'; +import 'package:zenit_ui/zenit_ui.dart'; +import 'package:zenit_ui_example/title.dart'; + +class ZenitWelcome extends StatefulWidget { + const ZenitWelcome({super.key}); + + @override + State createState() => _ZenitWelcomeState(); +} + +class _ZenitWelcomeState extends State { + double val = 0.5; + bool value = false; + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Gap(24), + SizedBox( + width: 500, + height: 175, + child: ZenitSection( + borderRadius: BorderRadius.circular(24), + child: Image.asset( + "assets/banner.png", + fit: BoxFit.cover, + ), + ), + ), + const Gap(24), + const ExampleTitle("ZenitUI Flutter Showcase App"), + const Gap(24), + const ExampleSubtitle("What is ZenitUI?"), + const Gap(8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: 750, + child: Text( + "ZenitUI is a widget library for Flutter that provides a set of customizable widgets that can be used to build user interfaces. It is designed to be compatible and interchangeable with the Material library. With ZenitUI, developers can create UIs with ease and speed up their development process.", + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), + ), + const Gap(24), + const ExampleSubtitle("Who maintains ZenitUI?"), + const Gap(8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: 750, + child: Text( + "ZenitUI is a community project maintained by dahliaOS", + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), + ), + const Gap(24), + const ExampleSubtitle("Currently supported ZenitUI widgets"), + const Gap(8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: 750, + child: Text( + "Button, Switch, Checkbox, RadioButton, IconButton, TextField, TabBar", + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), + ), + const Gap(24), + SizedBox( + width: 250, + height: 80, + child: ZenitSection( + borderRadius: BorderRadius.circular(16), + child: Image.asset( + Theme.of(context).darkMode ? "assets/dahliaos_banner_dark.png" : "assets/dahliaos_banner_light.png", + fit: BoxFit.cover, + ), + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/pages/zenit_components.dart b/example/lib/pages/zenit_components.dart deleted file mode 100644 index a142a03..0000000 --- a/example/lib/pages/zenit_components.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:gap/gap.dart'; -import 'package:zenit_ui/zenit_ui.dart'; - -const kButtonWidth = 132.0; - -class ZenitComponentsExample extends StatefulWidget { - const ZenitComponentsExample({super.key}); - - @override - State createState() => _ZenitComponentsExampleState(); -} - -class _ZenitComponentsExampleState extends State { - double val = 0.5; - bool value = false; - @override - Widget build(BuildContext context) { - return SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - const Gap(48), - Image.asset( - "assets/banner.png", - width: 557, - height: 192, - ), - const Gap(48), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - width: kButtonWidth, - child: ZenitTextButton( - onPressed: () => print("TextButton was clicked"), - child: const Text("TextButton"), - ), - ), - const Gap(16), - SizedBox( - width: kButtonWidth, - child: ZenitFilledButton( - onPressed: () => print("FilledButton was clicked"), - child: const Text("FilledButton"), - ), - ), - const Gap(16), - SizedBox( - width: kButtonWidth, - child: ZenitElevatedButton( - onPressed: () => print("FilledButton was clicked"), - foregroundColor: Colors.white, - child: const Text("ElevatedButton"), - ), - ), - ], - ), - const Gap(16), - SizedBox( - width: 300, - child: ZenitTextField( - controller: TextEditingController(), - hint: "ZenitTextField", - ), - ), - const Gap(16), - SizedBox( - width: 325, - child: ZenitSlider( - value: val, - onChanged: (value) => setState(() => val = value), - ), - ), - const Gap(16), - SizedBox( - width: 325, - child: Card( - child: ListTile( - title: const Text("Selectables"), - subtitle: Text(value ? "On" : "Off"), - onTap: () => setState(() => value = !value), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ZenitRadioButton( - value: true, - groupValue: value, - onChanged: (val) => setState(() => value = !val!), - ), - const Gap(8), - ZenitCheckbox( - value: value, - onChanged: (val) => setState(() => value = val!), - ), - const Gap(8), - ZenitSwitch( - value: value, - onChanged: (val) => setState(() => value = val), - ), - ], - ), - ), - ), - ), - const Gap(16), - ZenitIconButton( - icon: Icons.add, - onPressed: () { - ScaffoldMessenger.of(context).showMaterialBanner(MaterialBanner(content: const Text("Test"), actions: [ - ZenitIconButton( - icon: Icons.close, - hoverColor: Theme.of(context).foregroundColor.withOpacity(0.1), - onPressed: () => ScaffoldMessenger.of(context).clearMaterialBanners(), - ) - ])); - }, - ), - const Gap(48), - ], - ), - ); - } -} diff --git a/example/lib/title.dart b/example/lib/title.dart new file mode 100644 index 0000000..3066557 --- /dev/null +++ b/example/lib/title.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class ExampleTitle extends StatelessWidget { + final String title; + const ExampleTitle(this.title, {super.key}); + + @override + Widget build(BuildContext context) { + return Text( + title, + style: Theme.of(context).textTheme.headlineLarge, + textAlign: TextAlign.center, + ); + } +} + +class ExampleSubtitle extends StatelessWidget { + final String title; + const ExampleSubtitle(this.title, {super.key}); + + @override + Widget build(BuildContext context) { + return Text( + title, + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ); + } +} diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index e71a16d..ac82777 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,22 @@ #include "generated_plugin_registrant.h" +#include +#include +#include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) handy_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin"); + handy_window_plugin_register_with_registrar(handy_window_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); + g_autoptr(FlPluginRegistrar) yaru_window_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "YaruWindowLinuxPlugin"); + yaru_window_linux_plugin_register_with_registrar(yaru_window_linux_registrar); } diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index 2e1de87..32f5d67 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -3,6 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + handy_window + screen_retriever + window_manager + yaru_window_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/example/linux/my_application.cc b/example/linux/my_application.cc index 0ba8f43..68a9e79 100644 --- a/example/linux/my_application.cc +++ b/example/linux/my_application.cc @@ -40,25 +40,29 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_title(header_bar, "ZenitUI Example"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "example"); + gtk_window_set_title(window, "ZenitUI Example"); } - gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); + GdkGeometry geometry_min; + geometry_min.min_width = 500; + geometry_min.min_height = 720; + gtk_window_set_geometry_hints(window, nullptr, &geometry_min, GDK_HINT_MIN_SIZE); + + gtk_window_set_default_size(window, 1080, 720); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - + gtk_widget_show(GTK_WIDGET(view)); + gtk_widget_show(GTK_WIDGET(window)); gtk_widget_grab_focus(GTK_WIDGET(view)); } @@ -101,4 +105,4 @@ MyApplication* my_application_new() { "application-id", APPLICATION_ID, "flags", G_APPLICATION_NON_UNIQUE, nullptr)); -} +} \ No newline at end of file diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..b622947 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,10 @@ import FlutterMacOS import Foundation +import screen_retriever +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3f7f7cf..3660f9a 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: zenit_ui: path: ../ gap: ^3.0.0 + handy_window: ^0.3.1 dev_dependencies: flutter_lints: ^2.0.1 @@ -23,4 +24,6 @@ flutter: assets: - assets/icon_light.png - - assets/banner.png \ No newline at end of file + - assets/banner.png + - assets/dahliaos_banner_dark.png + - assets/dahliaos_banner_light.png \ No newline at end of file diff --git a/lib/src/base/button_base.dart b/lib/src/base/button_base.dart index 89119c1..0b70af6 100644 --- a/lib/src/base/button_base.dart +++ b/lib/src/base/button_base.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:zenit_ui/src/constants/constants.dart'; -import 'package:zenit_ui/src/theme/theme.dart'; class ButtonBase extends StatefulWidget { const ButtonBase({ @@ -11,6 +10,9 @@ class ButtonBase extends StatefulWidget { this.backgroundColor, this.hoverColor, this.splashColor, + this.borderSide, + this.borderRadius, + this.fontWeight = FontWeight.w500, }); final Widget? child; @@ -22,6 +24,9 @@ class ButtonBase extends StatefulWidget { final Color? foregroundColor; final Color? hoverColor; final Color? splashColor; + final BorderSide? borderSide; + final BorderRadius? borderRadius; + final FontWeight? fontWeight; @override _ButtonBaseState createState() => _ButtonBaseState(); @@ -38,23 +43,28 @@ class _ButtonBaseState extends State { enabled: widget.onPressed != null, child: Material( clipBehavior: Clip.antiAlias, - borderRadius: kDefaultBorderRadiusMedium, - color: widget.backgroundColor ?? theme.elementColor, + color: widget.backgroundColor ?? theme.colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: widget.borderRadius ?? kDefaultBorderRadiusMedium, + side: widget.borderSide ?? BorderSide.none, + ), child: InkWell( hoverColor: widget.hoverColor, splashColor: widget.splashColor, onTap: widget.onPressed, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: IconTheme.merge( data: IconThemeData( - color: widget.foregroundColor ?? theme.foregroundColor, + color: widget.foregroundColor ?? theme.colorScheme.onSurface, ), child: DefaultTextStyle( style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: widget.foregroundColor ?? theme.foregroundColor, + color: widget.foregroundColor ?? theme.colorScheme.onSurface, + fontWeight: widget.fontWeight, + letterSpacing: 0.2, + fontFamily: "Inter", + package: "zenit_ui", ), child: Center( widthFactor: 1, diff --git a/lib/src/base/tick_animator.dart b/lib/src/base/tick_animator.dart deleted file mode 100644 index 04447a0..0000000 --- a/lib/src/base/tick_animator.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:zenit_ui/src/constants/constants.dart'; -import 'package:zenit_ui/src/extensions/extensions.dart'; - -class TickAnimator extends StatefulWidget { - final VoidCallback? onPressed; - final Widget? child; - final BorderRadius? borderRadius; - final Duration duration; - final double multiplier; - - const TickAnimator({ - super.key, - required this.onPressed, - required this.child, - this.borderRadius, - this.duration = kDefaultAnimationDuration, - this.multiplier = 0.95, - }) : assert(multiplier > 0 && multiplier < 1); - - @override - State createState() => _TickAnimatorState(); -} - -//TODO possibly integrate PhysicalModel into this or make sure it's not needed -class _TickAnimatorState extends State - with SingleTickerProviderStateMixin { - late AnimationController animationController; - late Animation animation; - - @override - void initState() { - animationController = AnimationController( - duration: widget.duration, - vsync: this, - ); - animation = Tween(begin: 1.0, end: widget.multiplier).animate( - CurvedAnimation( - parent: animationController, - curve: Curves.fastOutSlowIn, - ), - )..addListener( - () => setState(() {}), - ); - super.initState(); - } - - @override - void dispose() { - animationController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Transform.scale( - scale: animation.value, - child: InkWell( - onTap: callbackAnimationHandler( - widget.onPressed, - animationController, - widget.duration ~/ 2, - ), - borderRadius: widget.borderRadius, - child: widget.child, - ), - ); - } -} - -Function()? callbackAnimationHandler( - VoidCallback? callback, - AnimationController controller, - Duration duration, -) { - if (callback.isNull) { - return null; - } else { - return () async { - controller.forward(); - callback!(); - await Future.delayed(duration, () => controller.reverse()); - }; - } -} diff --git a/lib/src/components/buttons/buttons.dart b/lib/src/components/buttons/buttons.dart index 4705c1a..f57545e 100644 --- a/lib/src/components/buttons/buttons.dart +++ b/lib/src/components/buttons/buttons.dart @@ -1,44 +1,73 @@ import 'package:flutter/material.dart'; import 'package:zenit_ui/src/base/button_base.dart'; -import 'package:zenit_ui/src/theme/theme.dart'; - -class ZenitTextButton extends StatelessWidget { - final Widget? child; +import 'package:zenit_ui/src/extensions/extensions.dart'; +class ZenitPrimaryButton extends StatelessWidget { final VoidCallback? onPressed; final Color? foregroundColor; + final Color? backgroundColor; + final Color? hoverColor; + final Color? splashColor; - const ZenitTextButton({super.key, this.child, this.onPressed, this.foregroundColor}); + final BorderSide? borderSide; + + final BorderRadius? borderRadius; + + final Widget? child; + + const ZenitPrimaryButton({ + super.key, + this.onPressed, + this.foregroundColor, + this.backgroundColor, + this.hoverColor, + this.splashColor, + this.borderSide, + this.borderRadius, + this.child, + }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return ButtonBase( onPressed: onPressed, - foregroundColor: foregroundColor ?? theme.primaryColor, - hoverColor: (foregroundColor ?? theme.primaryColor).withOpacity(0.25), - splashColor: (foregroundColor ?? theme.primaryColor).withOpacity(0.25), - backgroundColor: Colors.transparent, + foregroundColor: foregroundColor ?? theme.colorScheme.onPrimary, + backgroundColor: backgroundColor ?? theme.colorScheme.primary, + hoverColor: (backgroundColor ?? theme.colorScheme.onPrimary).withOpacity(0.1), + splashColor: (backgroundColor ?? theme.colorScheme.onPrimary).withOpacity(0.1), + borderSide: borderSide ?? BorderSide(color: theme.colorScheme.outline), + borderRadius: borderRadius, child: child, ); } } -class ZenitFilledButton extends StatelessWidget { - final Widget? child; - +class ZenitSecondaryButton extends StatelessWidget { final VoidCallback? onPressed; final Color? foregroundColor; final Color? backgroundColor; + final Color? hoverColor; + final Color? splashColor; + + final BorderSide? borderSide; - const ZenitFilledButton({ + final BorderRadius? borderRadius; + + final Widget? child; + + const ZenitSecondaryButton({ super.key, - this.child, this.onPressed, this.foregroundColor, this.backgroundColor, + this.hoverColor, + this.splashColor, + this.borderSide, + this.borderRadius, + this.child, }); @override @@ -46,31 +75,41 @@ class ZenitFilledButton extends StatelessWidget { final theme = Theme.of(context); return ButtonBase( onPressed: onPressed, - foregroundColor: foregroundColor ?? theme.foregroundColor, - backgroundColor: backgroundColor ?? theme.elementColor, - hoverColor: Color.alphaBlend( - theme.foregroundColor.withOpacity(0.1), - backgroundColor ?? theme.elementColor, - ), + foregroundColor: foregroundColor ?? theme.colorScheme.onSurface, + backgroundColor: backgroundColor ?? theme.colorScheme.surface, + hoverColor: (backgroundColor ?? theme.colorScheme.onSurface).withOpacity(0.1), + splashColor: (backgroundColor ?? theme.colorScheme.onSurface).withOpacity(0.1), + borderSide: borderSide ?? BorderSide(color: theme.colorScheme.outline), + borderRadius: borderRadius, child: child, ); } } -class ZenitElevatedButton extends StatelessWidget { - final Widget? child; - +class ZenitTextButton extends StatelessWidget { final VoidCallback? onPressed; final Color? foregroundColor; final Color? backgroundColor; + final Color? hoverColor; + final Color? splashColor; + + final BorderSide? borderSide; + + final BorderRadius? borderRadius; + + final Widget? child; - const ZenitElevatedButton({ + const ZenitTextButton({ super.key, - this.child, this.onPressed, this.foregroundColor, this.backgroundColor, + this.hoverColor, + this.splashColor, + this.borderSide, + this.borderRadius, + this.child, }); @override @@ -78,8 +117,13 @@ class ZenitElevatedButton extends StatelessWidget { final theme = Theme.of(context); return ButtonBase( onPressed: onPressed, - foregroundColor: foregroundColor ?? theme.foregroundColor, - backgroundColor: backgroundColor ?? theme.primaryColor, + foregroundColor: foregroundColor ?? theme.colorScheme.primary, + backgroundColor: backgroundColor ?? Colors.transparent, + hoverColor: (backgroundColor ?? theme.colorScheme.primary).withOpacity(0.1), + splashColor: (backgroundColor ?? theme.colorScheme.onPrimary).withOpacity(0.1), + borderSide: borderSide ?? BorderSide(color: theme.colorScheme.primary.withOpacity(0.25)), + borderRadius: borderRadius, + fontWeight: FontWeight.w600, child: child, ); } @@ -93,25 +137,108 @@ class ZenitButton extends StatelessWidget { final Color? foregroundColor; final Color? backgroundColor; final Color? hoverColor; + final Color? splashColor; + + final BorderSide? borderSide; + + final BorderRadius? borderRadius; + + final ButtonType type; const ZenitButton({ super.key, + this.onPressed, + this.foregroundColor, + this.backgroundColor, + this.hoverColor, + this.splashColor, + this.borderSide, + this.borderRadius, this.child, + }) : type = ButtonType.secondary; + + const ZenitButton.primary({ + super.key, this.onPressed, this.foregroundColor, this.backgroundColor, this.hoverColor, - }); + this.splashColor, + this.borderSide, + this.borderRadius, + this.child, + }) : type = ButtonType.primary; + + const ZenitButton.secondary({ + super.key, + this.onPressed, + this.foregroundColor, + this.backgroundColor, + this.hoverColor, + this.splashColor, + this.borderSide, + this.borderRadius, + this.child, + }) : type = ButtonType.primary; + + const ZenitButton.text({ + super.key, + this.onPressed, + this.foregroundColor, + this.backgroundColor, + this.hoverColor, + this.splashColor, + this.borderSide, + this.borderRadius, + this.child, + }) : type = ButtonType.text; @override Widget build(BuildContext context) { final theme = Theme.of(context); - return ButtonBase( - onPressed: onPressed, - foregroundColor: foregroundColor ?? theme.foregroundColor, - backgroundColor: backgroundColor ?? theme.elementColor, - hoverColor: hoverColor, - child: child, - ); + switch (type) { + case ButtonType.primary: + return ZenitPrimaryButton( + onPressed: onPressed, + foregroundColor: foregroundColor ?? theme.colorScheme.onPrimary, + backgroundColor: backgroundColor ?? theme.colorScheme.primary, + splashColor: splashColor, + hoverColor: hoverColor, + borderSide: borderSide, + borderRadius: borderRadius, + child: child, + ); + case ButtonType.secondary: + return ZenitSecondaryButton( + onPressed: onPressed, + foregroundColor: foregroundColor ?? theme.colorScheme.onSurface, + backgroundColor: backgroundColor ?? theme.colorScheme.surface, + splashColor: splashColor, + hoverColor: hoverColor, + borderSide: borderSide, + borderRadius: borderRadius, + child: child, + ); + case ButtonType.text: + return ZenitTextButton( + onPressed: onPressed, + foregroundColor: foregroundColor ?? + HSLColor.fromColor(theme.colorScheme.primary.themedLightness(context, 0.5)) + .withSaturation(1) + .toColor(), + backgroundColor: backgroundColor ?? theme.primaryColor.withOpacity(0.3), + splashColor: splashColor, + hoverColor: hoverColor, + borderSide: borderSide, + borderRadius: borderRadius, + child: child, + ); + } } } + +enum ButtonType { + primary, + secondary, + text, +} diff --git a/lib/src/components/checkbox/checkbox.dart b/lib/src/components/checkbox/checkbox.dart index b8266d6..af97d34 100644 --- a/lib/src/components/checkbox/checkbox.dart +++ b/lib/src/components/checkbox/checkbox.dart @@ -1,75 +1,118 @@ import 'package:flutter/material.dart'; -import 'package:zenit_ui/src/base/tick_animator.dart'; import 'package:zenit_ui/src/constants/constants.dart'; import 'package:zenit_ui/src/theme/theme.dart'; -class ZenitCheckbox extends StatelessWidget { +class ZenitCheckbox extends StatefulWidget { final bool? value; final ValueChanged? onChanged; final ZenitCheckboxTheme? theme; - final Color? activeColor; - final Color? checkColor; final FocusNode? focusNode; final bool autofocus; final bool tristate; - //TODO implement groupValue const ZenitCheckbox({ super.key, required this.value, this.onChanged, this.theme, - this.activeColor, - this.checkColor, this.focusNode, this.autofocus = false, this.tristate = false, }) : assert(tristate || value != null); + @override + State createState() => _ZenitCheckboxState(); +} + +class _ZenitCheckboxState extends State with TickerProviderStateMixin { + late AnimationController checkmarkController; + late AnimationController dashController; + + @override + void initState() { + super.initState(); + checkmarkController = AnimationController( + vsync: this, + duration: kDefaultAnimationDuration, + value: 1, + ); + dashController = AnimationController( + vsync: this, + duration: kDefaultAnimationDuration, + value: 1, + ); + } + + @override + void dispose() { + checkmarkController.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(covariant ZenitCheckbox oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget == oldWidget) return; + checkmarkController.animateTo( + switch (widget.value) { + true => 1, + false || null => 0, + }, + ); + dashController.animateTo( + switch (widget.value) { + null => 1, + true || false => 0, + }, + ); + } + void handleTap() { - if (onChanged == null) return; - switch (value) { + if (widget.onChanged == null) return; + switch (widget.value) { case false: - onChanged!(true); + widget.onChanged!(true); case true: - onChanged!(tristate ? null : false); + widget.onChanged!(widget.tristate ? null : false); case null: - onChanged!(false); + widget.onChanged!(false); } } + bool hover = false; + @override Widget build(BuildContext context) { - final theme = this.theme ?? ZenitTheme.checkboxTheme(context); - final activeBackgroundColor = activeColor ?? theme.activeBackgroundColor; + final theme = widget.theme ?? ZenitTheme.checkboxTheme(context); + final activeBackgroundColor = theme.activeBackgroundColor; final inactiveBackgroundColor = theme.inactiveBackgroundColor; - final foregroundColor = checkColor ?? theme.foregroundColor; - final colored = value != false; - return TickAnimator( - onPressed: handleTap, - borderRadius: kDefaultBorderRadiusMedium, - child: Focus( - focusNode: focusNode ?? FocusNode(), - autofocus: autofocus, - child: PhysicalModel( - borderRadius: kDefaultBorderRadiusMedium, - clipBehavior: Clip.antiAlias, - color: switch (colored) { - true => activeBackgroundColor, - false => inactiveBackgroundColor, - }, - child: SizedBox( - width: 24, - height: 24, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: IconTheme( - data: IconThemeData(color: foregroundColor, size: 20), - child: switch (value) { - true => Icon(Icons.check_rounded, color: foregroundColor), - false => const SizedBox(), - null => Icon(Icons.remove_rounded, color: foregroundColor), - }, + final foregroundColor = theme.foregroundColor; + final curvedCheckmark = CurvedAnimation(parent: checkmarkController, curve: Curves.easeOut); + final curvedDash = CurvedAnimation(parent: dashController, curve: Curves.easeOut); + final outlineColor = theme.outlineColor; + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => hover = true), + onExit: (_) => setState(() => hover = false), + child: GestureDetector( + onTap: handleTap, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: AnimatedBuilder( + animation: Listenable.merge([checkmarkController, dashController]), + builder: (context, child) => CustomPaint( + painter: _CheckboxPainter( + value: widget.value, + activeBackgroundColor: activeBackgroundColor, + inactiveBackgroundColor: inactiveBackgroundColor, + foregroundColor: foregroundColor, + checkmarkAnimationValue: curvedCheckmark.value, + dashAnimationValue: curvedDash.value, + outlineColor: outlineColor, + hover: hover, + hoverColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), ), + size: const Size.square(32), ), ), ), @@ -77,3 +120,105 @@ class ZenitCheckbox extends StatelessWidget { ); } } + +class _CheckboxPainter extends CustomPainter { + final bool? value; + final Color activeBackgroundColor; + final Color inactiveBackgroundColor; + final Color foregroundColor; + final double checkmarkAnimationValue; + final double dashAnimationValue; + final bool hover; + final Color hoverColor; + final Color outlineColor; + + const _CheckboxPainter({ + required this.value, + required this.activeBackgroundColor, + required this.inactiveBackgroundColor, + required this.foregroundColor, + required this.checkmarkAnimationValue, + required this.dashAnimationValue, + required this.hover, + required this.hoverColor, + required this.outlineColor, + }); + + @override + void paint(Canvas canvas, Size size) { + const shape = RoundedRectangleBorder(borderRadius: kDefaultBorderRadiusSmall); + final center = Offset(size.width / 2, size.height / 2); + final rect = Rect.fromCenter(center: center, width: 24, height: 24); + final bgPath = shape.getOuterPath(rect); + final backgroundPaint = Paint() + ..color = value != false ? activeBackgroundColor : inactiveBackgroundColor + ..style = PaintingStyle.fill; + + final Paint outlinePaint = Paint() + ..color = value == true ? activeBackgroundColor : outlineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + + final Paint foregroundPaint = Paint() + ..color = foregroundColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..strokeJoin = StrokeJoin.round + ..strokeCap = StrokeCap.round; + + canvas.drawPath(bgPath, backgroundPaint); + canvas.drawPath(bgPath, outlinePaint); + + if (hover) { + final hoverPaint = Paint() + ..color = hoverColor + ..style = PaintingStyle.fill; + canvas.drawCircle( + center, + 20, + hoverPaint, + ); + } + + switch (value) { + case true: + final checkmarkPath = Path() + ..moveTo(size.width * 0.33437500, size.width * 0.50843750) + ..lineTo(size.width * 0.43750000, size.width * 0.60375000) + ..lineTo(size.width * 0.66562500, size.width * 0.38343750); + final checkmarkMetrics = checkmarkPath.computeMetrics().first; + canvas.drawPath( + checkmarkMetrics.extractPath( + 0, + checkmarkAnimationValue * checkmarkMetrics.length, + ), + foregroundPaint, + ); + case false: + break; + case null: + final dashPath = Path() + ..moveTo(size.width * 0.35, size.height * 0.5) + ..lineTo(size.width * 0.65, size.height * 0.5); + + final dashMetrics = dashPath.computeMetrics().first; + canvas.drawPath( + dashMetrics.extractPath(0, dashAnimationValue * dashMetrics.length), + foregroundPaint, + ); + } + } + + @override + bool shouldRepaint(covariant _CheckboxPainter old) { + return value != old.value || + checkmarkAnimationValue != old.checkmarkAnimationValue || + hover != old.hover || + dashAnimationValue != old.dashAnimationValue || + hoverColor != old.hoverColor || + outlineColor != old.outlineColor || + activeBackgroundColor != old.activeBackgroundColor || + inactiveBackgroundColor != old.inactiveBackgroundColor || + foregroundColor != old.foregroundColor; + } +} diff --git a/lib/src/components/context_menu/context_menu.dart b/lib/src/components/context_menu/context_menu.dart new file mode 100644 index 0000000..6b1f7d4 --- /dev/null +++ b/lib/src/components/context_menu/context_menu.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:zenit_ui/src/components/context_menu/context_menu_item.dart'; +import 'package:zenit_ui/src/constants/constants.dart'; + +class ZenitContextMenuData { + final List entries; + + const ZenitContextMenuData({required this.entries}); +} + +class ZenitContextMenuBuilder extends StatefulWidget { + final Widget Function( + BuildContext context, + MenuController controller, + Widget? child, + ) builder; + final ZenitContextMenuData contextMenu; + final MenuStyle? menuStyle; + final ButtonStyle? menuItemStyle; + final ButtonStyle? nestedMenuItemStyle; + final Widget? child; + + const ZenitContextMenuBuilder({ + required this.builder, + required this.contextMenu, + this.menuStyle, + this.menuItemStyle, + this.nestedMenuItemStyle, + this.child, + super.key, + }); + + @override + State createState() => _ZenitContextMenuBuilderState(); +} + +class _ZenitContextMenuBuilderState extends State { + ShortcutRegistryEntry? shortcutRegistryEntry; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final shortcuts = _buildShortcuts(widget.contextMenu.entries); + + if (shortcutRegistryEntry == null) { + shortcutRegistryEntry = ShortcutRegistry.of(context).addAll(shortcuts); + } else { + shortcutRegistryEntry!.replaceAll(shortcuts); + } + } + + @override + void didUpdateWidget(covariant ZenitContextMenuBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + shortcutRegistryEntry?.dispose(); + shortcutRegistryEntry = null; + super.dispose(); + } + + Widget _buildItem(ContextMenuEntry item) { + return switch (item) { + final ZenitContextMenuItem item => MenuItemButton( + shortcut: item.shortcut, + leadingIcon: item.leading, + trailingIcon: item.trailing, + style: widget.menuItemStyle ?? _menuItemStyle, + onPressed: item.onPressed, + child: Text(item.label), + ), + final ZenitNestedContextMenuItem nested => SubmenuButton( + leadingIcon: nested.leading, + menuChildren: nested.children.map(_buildItem).toList(), + alignmentOffset: const Offset(12, 0), + style: widget.nestedMenuItemStyle ?? _menuItemStyle, + menuStyle: widget.menuStyle ?? _menuStyle(context), + child: Text(nested.label), + ), + ZenitContextMenuDivider() => const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider( + thickness: 1, + endIndent: 8, + indent: 8, + ), + ), + }; + } + + Map _buildShortcuts( + List entries, + ) { + final result = {}; + result.clear(); + + for (final selection in entries) { + if (selection case ZenitNestedContextMenuItem()) { + result.addAll(_buildShortcuts(selection.children)); + } else if (selection case ZenitContextMenuItem()) { + if (selection.shortcut != null && selection.onPressed != null) { + result[selection.shortcut!] = VoidCallbackIntent(selection.onPressed!); + } + } + } + + return result; + } + + @override + Widget build(BuildContext context) { + return MenuAnchor( + menuChildren: widget.contextMenu.entries.map(_buildItem).toList(), + style: widget.menuStyle ?? _menuStyle(context), + alignmentOffset: const Offset(4, 4), + builder: widget.builder, + child: widget.child, + ); + } +} + +const _menuItemStyle = ButtonStyle( + shape: MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: kDefaultBorderRadiusMedium, + ), + ), + iconSize: MaterialStatePropertyAll(16), + elevation: MaterialStatePropertyAll(0), + visualDensity: VisualDensity(horizontal: -2, vertical: -3), +); + +MenuStyle _menuStyle(BuildContext context) { + return MenuStyle( + surfaceTintColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.surface, + ), + backgroundColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.surface, + ), + padding: const MaterialStatePropertyAll(EdgeInsets.all(6)), + shape: const MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: kDefaultBorderRadiusLarge, + ), + ), + side: MaterialStatePropertyAll( + BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + ), + elevation: const MaterialStatePropertyAll(0), + visualDensity: VisualDensity.standard, + ); +} diff --git a/lib/src/components/context_menu/context_menu_item.dart b/lib/src/components/context_menu/context_menu_item.dart new file mode 100644 index 0000000..513b5bf --- /dev/null +++ b/lib/src/components/context_menu/context_menu_item.dart @@ -0,0 +1,37 @@ +import 'package:zenit_ui/zenit_ui.dart'; + +sealed class ContextMenuEntry { + const ContextMenuEntry(); +} + +class ZenitContextMenuItem extends ContextMenuEntry { + final String label; + final MenuSerializableShortcut? shortcut; + final VoidCallback? onPressed; + final Widget? leading; + final Widget? trailing; + + const ZenitContextMenuItem({ + required this.label, + this.shortcut, + this.onPressed, + this.leading, + this.trailing, + }); +} + +class ZenitNestedContextMenuItem extends ContextMenuEntry { + final String label; + final List children; + final Widget? leading; + + const ZenitNestedContextMenuItem({ + required this.label, + required this.children, + this.leading, + }); +} + +class ZenitContextMenuDivider extends ContextMenuEntry { + const ZenitContextMenuDivider(); +} diff --git a/lib/src/components/context_menu/context_menu_region.dart b/lib/src/components/context_menu/context_menu_region.dart new file mode 100644 index 0000000..e4eb07c --- /dev/null +++ b/lib/src/components/context_menu/context_menu_region.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:zenit_ui/src/components/context_menu/context_menu.dart'; + +class ZenitContextMenuRegion extends StatelessWidget { + /// Creates an instance of [ZenitContextMenuRegion]. + const ZenitContextMenuRegion({ + required this.child, + required this.contextMenu, + }); + + /// Builds the context menu. + final ZenitContextMenuData contextMenu; + + /// The child widget that will be listened to for gestures. + final Widget child; + @override + Widget build(BuildContext context) { + return ZenitContextMenuBuilder( + contextMenu: contextMenu, + child: child, + builder: (context, controller, builderChild) => GestureDetector( + onTap: controller.isOpen ? controller.close : null, + onSecondaryTap: controller.isOpen ? controller.close : null, + onSecondaryTapUp: (_) => controller.isOpen + ? controller.close + : controller.open(position: _.localPosition), + child: builderChild, + ), + ); + } +} diff --git a/lib/src/components/dialog/dialog.dart b/lib/src/components/dialog/dialog.dart new file mode 100644 index 0000000..76ccf76 --- /dev/null +++ b/lib/src/components/dialog/dialog.dart @@ -0,0 +1,126 @@ +import 'package:zenit_ui/zenit_ui.dart'; + +class ZenitDialog extends AlertDialog { + const ZenitDialog({ + super.key, + super.icon, + super.iconPadding, + super.iconColor, + super.title, + super.titlePadding, + super.titleTextStyle, + super.content, + super.contentPadding, + super.contentTextStyle, + super.actions, + super.actionsPadding, + super.actionsAlignment, + super.actionsOverflowAlignment, + super.actionsOverflowDirection, + super.actionsOverflowButtonSpacing, + super.buttonPadding, + super.backgroundColor, + super.elevation, + super.shadowColor, + super.surfaceTintColor, + super.semanticLabel, + super.insetPadding = const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0), + super.clipBehavior = Clip.antiAlias, + super.shape, + super.alignment, + super.scrollable = false, + }); + + @override + Widget? get title => _ZenitDialogToolbar( + title: super.title, + padding: super.titlePadding ?? const EdgeInsets.symmetric(horizontal: 6), + leadingActions: super.icon != null + ? [ + Padding( + padding: super.iconPadding ?? const EdgeInsets.all(6), + child: super.icon, + ), + ] + : [], + ); + + @override + EdgeInsetsGeometry? get titlePadding => EdgeInsets.zero; + + @override + double? get elevation => 0; + + @override + EdgeInsetsGeometry? get contentPadding => const EdgeInsets.all(28); + + @override + Widget build(BuildContext context) { + return AlertDialog( + actions: super.actions, + actionsAlignment: super.actionsAlignment, + actionsOverflowAlignment: super.actionsOverflowAlignment, + actionsOverflowButtonSpacing: super.actionsOverflowButtonSpacing, + actionsOverflowDirection: super.actionsOverflowDirection, + actionsPadding: super.actionsPadding, + alignment: super.alignment, + backgroundColor: super.backgroundColor, + buttonPadding: super.buttonPadding, + clipBehavior: super.clipBehavior, + content: DefaultTextStyle( + style: Theme.of(context).textTheme.bodyMedium ?? const TextStyle(), + child: super.content ?? const SizedBox.shrink(), + ), + contentPadding: contentPadding, + contentTextStyle: super.contentTextStyle, + elevation: elevation, + insetPadding: super.insetPadding, + semanticLabel: super.semanticLabel, + scrollable: super.scrollable, + shadowColor: super.shadowColor, + shape: super.shape ?? + RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(10)), + side: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + ), + surfaceTintColor: super.surfaceTintColor, + title: title, + titlePadding: titlePadding, + titleTextStyle: super.titleTextStyle, + iconColor: super.iconColor, + iconPadding: super.iconPadding, + ); + } +} + +class _ZenitDialogToolbar extends ZenitToolbar { + const _ZenitDialogToolbar({ + super.title, + super.leadingActions, + super.padding, + }); + + @override + Widget build(BuildContext context) { + return ZenitToolbar( + backgroundColor: Colors.transparent, + title: super.title, + titleStyle: super.titleStyle, + centerTitle: super.centerTitle, + height: super.height, + leadingActions: super.leadingActions, + padding: super.padding, + trailingActions: [ + if (Navigator.canPop(context)) + ZenitIconButton( + icon: Icons.close, + onPressed: () => Navigator.pop(context), + buttonSize: 32, + iconSize: 16, + ), + ], + ); + } +} diff --git a/lib/src/components/icon_button/icon_button.dart b/lib/src/components/icon_button/icon_button.dart new file mode 100644 index 0000000..dbea90e --- /dev/null +++ b/lib/src/components/icon_button/icon_button.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:zenit_ui/src/constants/constants.dart'; + +class ZenitIconButton extends StatelessWidget { + const ZenitIconButton({ + super.key, + required this.icon, + this.hoverColor, + this.onPressed, + this.borderRadius = kDefaultBorderRadiusExtraLarge, + this.buttonSize = 40, + this.iconSize = 20, + }); + + final IconData? icon; + + final Color? hoverColor; + + final VoidCallback? onPressed; + + final BorderRadius borderRadius; + + final double buttonSize; + + final double iconSize; + + @override + Widget build(BuildContext context) { + return Material( + type: MaterialType.transparency, + child: InkWell( + onTap: onPressed, + borderRadius: borderRadius, + hoverColor: hoverColor, + child: SizedBox.square( + dimension: buttonSize, + child: Icon( + icon, + size: iconSize, + ), + ), + ), + ); + } +} diff --git a/lib/src/components/icon_button/zenit_icon_button.dart b/lib/src/components/icon_button/zenit_icon_button.dart deleted file mode 100644 index e608b9a..0000000 --- a/lib/src/components/icon_button/zenit_icon_button.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:zenit_ui/src/base/tick_animator.dart'; -import 'package:zenit_ui/src/constants/constants.dart'; - -class ZenitIconButton extends StatefulWidget { - const ZenitIconButton({ - super.key, - required this.icon, - this.hoverColor, - this.onPressed, - this.borderRadius = kDefaultBorderRadiusMedium, - }); - - final IconData? icon; - - final Color? hoverColor; - - final VoidCallback? onPressed; - - final BorderRadius borderRadius; - - @override - State createState() => ZenitIconButtonState(); -} - -class ZenitIconButtonState extends State with SingleTickerProviderStateMixin { - late AnimationController animationController; - late Animation animation; - - @override - void initState() { - animationController = AnimationController( - duration: const Duration(milliseconds: 300), - vsync: this, - ); - animation = Tween(begin: 1.0, end: 0.8).animate( - CurvedAnimation( - parent: animationController, - curve: Curves.fastOutSlowIn, - ), - )..addListener( - () => setState(() {}), - ); - super.initState(); - } - - Color backgroundColor = Colors.transparent; - - @override - Widget build(BuildContext context) { - return TickAnimator( - onPressed: widget.onPressed, - borderRadius: widget.borderRadius, - child: PhysicalModel( - clipBehavior: Clip.antiAlias, - color: backgroundColor, - child: Padding( - padding: const EdgeInsets.all(8), - child: Icon( - widget.icon, - ), - ), - ), - ); - } - - void handleHover(dynamic event) { - if (event is PointerEnterEvent) { - setState(() { - backgroundColor = widget.hoverColor ?? Theme.of(context).colorScheme.surface; - }); - } else { - setState(() { - backgroundColor = Colors.transparent; - }); - } - } -} diff --git a/lib/src/components/list_tile/checkbox_list_tile.dart b/lib/src/components/list_tile/checkbox_list_tile.dart new file mode 100644 index 0000000..a153c29 --- /dev/null +++ b/lib/src/components/list_tile/checkbox_list_tile.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:zenit_ui/src/components/checkbox/checkbox.dart'; +import 'package:zenit_ui/src/theme/theme.dart'; + +class ZenitCheckboxListTile extends StatelessWidget { + const ZenitCheckboxListTile({ + super.key, + required this.value, + required this.onChanged, + this.tristate = false, + this.checkboxTheme, + this.tileColor, + this.activeThumbImage, + this.inactiveThumbImage, + this.title, + this.subtitle, + this.secondary, + this.isThreeLine = false, + this.dense, + this.contentPadding, + this.selected = false, + this.autofocus = false, + this.controlAffinity = ListTileControlAffinity.platform, + this.shape, + this.selectedTileColor, + this.visualDensity, + this.focusNode, + this.enableFeedback, + this.hoverColor, + }) : assert(!isThreeLine || subtitle != null); + + final bool? value; + + final ValueChanged? onChanged; + + final bool tristate; + + final ZenitCheckboxTheme? checkboxTheme; + + final Color? tileColor; + + final ImageProvider? activeThumbImage; + + final ImageProvider? inactiveThumbImage; + + final Widget? title; + + final Widget? subtitle; + + final Widget? secondary; + + final bool isThreeLine; + + final bool? dense; + + final EdgeInsetsGeometry? contentPadding; + + final bool selected; + + final bool autofocus; + + final ListTileControlAffinity controlAffinity; + + final ShapeBorder? shape; + + final Color? selectedTileColor; + + final VisualDensity? visualDensity; + + final FocusNode? focusNode; + + final bool? enableFeedback; + + final Color? hoverColor; + + void handleTap() { + if (onChanged == null) return; + switch (value) { + case false: + onChanged!(true); + case true: + onChanged!(tristate ? null : false); + case null: + onChanged!(false); + } + } + + @override + Widget build(BuildContext context) { + return ListTile( + trailing: ZenitCheckbox( + value: value, + onChanged: onChanged, + tristate: tristate, + theme: checkboxTheme, + ), + title: title, + subtitle: subtitle, + leading: secondary, + isThreeLine: isThreeLine, + dense: dense, + contentPadding: contentPadding, + selected: selected, + autofocus: autofocus, + shape: shape, + selectedTileColor: selectedTileColor, + visualDensity: visualDensity, + focusNode: focusNode, + enableFeedback: enableFeedback, + hoverColor: hoverColor, + onTap: handleTap, + ); + } +} diff --git a/lib/src/components/list_tile/radio_list_tile.dart b/lib/src/components/list_tile/radio_list_tile.dart new file mode 100644 index 0000000..c823991 --- /dev/null +++ b/lib/src/components/list_tile/radio_list_tile.dart @@ -0,0 +1,102 @@ +import 'package:flutter/material.dart'; +import 'package:zenit_ui/src/components/radio_button/radio_button.dart'; +import 'package:zenit_ui/src/theme/theme.dart'; + +class ZenitRadioButtonListTile extends StatelessWidget { + const ZenitRadioButtonListTile({ + super.key, + required this.value, + required this.groupValue, + required this.onChanged, + this.radioButtonTheme, + this.tileColor, + this.activeThumbImage, + this.inactiveThumbImage, + this.title, + this.subtitle, + this.secondary, + this.isThreeLine = false, + this.dense, + this.contentPadding, + this.selected = false, + this.autofocus = false, + this.controlAffinity = ListTileControlAffinity.platform, + this.shape, + this.selectedTileColor, + this.visualDensity, + this.focusNode, + this.enableFeedback, + this.hoverColor, + }) : assert(!isThreeLine || subtitle != null); + + final T? value; + + final T? groupValue; + + final ValueChanged? onChanged; + + final ZenitRadioButtonTheme? radioButtonTheme; + + final Color? tileColor; + + final ImageProvider? activeThumbImage; + + final ImageProvider? inactiveThumbImage; + + final Widget? title; + + final Widget? subtitle; + + final Widget? secondary; + + final bool isThreeLine; + + final bool? dense; + + final EdgeInsetsGeometry? contentPadding; + + final bool selected; + + final bool autofocus; + + final ListTileControlAffinity controlAffinity; + + final ShapeBorder? shape; + + final Color? selectedTileColor; + + final VisualDensity? visualDensity; + + final FocusNode? focusNode; + + final bool? enableFeedback; + + final Color? hoverColor; + + @override + Widget build(BuildContext context) { + return ListTile( + trailing: ZenitRadioButton( + value: value, + groupValue: groupValue, + onChanged: onChanged, + theme: radioButtonTheme, + ), + title: title, + subtitle: subtitle, + leading: secondary, + isThreeLine: isThreeLine, + dense: dense, + contentPadding: contentPadding, + selected: selected, + autofocus: autofocus, + shape: shape, + selectedTileColor: selectedTileColor, + visualDensity: visualDensity, + focusNode: focusNode, + enableFeedback: enableFeedback, + hoverColor: hoverColor, + onTap: onChanged != null ? () => onChanged?.call(value) : null, + ); + } +} diff --git a/lib/src/components/list_tile/switch_list_tile.dart b/lib/src/components/list_tile/switch_list_tile.dart index ac23fd4..d8f1004 100644 --- a/lib/src/components/list_tile/switch_list_tile.dart +++ b/lib/src/components/list_tile/switch_list_tile.dart @@ -1,15 +1,13 @@ import 'package:flutter/material.dart'; import 'package:zenit_ui/src/components/switch/switch.dart'; +import 'package:zenit_ui/src/theme/theme.dart'; class ZenitSwitchListTile extends StatelessWidget { const ZenitSwitchListTile({ super.key, required this.value, this.onChanged, - /* this.activeColor, - this.activeTrackColor, - this.inactiveThumbColor, - this.inactiveTrackColor, */ + this.switchTheme, this.tileColor, this.activeThumbImage, this.inactiveThumbImage, @@ -34,13 +32,7 @@ class ZenitSwitchListTile extends StatelessWidget { final ValueChanged? onChanged; - //final Color? activeColor; - - //final Color? activeTrackColor; - - //final Color? inactiveThumbColor; - - //final Color? inactiveTrackColor; + final ZenitSwitchTheme? switchTheme; final Color? tileColor; @@ -81,7 +73,11 @@ class ZenitSwitchListTile extends StatelessWidget { @override Widget build(BuildContext context) { return ListTile( - trailing: ZenitSwitch(value: value, onChanged: onChanged), + trailing: ZenitSwitch( + value: value, + onChanged: onChanged, + theme: switchTheme, + ), title: title, subtitle: subtitle, leading: secondary, diff --git a/lib/src/components/pill_icon/zenit_pill_icon.dart b/lib/src/components/pill_icon/zenit_pill_icon.dart deleted file mode 100644 index 6392c20..0000000 --- a/lib/src/components/pill_icon/zenit_pill_icon.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:zenit_ui/src/constants/constants.dart'; - -class ZenitPillIcon extends StatelessWidget { - const ZenitPillIcon({ - super.key, - this.selected = false, - required this.child, - }); - - final bool selected; - final Widget? child; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return SizedBox( - width: 52, - height: 28, - child: DecoratedBox( - decoration: BoxDecoration( - color: selected ? theme.primaryColor : Colors.transparent, - borderRadius: kDefaultBorderRadiusBig, - ), - child: IconTheme( - data: IconTheme.of(context).copyWith( - color: selected ? theme.colorScheme.background : null, - ), - child: child ?? const SizedBox(), - ), - ), - ); - } -} diff --git a/lib/src/components/radio_button/radio_button.dart b/lib/src/components/radio_button/radio_button.dart index f5b60e6..408d34a 100644 --- a/lib/src/components/radio_button/radio_button.dart +++ b/lib/src/components/radio_button/radio_button.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:zenit_ui/src/base/tick_animator.dart'; import 'package:zenit_ui/src/constants/constants.dart'; import 'package:zenit_ui/src/theme/theme.dart'; -class ZenitRadioButton extends StatelessWidget { +class ZenitRadioButton extends StatefulWidget { final T? value; final T? groupValue; final ValueChanged? onChanged; @@ -16,41 +15,149 @@ class ZenitRadioButton extends StatelessWidget { this.theme, }); + @override + State> createState() => _ZenitRadioButtonState(); +} + +class _ZenitRadioButtonState extends State> with SingleTickerProviderStateMixin { + late Animation animation; + late AnimationController controller; + + @override + void initState() { + super.initState(); + controller = AnimationController( + duration: kDefaultAnimationDuration, + vsync: this, + value: widget.value == widget.groupValue ? 1 : 0, + ); + animation = Tween(begin: 0.0, end: 1.0).animate(controller); + } + + @override + void didUpdateWidget(covariant ZenitRadioButton oldWidget) { + controller.animateTo( + widget.value == widget.groupValue ? 1 : 0, + curve: Curves.easeOut, + ); + if (widget == oldWidget) return; + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + bool hover = false; + @override Widget build(BuildContext context) { - final theme = this.theme ?? ZenitTheme.radioButtonTheme(context); + final theme = widget.theme ?? ZenitTheme.radioButtonTheme(context); final activeBackgroundColor = theme.activeBackgroundColor; final inactiveBackgroundColor = theme.inactiveBackgroundColor; - final activeThumbColor = theme.activeThumbColor; - final inactiveThumbColor = theme.inactiveThumbColor; - final selected = value == groupValue; - return TickAnimator( - onPressed: () => onChanged?.call(value), - borderRadius: BorderRadius.circular(24), - child: PhysicalModel( - borderRadius: kDefaultBorderRadiusBig, - clipBehavior: Clip.antiAlias, - color: switch (selected) { - true => activeBackgroundColor, - false => inactiveBackgroundColor, - }, - child: SizedBox( - width: 24, - height: 24, - child: Padding( - padding: const EdgeInsets.all(5.0), - child: DecoratedBox( - decoration: BoxDecoration( - color: switch (selected) { - true => activeThumbColor, - false => inactiveThumbColor, - }, - borderRadius: kDefaultBorderRadiusBig, - ), - ), + final thumbColor = theme.thumbColor; + final outlineColor = theme.outlineColor; + + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => hover = true), + onExit: (_) => setState(() => hover = false), + child: GestureDetector( + onTap: () => widget.onChanged?.call(widget.value), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: AnimatedBuilder( + animation: animation, + builder: (context, child) { + return CustomPaint( + painter: _RadioPainter( + selected: widget.value == widget.groupValue, + activeBackgroundColor: activeBackgroundColor, + inactiveBackgroundColor: inactiveBackgroundColor, + thumbColor: thumbColor, + animationValue: animation.value, + hover: hover, + hoverColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), + outlineColor: outlineColor, + ), + size: const Size.square(32), + ); + }, ), ), ), ); } } + +class _RadioPainter extends CustomPainter { + final bool selected; + final Color activeBackgroundColor; + final Color inactiveBackgroundColor; + final Color thumbColor; + final bool hover; + final Color hoverColor; + final Color outlineColor; + + final double animationValue; + + const _RadioPainter({ + required this.selected, + required this.activeBackgroundColor, + required this.inactiveBackgroundColor, + required this.thumbColor, + required this.animationValue, + required this.hover, + required this.hoverColor, + required this.outlineColor, + }); + + @override + void paint(Canvas canvas, Size size) { + final Paint backgroundPaint = Paint() + ..color = selected ? activeBackgroundColor : inactiveBackgroundColor + ..style = PaintingStyle.fill; + + final Paint outlinePaint = Paint() + ..color = selected ? activeBackgroundColor : outlineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + + final Paint thumbPaint = Paint() + ..color = thumbColor + ..style = PaintingStyle.fill; + + final Offset center = Offset(size.height / 2, size.width / 2); + + // Draw Background + canvas.drawCircle(center, 12, backgroundPaint); + canvas.drawCircle(center, 12, outlinePaint); + + // Draw hover + if (hover) { + final hoverPaint = Paint() + ..color = hoverColor + ..style = PaintingStyle.fill; + canvas.drawCircle(center, 20, hoverPaint); + } + + // Draw thumb + if (animationValue != 0) { + canvas.drawCircle(center, animationValue * 6, thumbPaint); + } + } + + @override + bool shouldRepaint(covariant _RadioPainter old) { + return selected != old.selected || + animationValue != old.animationValue || + hover != old.hover || + hoverColor != old.hoverColor || + outlineColor != old.outlineColor || + activeBackgroundColor != old.activeBackgroundColor || + inactiveBackgroundColor != old.inactiveBackgroundColor || + thumbColor != old.thumbColor; + } +} diff --git a/lib/src/components/section/section.dart b/lib/src/components/section/section.dart new file mode 100644 index 0000000..828724a --- /dev/null +++ b/lib/src/components/section/section.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:zenit_ui/src/constants/constants.dart'; + +class ZenitSection extends StatelessWidget { + final Color? color; + final BorderRadiusGeometry? borderRadius; + final Widget child; + + const ZenitSection({ + super.key, + this.color, + this.borderRadius, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4.0), + child: Material( + color: color ?? Theme.of(context).colorScheme.background, + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: borderRadius ?? kCardBorderRadius, + side: BorderSide( + color: Theme.of(context).colorScheme.outline, + ), + ), + child: child, + ), + ); + } +} diff --git a/lib/src/components/slider/slider.dart b/lib/src/components/slider/slider.dart index 7f612aa..2340883 100644 --- a/lib/src/components/slider/slider.dart +++ b/lib/src/components/slider/slider.dart @@ -1,76 +1,156 @@ import 'package:flutter/material.dart'; import 'package:zenit_ui/src/theme/theme.dart'; -class ZenitSlider extends StatelessWidget { +class ZenitSlider extends StatefulWidget { final double value; final ValueChanged onChanged; - final double? height; final Color? activeColor; - final bool autofocus; - final int? divisions; - final FocusNode? focusNode; - final Color? inactiveColor; - final String? label; - final double max; - final double min; + // TODO: implement divisions + //final int? divisions; + final Color? trackColor; final MouseCursor? mouseCursor; - final ValueChanged? onChangeEnd; - final ValueChanged? onChangeStart; - final SemanticFormatterCallback? semanticFormatterCallback; final Color? thumbColor; const ZenitSlider({ super.key, required this.value, required this.onChanged, - this.height = 8.0, this.activeColor, - this.autofocus = false, - this.divisions, - this.focusNode, - this.inactiveColor, - this.label, - this.max = 1.0, - this.min = 0.0, + //this.divisions, + this.trackColor, this.mouseCursor, - this.onChangeEnd, - this.onChangeStart, - this.semanticFormatterCallback, this.thumbColor, - }); + }) : assert(value >= 0.0 && value <= 1.0); + + @override + State createState() => _ZenitSliderState(); +} +class _ZenitSliderState extends State { + bool hover = false; @override Widget build(BuildContext context) { - final ZenitSliderTheme sliderTheme = ZenitTheme.sliderTheme(context); - return SliderTheme( - data: SliderThemeData( - trackHeight: height, - thumbShape: RoundSliderThumbShape( - elevation: 0.0, - enabledThumbRadius: height! * 1.5, - disabledThumbRadius: height! * 1.5, - pressedElevation: 0.0, - ), - ), - child: Slider( - value: value, - onChanged: onChanged, - activeColor: activeColor ?? sliderTheme.activeTrackColor, - autofocus: autofocus, - divisions: divisions, - focusNode: focusNode, - inactiveColor: inactiveColor ?? sliderTheme.trackColor, - key: key, - label: label, - max: max, - min: min, - mouseCursor: mouseCursor, - onChangeEnd: onChangeEnd, - onChangeStart: onChangeStart, - semanticFormatterCallback: semanticFormatterCallback, - thumbColor: thumbColor, - ), + final sliderTheme = ZenitTheme.sliderTheme(context); + double newValue = widget.value; + return LayoutBuilder( + builder: (context, constraints) { + return MouseRegion( + cursor: widget.mouseCursor ?? SystemMouseCursors.click, + onEnter: (_) => setState(() => hover = true), + onExit: (_) => setState(() => hover = false), + child: Listener( + onPointerPanZoomStart: (details) { + newValue = details.localPosition.dx / (constraints.maxWidth); + widget.onChanged(newValue); + }, + child: GestureDetector( + onTapDown: (details) { + newValue = details.localPosition.dx / (constraints.maxWidth); + widget.onChanged(newValue); + }, + onHorizontalDragUpdate: (details) { + newValue += details.delta.dx / constraints.maxWidth; + if (newValue >= 0.0 && newValue <= 1.0) { + widget.onChanged(newValue); + } + }, + child: CustomPaint( + painter: _SliderPainter( + trackColor: widget.trackColor ?? sliderTheme.trackColor, + activeColor: widget.activeColor ?? sliderTheme.activeTrackColor, + thumbColor: widget.thumbColor ?? sliderTheme.thumbColor, + hover: hover, + hoverColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), + outlineColor: sliderTheme.outlineColor, + value: widget.value, + ), + size: Size(constraints.maxWidth, 48), + ), + ), + ), + ); + }, + ); + } +} + +class _SliderPainter extends CustomPainter { + final Color trackColor; + final Color activeColor; + final double value; + final bool hover; + final Color hoverColor; + final Color thumbColor; + final Color outlineColor; + + _SliderPainter({ + required this.trackColor, + required this.activeColor, + required this.value, + required this.hover, + required this.hoverColor, + required this.thumbColor, + required this.outlineColor, + }); + + @override + void paint(Canvas canvas, Size size) { + final Paint trackPaint = Paint() + ..color = trackColor + ..style = PaintingStyle.fill; + + final Paint activePaint = Paint() + ..color = activeColor + ..style = PaintingStyle.fill; + + final Paint thumbPaint = Paint() + ..color = thumbColor + ..style = PaintingStyle.fill; + + final Paint outlinePaint = Paint() + ..color = outlineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + + final RRect track = RRect.fromLTRBR(0.0, 12.0, size.width, size.height - 12, const Radius.circular(12.0)); + final RRect active = RRect.fromLTRBR( + 0.0, + 12.0, + (size.width * value) > 24 ? (size.width * value) : 24.0, + size.height - 12.0, + const Radius.circular(12.0), ); + + final Offset thumbPosition = Offset( + (size.width * value) > 24 ? (size.width * value) - ((size.height - 26) / 3) - 6 : 24 - 12, + size.height / 2, + ); + + canvas.drawRRect(track, trackPaint); + canvas.drawRRect(track, outlinePaint); + + canvas.drawRRect(active, activePaint); + + if (hover) { + final Paint hoverPaint = Paint() + ..color = hoverColor + ..style = PaintingStyle.fill; + + canvas.drawCircle(thumbPosition, 20, hoverPaint); + } + + canvas.drawCircle(thumbPosition, (size.height - 24) / 3, thumbPaint); + } + + @override + bool shouldRepaint(covariant _SliderPainter old) { + return old.value != value || + old.activeColor != activeColor || + old.trackColor != trackColor || + old.hover != hover || + old.hoverColor != hoverColor || + old.thumbColor != thumbColor || + old.outlineColor != outlineColor; } } diff --git a/lib/src/components/switch/switch.dart b/lib/src/components/switch/switch.dart index 2a79f87..0572ff4 100644 --- a/lib/src/components/switch/switch.dart +++ b/lib/src/components/switch/switch.dart @@ -1,7 +1,7 @@ //Credits: @HrX03 - for the base (which was slightly altered) import 'package:flutter/material.dart'; -import 'package:zenit_ui/src/base/tick_animator.dart'; +import 'package:zenit_ui/src/constants/constants.dart'; import 'package:zenit_ui/src/theme/theme.dart'; class ZenitSwitch extends StatefulWidget { @@ -23,14 +23,9 @@ class ZenitSwitch extends StatefulWidget { class _ZenitSwitchState extends State with TickerProviderStateMixin { late final AnimationController _positionController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 150), + duration: kDefaultAnimationDuration, value: widget.value ? 1 : 0, ); - final AlignmentGeometryTween _thumbPositionTween = AlignmentGeometryTween( - begin: AlignmentDirectional.centerStart, - end: AlignmentDirectional.centerEnd, - ); - //Set get _states => {if (widget.value) MaterialState.selected}; @override void initState() { @@ -47,6 +42,14 @@ class _ZenitSwitchState extends State with TickerProviderStateMixin super.didUpdateWidget(old); } + @override + void dispose() { + _positionController.dispose(); + super.dispose(); + } + + bool hover = false; + @override Widget build(BuildContext context) { final ZenitSwitchTheme switchTheme = widget.theme ?? ZenitTheme.switchTheme(context); @@ -54,49 +57,37 @@ class _ZenitSwitchState extends State with TickerProviderStateMixin final Color inactiveTrackColor = switchTheme.inactiveTrackColor; final Color activeThumbColor = switchTheme.activeThumbColor; final Color inactiveThumbColor = switchTheme.inactiveThumbColor; + final Color outlineColor = switchTheme.outlineColor; return MouseRegion( cursor: SystemMouseCursors.click, - child: TickAnimator( - borderRadius: BorderRadius.circular(24), - onPressed: () { - widget.onChanged?.call(!widget.value); + onEnter: (_) => setState(() => hover = true), + onExit: (_) => setState(() => hover = false), + child: GestureDetector( + onHorizontalDragUpdate: (details) { + _positionController.value += details.primaryDelta! / 48; }, - child: GestureDetector( - onHorizontalDragUpdate: (details) { - _positionController.value += details.primaryDelta! / 48; - }, - onHorizontalDragEnd: (details) { - widget.onChanged?.call(_positionController.value > 0.5); - }, - child: SizedBox( - width: 48, - height: 24, - child: DecoratedBox( - decoration: ShapeDecoration( - shape: const StadiumBorder(), - color: widget.value ? activeTrackColor : inactiveTrackColor, - ), - child: Padding( - padding: const EdgeInsets.all(4), - child: AnimatedBuilder( - animation: _positionController, - builder: (context, child) { - return Align( - alignment: _thumbPositionTween.evaluate(_positionController)!, - child: child, - ); - }, - child: Container( - width: 16, - height: 16, - decoration: ShapeDecoration( - shape: const CircleBorder(), - color: widget.value ? activeThumbColor : inactiveThumbColor, - ), - ), - ), + onHorizontalDragEnd: (details) { + widget.onChanged?.call(_positionController.value > 0.5); + }, + onTap: () => widget.onChanged?.call(!widget.value), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: AnimatedBuilder( + animation: _positionController, + builder: (context, child) => CustomPaint( + painter: _SwitchPainter( + value: widget.value, + activeTrackColor: activeTrackColor, + inactiveTrackColor: inactiveTrackColor, + activeThumbColor: activeThumbColor, + inactiveThumbColor: inactiveThumbColor, + positionValue: _positionController.value, + hover: hover, + hoverColor: Theme.of(context).colorScheme.onSurface.withOpacity(0.05), + outlineColor: outlineColor, ), + size: const Size(44, 24), ), ), ), @@ -104,3 +95,76 @@ class _ZenitSwitchState extends State with TickerProviderStateMixin ); } } + +class _SwitchPainter extends CustomPainter { + final bool value; + final Color activeTrackColor; + final Color inactiveTrackColor; + final Color activeThumbColor; + final Color inactiveThumbColor; + final double positionValue; + final bool hover; + final Color hoverColor; + final Color outlineColor; + + const _SwitchPainter({ + required this.value, + required this.activeTrackColor, + required this.inactiveTrackColor, + required this.activeThumbColor, + required this.inactiveThumbColor, + required this.positionValue, + required this.hover, + required this.hoverColor, + required this.outlineColor, + }); + + @override + void paint(Canvas canvas, Size size) { + const shape = StadiumBorder(); + final center = Offset(size.width / 2, size.height / 2); + final rect = Rect.fromCenter(center: center, width: 44, height: 24); + final trackPath = shape.getOuterPath(rect); + + final Paint trackPaint = Paint() + ..color = value ? activeTrackColor : inactiveTrackColor + ..style = PaintingStyle.fill; + + final Paint outlinePaint = Paint() + ..color = value ? activeTrackColor : outlineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + + final Paint thumbPaint = Paint() + ..color = value ? activeThumbColor : inactiveThumbColor + ..style = PaintingStyle.fill; + + canvas.drawPath(trackPath, trackPaint); + canvas.drawPath(trackPath, outlinePaint); + + final Offset thumbPosition = Offset(12 + (20 * positionValue), size.height / 2); + + if (hover) { + final Paint hoverPaint = Paint() + ..color = hoverColor + ..style = PaintingStyle.fill; + + canvas.drawCircle(thumbPosition, 20, hoverPaint); + } + + canvas.drawCircle(thumbPosition, size.height / 3, thumbPaint); + } + + @override + bool shouldRepaint(covariant _SwitchPainter old) { + return value != old.value || + positionValue != old.positionValue || + hover != old.hover || + outlineColor != old.outlineColor || + activeTrackColor != old.activeTrackColor || + inactiveTrackColor != old.inactiveTrackColor || + activeThumbColor != old.activeThumbColor || + inactiveThumbColor != old.inactiveThumbColor || + hoverColor != old.hoverColor; + } +} diff --git a/lib/src/components/textfield/text_flield.dart b/lib/src/components/textfield/text_flield.dart index 32aa490..9dc053a 100644 --- a/lib/src/components/textfield/text_flield.dart +++ b/lib/src/components/textfield/text_flield.dart @@ -4,7 +4,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:zenit_ui/src/constants/constants.dart'; -import 'package:zenit_ui/src/theme/theme.dart'; export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType, TextCapitalization, TextInputAction, TextInputType; @@ -67,8 +66,10 @@ class ZenitTextField extends StatelessWidget { this.contextMenuBuilder, this.hint, }) : assert(obscuringCharacter.length == 1), - smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), - smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), + smartDashesType = + smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), + smartQuotesType = + smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), assert(maxLines == null || maxLines > 0), assert(minLines == null || minLines > 0), assert( @@ -261,7 +262,7 @@ class ZenitTextField extends StatelessWidget { smartDashesType: smartDashesType, smartQuotesType: smartQuotesType, strutStyle: strutStyle, - style: const TextStyle(fontSize: 15).merge(style), + style: const TextStyle(fontSize: 14).merge(style), textAlign: textAlign, textAlignVertical: textAlignVertical, textCapitalization: textCapitalization, @@ -272,11 +273,8 @@ class ZenitTextField extends StatelessWidget { } } -const double _kDefaultBorderWidth = 2.0; - const BorderSide _kDefaultBorderSideInactive = BorderSide( color: Colors.transparent, - width: _kDefaultBorderWidth, ); const OutlineInputBorder _kDefaultOutlineInputBorderInactive = OutlineInputBorder( @@ -287,23 +285,35 @@ const OutlineInputBorder _kDefaultOutlineInputBorderInactive = OutlineInputBorde InputDecorationTheme zenitInputDecorationTheme(ThemeData theme) { final BorderSide kDefaultBorderSideActive = BorderSide( color: theme.primaryColor, - width: _kDefaultBorderWidth, + width: 2, + ); + + final BorderSide kDefaultBorderSideNormal = BorderSide( + color: theme.colorScheme.outline, ); final kDefaultInputDecorationTheme = InputDecorationTheme( filled: true, - fillColor: theme.elementColor, - border: _kDefaultOutlineInputBorderInactive, - enabledBorder: _kDefaultOutlineInputBorderInactive, + fillColor: theme.colorScheme.surface, + border: OutlineInputBorder( + borderSide: kDefaultBorderSideActive, + borderRadius: kDefaultBorderRadiusMedium, + ), + enabledBorder: OutlineInputBorder( + borderSide: kDefaultBorderSideNormal, + borderRadius: kDefaultBorderRadiusMedium, + ), disabledBorder: _kDefaultOutlineInputBorderInactive, focusedBorder: OutlineInputBorder( borderSide: kDefaultBorderSideActive, borderRadius: kDefaultBorderRadiusMedium, ), - labelStyle: TextStyle(color: theme.foregroundColor.withOpacity(0.75)), - floatingLabelBehavior: FloatingLabelBehavior.never, - alignLabelWithHint: true, - contentPadding: const EdgeInsets.symmetric(horizontal: 16), + labelStyle: TextStyle(color: theme.colorScheme.onSurface.withOpacity(0.75)), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + isDense: true, ); return kDefaultInputDecorationTheme; diff --git a/lib/src/constants/constants.dart b/lib/src/constants/constants.dart index c04fa64..a1ac197 100644 --- a/lib/src/constants/constants.dart +++ b/lib/src/constants/constants.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; const kDefaultBorderRadiusSmall = BorderRadius.all(Radius.circular(6.0)); const kDefaultBorderRadiusMedium = BorderRadius.all(Radius.circular(8.0)); -const kDefaultBorderRadiusBig = BorderRadius.all(Radius.circular(24.0)); +const kDefaultBorderRadiusLarge = BorderRadius.all(Radius.circular(12.0)); +const kDefaultBorderRadiusExtraLarge = BorderRadius.all(Radius.circular(24.0)); const kCardBorderRadius = BorderRadius.all(Radius.circular(12.0)); -const kDefaultPageMargin = EdgeInsets.all(8.0); - -const kDefaultAnimationDuration = Duration(milliseconds: 300); +const kDefaultAnimationDuration = Duration(milliseconds: 150); const kDefaultElevation = 0.0; diff --git a/lib/src/extensions/extensions.dart b/lib/src/extensions/extensions.dart index 01033ac..b621566 100644 --- a/lib/src/extensions/extensions.dart +++ b/lib/src/extensions/extensions.dart @@ -1,4 +1,4 @@ -import 'dart:ui'; +import 'package:flutter/material.dart'; extension ObX on Object? { bool get isNull => this == null; @@ -7,4 +7,29 @@ extension ObX on Object? { extension ColorX on Color { Color mix(Color foregrounnd) => Color.lerp(this, foregrounnd, 0.5) ?? this; + + Color darken([double amount = 0.1]) { + assert(amount >= 0 && amount <= 1); + return Color.alphaBlend(Colors.black.withOpacity(amount), this); + } + + Color brighten([double amount = 0.1]) { + assert(amount >= 0 && amount <= 1); + return Color.alphaBlend(Colors.white.withOpacity(amount), this); + } + + Color themedLightness(BuildContext context, [double amount = 0.1]) { + assert(amount >= 0 && amount <= 1); + return Theme.of(context).brightness == Brightness.dark + ? brighten(amount) + : darken(amount); + } + + Color themedLightnessFromBrightness( + Brightness brightness, [ + double amount = 0.1, + ]) { + assert(amount >= 0 && amount <= 1); + return brightness == Brightness.dark ? brighten(amount) : darken(amount); + } } diff --git a/lib/src/extensions/hover_builder.dart b/lib/src/extensions/hover_builder.dart new file mode 100644 index 0000000..07e60fe --- /dev/null +++ b/lib/src/extensions/hover_builder.dart @@ -0,0 +1,37 @@ +import 'package:zenit_ui/zenit_ui.dart'; + +class HoverBuilder extends StatefulWidget { + final Widget Function(BuildContext, bool, Widget?) builder; + final Widget? child; + final MouseCursor? cursor; + + const HoverBuilder({ + super.key, + required this.builder, + this.child, + this.cursor, + }); + + @override + _HoverBuilderState createState() => _HoverBuilderState(); +} + +class _HoverBuilderState extends State { + bool isHovering = false; + + @override + void dispose() { + isHovering = false; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: widget.cursor ?? MouseCursor.defer, + onEnter: (_) => setState(() => isHovering = true), + onExit: (_) => setState(() => isHovering = false), + child: widget.builder(context, isHovering, widget.child), + ); + } +} diff --git a/lib/src/fonts/Inter-Black.ttf b/lib/src/fonts/Inter-Black.ttf new file mode 100644 index 0000000..5aecf7d Binary files /dev/null and b/lib/src/fonts/Inter-Black.ttf differ diff --git a/lib/src/fonts/Inter-Bold.ttf b/lib/src/fonts/Inter-Bold.ttf new file mode 100644 index 0000000..8e82c70 Binary files /dev/null and b/lib/src/fonts/Inter-Bold.ttf differ diff --git a/lib/src/fonts/Inter-ExtraBold.ttf b/lib/src/fonts/Inter-ExtraBold.ttf new file mode 100644 index 0000000..cb4b821 Binary files /dev/null and b/lib/src/fonts/Inter-ExtraBold.ttf differ diff --git a/lib/src/fonts/Inter-ExtraLight.ttf b/lib/src/fonts/Inter-ExtraLight.ttf new file mode 100644 index 0000000..64aee30 Binary files /dev/null and b/lib/src/fonts/Inter-ExtraLight.ttf differ diff --git a/lib/src/fonts/Inter-Light.ttf b/lib/src/fonts/Inter-Light.ttf new file mode 100644 index 0000000..9e265d8 Binary files /dev/null and b/lib/src/fonts/Inter-Light.ttf differ diff --git a/lib/src/fonts/Inter-Medium.ttf b/lib/src/fonts/Inter-Medium.ttf new file mode 100644 index 0000000..b53fb1c Binary files /dev/null and b/lib/src/fonts/Inter-Medium.ttf differ diff --git a/lib/src/fonts/Inter-Regular.ttf b/lib/src/fonts/Inter-Regular.ttf new file mode 100644 index 0000000..8d4eebf Binary files /dev/null and b/lib/src/fonts/Inter-Regular.ttf differ diff --git a/lib/src/fonts/Inter-SemiBold.ttf b/lib/src/fonts/Inter-SemiBold.ttf new file mode 100644 index 0000000..c6aeeb1 Binary files /dev/null and b/lib/src/fonts/Inter-SemiBold.ttf differ diff --git a/lib/src/fonts/Inter-Thin.ttf b/lib/src/fonts/Inter-Thin.ttf new file mode 100644 index 0000000..7aed55d Binary files /dev/null and b/lib/src/fonts/Inter-Thin.ttf differ diff --git a/lib/src/layout/layout_page/constraints/zenit_landscape_layout.dart b/lib/src/layout/layout_page/constraints/zenit_landscape_layout.dart index e881447..3a75e30 100644 --- a/lib/src/layout/layout_page/constraints/zenit_landscape_layout.dart +++ b/lib/src/layout/layout_page/constraints/zenit_landscape_layout.dart @@ -1,8 +1,5 @@ -import 'package:flutter/material.dart'; -import 'package:zenit_ui/src/constants/constants.dart'; import 'package:zenit_ui/src/layout/layout_page/list_view/zenit_layout_destination_list_view.dart'; -import 'package:zenit_ui/src/layout/layout_page/zenit_navigation_layout.dart'; -import 'package:zenit_ui/src/layout/navigator/zenit_navigator_observer.dart'; +import 'package:zenit_ui/zenit_ui.dart'; class ZenitLandscapeLayout extends StatefulWidget { const ZenitLandscapeLayout({ @@ -13,15 +10,17 @@ class ZenitLandscapeLayout extends StatefulWidget { required this.onPageSelected, required this.destinationBuilder, this.controller, - this.appBar, this.globalFloatingActionButton, - this.margin = kDefaultPageMargin, + this.sidebarColor, + this.sidebarWidth = 256, + this.sidebarToolbar, + this.pageToolbarBuilder, }); /// The number of pages in the [ZenitLandscapeLayout]. final int length; - final ZenitNavigationLayoutBuilder destinationBuilder; + final ZenitNavigationSidebarBuilder destinationBuilder; /// The index of the selected page. final int selectedIndex; @@ -35,14 +34,20 @@ class ZenitLandscapeLayout extends StatefulWidget { /// A controller that can control the index of the [ZenitLandscapeLayout]. final ValueNotifier? controller; - /// AppBar for the [ZenitLandscapeLayout] - final PreferredSizeWidget? appBar; - /// Creates a global floating action button throughout all Pages final FloatingActionButton? globalFloatingActionButton; - /// Page Margin - final EdgeInsets margin; + /// Sets the color of the sidebar + final Color? sidebarColor; + + /// The width of the sidebar + final double sidebarWidth; + + // Toolbar on the top of the sidebar + final PreferredSizeWidget? sidebarToolbar; + + // Toolbar on the top of the page + final PreferredSizeWidget? Function(BuildContext context, int index)? pageToolbarBuilder; @override State createState() => _ZenitLandscapeLayoutState(); @@ -74,6 +79,7 @@ class _ZenitLandscapeLayoutState extends State { @override Widget build(BuildContext context) { + final Color sidebarColor = widget.sidebarColor ?? Theme.of(context).colorScheme.surface; return LayoutBuilder( builder: (context, constraints) { return Row( @@ -81,46 +87,50 @@ class _ZenitLandscapeLayoutState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: 256, - child: Padding( - padding: widget.margin, + width: widget.sidebarWidth, + child: DecoratedBox( + decoration: BoxDecoration( + color: sidebarColor, + ), child: Scaffold( - body: ZenitLayoutDestinationListView( - length: widget.length, - selectedIndex: selectedIndex, - onTap: onTap, - builder: widget.destinationBuilder, + backgroundColor: sidebarColor, + appBar: widget.sidebarToolbar, + body: Padding( + padding: const EdgeInsets.all(8), + child: ZenitLayoutDestinationListView( + length: widget.length, + selectedIndex: selectedIndex, + onTap: onTap, + builder: widget.destinationBuilder, + ), ), ), ), ), - const VerticalDivider( - width: 1, + VerticalDivider( + color: Theme.of(context).colorScheme.outline.withOpacity(0.1), ), Expanded( child: SizedBox.expand( - child: ClipRRect( - child: Padding( - padding: widget.margin, - child: ZenitNavigatorPopTransactionObserver( - navigatorKey: _navigatorKey, - child: Navigator( - key: _navigatorKey, - pages: [ - MaterialPage( - key: ValueKey(_selectedIndex), - child: widget.length > _selectedIndex - ? widget.pageBuilder(context, _selectedIndex) - : widget.pageBuilder(context, 0), - ), - ], - onPopPage: (route, result) => route.didPop(result), - observers: [ - ZenitNavigatorCanPopObserver.withContext(context), - HeroController(), - ], + child: ZenitNavigatorPopTransactionObserver( + navigatorKey: _navigatorKey, + child: Navigator( + key: _navigatorKey, + pages: [ + MaterialPage( + key: ValueKey(_selectedIndex), + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + appBar: widget.pageToolbarBuilder?.call(context, _selectedIndex), + body: widget.pageBuilder(context, _selectedIndex), + ), ), - ), + ], + onPopPage: (route, result) => route.didPop(result), + observers: [ + ZenitNavigatorCanPopObserver.withContext(context), + HeroController(), + ], ), ), ), diff --git a/lib/src/layout/layout_page/constraints/zenit_portrait_layout.dart b/lib/src/layout/layout_page/constraints/zenit_portrait_layout.dart index 682f82f..ee75f43 100644 --- a/lib/src/layout/layout_page/constraints/zenit_portrait_layout.dart +++ b/lib/src/layout/layout_page/constraints/zenit_portrait_layout.dart @@ -1,8 +1,5 @@ -import 'package:flutter/material.dart'; -import 'package:zenit_ui/src/constants/constants.dart'; import 'package:zenit_ui/src/layout/layout_page/list_view/zenit_layout_destination_list_view.dart'; -import 'package:zenit_ui/src/layout/layout_page/zenit_navigation_layout.dart'; -import 'package:zenit_ui/src/layout/navigator/zenit_navigator_observer.dart'; +import 'package:zenit_ui/zenit_ui.dart'; class ZenitPortraitLayout extends StatefulWidget { const ZenitPortraitLayout({ @@ -13,14 +10,15 @@ class ZenitPortraitLayout extends StatefulWidget { required this.onPageSelected, required this.destinationBuilder, this.controller, - this.appBar, - this.margin = kDefaultPageMargin, + this.sidebarColor, + this.sidebarToolbar, + this.pageToolbarBuilder, }); /// The number of pages in the [ZenitPortraitLayout]. final int length; - final ZenitNavigationLayoutBuilder destinationBuilder; + final ZenitNavigationSidebarBuilder destinationBuilder; /// The index of the selected page. final int selectedIndex; @@ -34,11 +32,14 @@ class ZenitPortraitLayout extends StatefulWidget { /// A controller that can control the index of the [ZenitPortraitLayout]. final ValueNotifier? controller; - /// AppBar for the [ZenitPortraitLayout] - final PreferredSizeWidget? appBar; + /// Sets the color of the sidebar + final Color? sidebarColor; - /// Page Margin - final EdgeInsets margin; + // Toolbar on the top of the sidebar + final PreferredSizeWidget? sidebarToolbar; + + // Toolbar on the top of the page + final PreferredSizeWidget? Function(BuildContext context, int index)? pageToolbarBuilder; @override State createState() => _ZenitPortraitLayoutState(); @@ -72,7 +73,11 @@ class _ZenitPortraitLayoutState extends State { // The router for the indexed pages. MaterialPageRoute pageRoute(int index) { return MaterialPageRoute( - builder: (context) => widget.pageBuilder(context, index), + builder: (context) => Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + appBar: widget.pageToolbarBuilder?.call(context, index), + body: widget.pageBuilder(context, index), + ), ); } @@ -83,12 +88,25 @@ class _ZenitPortraitLayoutState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async => !await navigator.maybePop().then(setPage), - child: Padding( - padding: widget.margin, - child: ZenitNavigatorPopTransactionObserver( - navigatorKey: _navigatorKey, + final sidebarColor = + widget.sidebarColor ?? Theme.of(context).colorScheme.background.themedLightness(context, 0.05); + return PopScope( + onPopInvoked: (_) async => !await navigator.maybePop().then(setPage), + child: ZenitNavigatorPopTransactionObserver( + navigatorKey: _navigatorKey, + child: Theme( + data: Theme.of(context).copyWith( + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.linux: CupertinoPageTransitionsBuilder(), + TargetPlatform.windows: CupertinoPageTransitionsBuilder(), + TargetPlatform.macOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.android: CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), + TargetPlatform.fuchsia: CupertinoPageTransitionsBuilder(), + }, + ), + ), child: Navigator( key: _navigatorKey, observers: [ZenitNavigatorCanPopObserver.withContext(context)], @@ -96,12 +114,22 @@ class _ZenitPortraitLayoutState extends State { return [ MaterialPageRoute( builder: (context) { - return Scaffold( - body: ZenitLayoutDestinationListView( - length: widget.length, - selectedIndex: selectedIndex, - onTap: onTap, - builder: widget.destinationBuilder, + return DecoratedBox( + decoration: BoxDecoration( + color: sidebarColor, + ), + child: Scaffold( + backgroundColor: sidebarColor, + appBar: widget.sidebarToolbar, + body: Padding( + padding: const EdgeInsets.all(8), + child: ZenitLayoutDestinationListView( + length: widget.length, + selectedIndex: -1, + onTap: onTap, + builder: widget.destinationBuilder, + ), + ), ), ); }, diff --git a/lib/src/layout/layout_page/list_view/zenit_layout_destination_list_view.dart b/lib/src/layout/layout_page/list_view/zenit_layout_destination_list_view.dart index 877f6ad..56166f7 100644 --- a/lib/src/layout/layout_page/list_view/zenit_layout_destination_list_view.dart +++ b/lib/src/layout/layout_page/list_view/zenit_layout_destination_list_view.dart @@ -9,21 +9,26 @@ class ZenitLayoutDestinationListView extends StatefulWidget { required this.length, required this.onTap, required this.selectedIndex, + this.isPortrait = false, }); final int length; - final ZenitNavigationLayoutBuilder builder; + final ZenitNavigationSidebarBuilder builder; final ValueChanged onTap; final int selectedIndex; + final bool isPortrait; + @override - State createState() => _ZenitLayoutDestinationListViewState(); + State createState() => + _ZenitLayoutDestinationListViewState(); } -class _ZenitLayoutDestinationListViewState extends State { +class _ZenitLayoutDestinationListViewState + extends State { final controller = ScrollController(); @override @@ -44,7 +49,8 @@ class _ZenitLayoutDestinationListViewState extends State widget.onTap(index), child: Builder( - builder: (context) => widget.builder(context, index, index == widget.selectedIndex), + builder: (context) => + widget.builder(context, index, index == widget.selectedIndex), ), ), ); diff --git a/lib/src/layout/layout_page/list_view/zenit_layout_tile.dart b/lib/src/layout/layout_page/list_view/zenit_layout_tile.dart index 337efdf..7e56ef6 100644 --- a/lib/src/layout/layout_page/list_view/zenit_layout_tile.dart +++ b/lib/src/layout/layout_page/list_view/zenit_layout_tile.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:zenit_ui/src/constants/constants.dart'; -import 'package:zenit_ui/src/theme/theme.dart'; class ZenitLayoutTile extends StatelessWidget { const ZenitLayoutTile({ @@ -32,20 +31,22 @@ class ZenitLayoutTile extends StatelessWidget { return Material( clipBehavior: Clip.antiAlias, color: isSelected ? Theme.of(context).colorScheme.surface : Colors.transparent, - borderRadius: kDefaultBorderRadiusSmall, + borderRadius: kDefaultBorderRadiusMedium, child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 8), dense: true, selected: isSelected, title: DefaultTextStyle( style: Theme.of(context).textTheme.titleSmall!.copyWith( - color: Theme.of(context).foregroundColor, - fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onBackground, ), child: title!, ), - iconColor: Theme.of(context).foregroundColor, - selectedColor: Theme.of(context).primaryColor, + iconColor: Theme.of(context).colorScheme.onBackground, + selectedColor: Theme.of(context).colorScheme.onPrimary, + selectedTileColor: Theme.of(context).colorScheme.primary, subtitle: subtitle, leading: SizedBox(width: 56, child: leading), trailing: trailing, diff --git a/lib/src/layout/layout_page/zenit_navigation_layout.dart b/lib/src/layout/layout_page/zenit_navigation_layout.dart index 7bce937..e1c5653 100644 --- a/lib/src/layout/layout_page/zenit_navigation_layout.dart +++ b/lib/src/layout/layout_page/zenit_navigation_layout.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:zenit_ui/src/constants/constants.dart'; import 'package:zenit_ui/src/layout/layout_page/constraints/zenit_landscape_layout.dart'; import 'package:zenit_ui/src/layout/layout_page/constraints/zenit_portrait_layout.dart'; import 'package:zenit_ui/src/layout/navigator/zenit_navigator_messenger.dart'; -typedef ZenitNavigationLayoutBuilder = Widget Function( +typedef ZenitNavigationSidebarBuilder = Widget Function( BuildContext context, int index, bool selected, @@ -19,16 +18,19 @@ class ZenitNavigationLayout extends StatefulWidget { this.initialIndex, this.onPageSelected, this.controller, - this.appBar, + this.titlebar, this.globalFloatingActionButton, - this.margin = kDefaultPageMargin, + this.sidebarColor, + this.sidebarWidth = 256, + this.sidebarToolbar, + this.pageToolbarBuilder, }); /// The number of pages in the [ZenitNavigationLayout]. final int length; /// Builds a destination for the given index. - final ZenitNavigationLayoutBuilder destinationBuilder; + final ZenitNavigationSidebarBuilder destinationBuilder; /// Builds a page for the given index. final IndexedWidgetBuilder pageBuilder; @@ -43,13 +45,22 @@ class ZenitNavigationLayout extends StatefulWidget { final ValueNotifier? controller; /// The ZenitNavigationLayout AppBar - final PreferredSizeWidget? appBar; + final PreferredSizeWidget? titlebar; /// Creates a global floating action button throughout all Pages final Widget? globalFloatingActionButton; - /// Page Margin - final EdgeInsets margin; + /// Sets the color of the sidebar + final Color? sidebarColor; + + /// Sets the width of the sidebar + final double sidebarWidth; + + // Toolbar on the top of the sidebar + final PreferredSizeWidget? sidebarToolbar; + + // Toolbar on the top of the page + final PreferredSizeWidget? Function(BuildContext context, int index)? pageToolbarBuilder; @override State createState() => _ZenitNavigationLayoutState(); @@ -84,7 +95,8 @@ class _ZenitNavigationLayoutState extends State { return ZenitNavigatorMessengerHost( child: Scaffold( floatingActionButton: widget.globalFloatingActionButton, - appBar: widget.appBar, + extendBodyBehindAppBar: true, + appBar: widget.titlebar, body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { if (constraints.maxWidth < 620) { @@ -95,8 +107,9 @@ class _ZenitNavigationLayoutState extends State { selectedIndex: _index, onPageSelected: setIndex, controller: widget.controller, - appBar: widget.appBar, - margin: widget.margin, + sidebarColor: widget.sidebarColor, + sidebarToolbar: widget.sidebarToolbar, + pageToolbarBuilder: widget.pageToolbarBuilder, ); } else { return ZenitLandscapeLayout( @@ -106,8 +119,10 @@ class _ZenitNavigationLayoutState extends State { selectedIndex: _index == -1 ? _previousIndex : _index, onPageSelected: setIndex, controller: widget.controller, - appBar: widget.appBar, - margin: widget.margin, + sidebarColor: widget.sidebarColor, + sidebarWidth: widget.sidebarWidth, + sidebarToolbar: widget.sidebarToolbar, + pageToolbarBuilder: widget.pageToolbarBuilder, ); } }, diff --git a/lib/src/layout/navigator/zenit_navigator_messenger.dart b/lib/src/layout/navigator/zenit_navigator_messenger.dart index 65b4319..9cbe996 100644 --- a/lib/src/layout/navigator/zenit_navigator_messenger.dart +++ b/lib/src/layout/navigator/zenit_navigator_messenger.dart @@ -13,12 +13,10 @@ class ZenitNavigatorMessengerHost extends StatefulWidget { }); @override - State createState() => - _ZenitNavigatorMessengerHostState(); + State createState() => _ZenitNavigatorMessengerHostState(); } -class _ZenitNavigatorMessengerHostState - extends State { +class _ZenitNavigatorMessengerHostState extends State { bool _canPop = false; bool _requestedPopTransaction = false; bool _lastCanPop = false; @@ -95,8 +93,7 @@ class ZenitNavigatorMessenger extends InheritedWidget { /// Get the closest [ZenitNavigatorMessenger] ancestor if any is available. /// If it's not, return `null`. static ZenitNavigatorMessenger? maybeOf(BuildContext context) { - return context - .dependOnInheritedWidgetOfExactType(); + return context.dependOnInheritedWidgetOfExactType(); } /// Get the closest [ZenitNavigatorMessenger] ancestor if any is available. diff --git a/lib/src/layout/navigator/zenit_navigator_observer.dart b/lib/src/layout/navigator/zenit_navigator_observer.dart index 143b973..35c2be8 100644 --- a/lib/src/layout/navigator/zenit_navigator_observer.dart +++ b/lib/src/layout/navigator/zenit_navigator_observer.dart @@ -70,21 +70,17 @@ class ZenitNavigatorPopTransactionObserver extends StatefulWidget { }); @override - State createState() => - _ZenitNavigatorPopTransactionObserverState(); + State createState() => _ZenitNavigatorPopTransactionObserverState(); } -class _ZenitNavigatorPopTransactionObserverState - extends State { +class _ZenitNavigatorPopTransactionObserverState extends State { @override void didChangeDependencies() { final navigatorInterceptor = ZenitNavigatorMessenger.maybeOf(context); - final bool requestedPop = - navigatorInterceptor?.requestedPopTransaction ?? false; + final bool requestedPop = navigatorInterceptor?.requestedPopTransaction ?? false; if (requestedPop) { - final NavigatorState? navigator = - widget.navigatorKey?.currentState ?? Navigator.maybeOf(context); + final NavigatorState? navigator = widget.navigatorKey?.currentState ?? Navigator.maybeOf(context); navigator?.pop(); navigatorInterceptor?.completeCurrentTransaction(); diff --git a/lib/src/layout/tab_view/tab.dart b/lib/src/layout/tab_view/tab.dart index 0470214..04ff4a5 100644 --- a/lib/src/layout/tab_view/tab.dart +++ b/lib/src/layout/tab_view/tab.dart @@ -1,6 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:zenit_ui/src/constants/constants.dart'; -import 'package:zenit_ui/src/theme/theme.dart'; +import 'package:zenit_ui/zenit_ui.dart'; class ZenitTab extends StatelessWidget { const ZenitTab({ @@ -27,55 +26,41 @@ class ZenitTab extends StatelessWidget { @override Widget build(BuildContext context) { - // Colors - final backgroundColor = selected ? Theme.of(context).colorScheme.surface : Colors.transparent; - final borderColor = - selected ? Theme.of(context).foregroundColor.withOpacity(0.15) : Theme.of(context).colorScheme.surface; - return SizedBox( width: 200, child: GestureDetector( - onTap: onPressed, onTertiaryTapUp: (_) => onClose?.call(), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Tooltip( - message: title, - child: Material( - color: backgroundColor, - shape: RoundedRectangleBorder( - borderRadius: kDefaultBorderRadiusMedium, - side: BorderSide(color: borderColor), - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - children: [ - if (icon != null) SizedBox.square(dimension: 20, child: icon), - const SizedBox.square(dimension: 8), - SizedBox( - width: 132, - child: Text( - title, - style: const TextStyle(overflow: TextOverflow.ellipsis), - ), - ), - const Spacer(), - if (onClose != null) - IconButton( - iconSize: 16, - constraints: const BoxConstraints.tightFor(width: 24, height: 24), - color: Theme.of(context).foregroundColor, - padding: EdgeInsets.zero, - onPressed: onClose, - icon: const Icon( - Icons.close, - ), - ) - ], - ), - ), + child: Tooltip( + message: title, + child: ListTile( + titleTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(overflow: TextOverflow.ellipsis), + onTap: onPressed, + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + selected: selected, + selectedColor: Theme.of(context).colorScheme.onSurface, + selectedTileColor: Theme.of(context).colorScheme.surface, + title: Text( + title, + ), + leading: SizedBox.square(dimension: 20, child: icon), + minLeadingWidth: 20, + trailing: ZenitIconButton( + iconSize: 16, + buttonSize: 24, + onPressed: onClose, + icon: Icons.close, + ), + shape: RoundedRectangleBorder( + borderRadius: kDefaultBorderRadiusMedium, + side: selected + ? BorderSide( + color: Theme.of(context).colorScheme.outline, + ) + : BorderSide.none, ), + //TODO maybe find a better way because this is hacky af and very wrong + visualDensity: const VisualDensity(vertical: -3), + dense: true, ), ), ), diff --git a/lib/src/layout/tab_view/zenit_tab_bar.dart b/lib/src/layout/tab_view/zenit_tab_bar.dart index 036e39a..f9faab3 100644 --- a/lib/src/layout/tab_view/zenit_tab_bar.dart +++ b/lib/src/layout/tab_view/zenit_tab_bar.dart @@ -43,14 +43,16 @@ class _ZenitTabBarState extends State { SizedBox( height: 48, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), + padding: const EdgeInsets.symmetric(vertical: 6.0), child: ListView.builder( - itemCount: widget.onAddTab != null ? widget.tabs.length + 1 : widget.tabs.length, + itemCount: widget.onAddTab != null + ? widget.tabs.length + 1 + : widget.tabs.length, scrollDirection: Axis.horizontal, itemBuilder: (context, index) { if (index < widget.tabs.length) { return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), + padding: const EdgeInsets.symmetric(horizontal: 2.0), child: ZenitTab( icon: widget.tabs.elementAt(index).leading, title: widget.tabs.elementAt(index).title, @@ -61,7 +63,8 @@ class _ZenitTabBarState extends State { ); } else { return IconButton( - constraints: const BoxConstraints.tightFor(height: 40, width: 40), + constraints: + const BoxConstraints.tightFor(height: 36, width: 36), padding: EdgeInsets.zero, onPressed: widget.onAddTab, icon: const Icon(Icons.add), diff --git a/lib/src/layout/toolbar/toolbar.dart b/lib/src/layout/toolbar/toolbar.dart new file mode 100644 index 0000000..22dc941 --- /dev/null +++ b/lib/src/layout/toolbar/toolbar.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:zenit_ui/src/extensions/extensions.dart'; + +class ZenitToolbar extends StatelessWidget implements PreferredSizeWidget { + const ZenitToolbar({ + super.key, + this.leadingActions, + this.trailingActions, + this.title, + this.titleStyle = const TextStyle(), + this.backgroundColor, + this.centerTitle = true, + this.border, + this.height, + this.padding = const EdgeInsets.symmetric(horizontal: 6), + }); + + final List? leadingActions; + final List? trailingActions; + final Widget? title; + final TextStyle titleStyle; + final Color? backgroundColor; + final Border? border; + final bool centerTitle; + final double? height; + final EdgeInsetsGeometry padding; + + @override + Size get preferredSize => Size.fromHeight(height ?? 48); + + @override + Widget build(BuildContext context) { + final bgColor = + backgroundColor ?? Theme.of(context).colorScheme.background.themedLightness(context, 0.05); + return SizedBox( + height: preferredSize.height, + child: DecoratedBox( + decoration: BoxDecoration( + color: bgColor, + border: border, + ), + child: Stack( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.only(left: padding.horizontal / 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: leadingActions ?? [], + ), + ), + ), + Align( + alignment: centerTitle ? Alignment.center : Alignment.centerLeft, + child: DefaultTextStyle( + style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w600) ?? + titleStyle, + child: Padding( + padding: centerTitle ? EdgeInsets.zero : const EdgeInsets.only(left: 16.0), + child: title, + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: Padding( + padding: EdgeInsets.only(right: padding.horizontal / 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: trailingActions ?? [], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/theme/text_theme.dart b/lib/src/theme/text_theme.dart new file mode 100644 index 0000000..4da08f8 --- /dev/null +++ b/lib/src/theme/text_theme.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +// Credits to the yaru team, this file is basically just copied and altered a bit + +TextTheme createTextTheme(Color textColor) { + return TextTheme( + displayLarge: TextStyle( + fontSize: 64, + fontWeight: FontWeight.w300, + color: textColor, + ).asZenitTextStyle(), + displayMedium: TextStyle( + fontSize: 48, + fontWeight: FontWeight.w300, + color: textColor, + ).asZenitTextStyle(), + displaySmall: TextStyle( + fontSize: 30, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + headlineLarge: TextStyle( + fontSize: 32, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + headlineMedium: TextStyle( + fontSize: 28, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + headlineSmall: TextStyle( + fontSize: 20, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + titleLarge: TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + titleMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + titleSmall: TextStyle( + fontSize: 13, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + bodyLarge: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + bodyMedium: TextStyle( + fontSize: 13, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + bodySmall: TextStyle( + fontSize: 11, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + labelLarge: TextStyle( + fontSize: 13, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + labelMedium: TextStyle( + fontSize: 11, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + labelSmall: TextStyle( + fontSize: 9, + fontWeight: FontWeight.normal, + color: textColor, + ).asZenitTextStyle(), + ); +} + +extension TextStyleX on TextStyle { + TextStyle asZenitTextStyle() { + return copyWith( + fontFamily: 'Inter', + package: 'zenit_ui', + overflow: TextOverflow.ellipsis, + ); + } +} diff --git a/lib/src/theme/theme.dart b/lib/src/theme/theme.dart index e032665..f835fc8 100644 --- a/lib/src/theme/theme.dart +++ b/lib/src/theme/theme.dart @@ -2,18 +2,22 @@ import 'package:flutter/material.dart'; import 'package:zenit_ui/src/constants/constants.dart'; +import 'package:zenit_ui/src/extensions/extensions.dart'; +import 'package:zenit_ui/src/theme/text_theme.dart'; + +export 'package:zenit_ui/src/extensions/extensions.dart'; mixin ZenitTheme { static ZenitSwitchTheme switchTheme(BuildContext context) { final theme = Theme.of(context); return ZenitSwitchTheme( - activeTrackColor: theme.primaryColor, - inactiveTrackColor: theme.elementColor, - activeThumbColor: theme.colorScheme.background, - inactiveThumbColor: - theme.darkMode ? theme.foregroundColor.withOpacity(0.35) : theme.foregroundColor.withOpacity(0.35), + activeTrackColor: theme.colorScheme.primary, + inactiveTrackColor: theme.colorScheme.surface, + activeThumbColor: theme.colorScheme.onPrimary, + inactiveThumbColor: theme.colorScheme.onSurface.withOpacity(0.35), disabledTrackColor: theme.disabledColor, disabledThumbColor: theme.colorScheme.background, + outlineColor: theme.colorScheme.outline, ); } @@ -21,83 +25,68 @@ mixin ZenitTheme { final theme = Theme.of(context); return ZenitSliderTheme( activeTrackColor: theme.primaryColor, - trackColor: theme.primaryColor.withOpacity(0.25), + trackColor: theme.colorScheme.surface, + thumbColor: theme.colorScheme.onPrimary, + outlineColor: theme.colorScheme.outline, ); } static ZenitRadioButtonTheme radioButtonTheme(BuildContext context) { final theme = Theme.of(context); return ZenitRadioButtonTheme( - activeBackgroundColor: theme.primaryColor, - inactiveBackgroundColor: theme.elementColor, - activeThumbColor: theme.colorScheme.background, - inactiveThumbColor: theme.elementColor, + activeBackgroundColor: theme.colorScheme.primary, + inactiveBackgroundColor: theme.colorScheme.surface, disabledBackgroundColor: theme.disabledColor, + thumbColor: theme.colorScheme.background, + outlineColor: theme.colorScheme.outline, ); } static ZenitCheckboxTheme checkboxTheme(BuildContext context) { final theme = Theme.of(context); return ZenitCheckboxTheme( - activeBackgroundColor: theme.primaryColor, - inactiveBackgroundColor: theme.elementColor, + activeBackgroundColor: theme.colorScheme.primary, + inactiveBackgroundColor: theme.colorScheme.surface, foregroundColor: theme.colorScheme.background, disabledBackgroundColor: theme.disabledColor, - ); - } -} - -class ZenitElementColor extends ThemeExtension { - const ZenitElementColor({ - required this.elementColor, - }); - - final Color? elementColor; - - @override - ZenitElementColor copyWith({Color? elementColor, Color? newElementColor}) { - return ZenitElementColor( - elementColor: elementColor ?? this.elementColor, - ); - } - - @override - ZenitElementColor lerp(ZenitElementColor? other, double t) { - if (other is! ZenitElementColor) { - return this; - } - return ZenitElementColor( - elementColor: Color.lerp(elementColor, other.elementColor, t), + outlineColor: theme.colorScheme.outline, ); } } ThemeData createZenitTheme({ - Brightness? brightness = Brightness.light, + Brightness brightness = Brightness.light, Color? primaryColor, Color? backgroundColor, Color? surfaceColor, - Color? elementColor, Color? foregroundColor, }) { // Default Values + + const Color lightBackground = Color(0xFFFFFFFF); + const Color lightForeground = Color(0xFF000000); + const Color darkBackground = Color(0xFF1A1A1A); + const Color darkForeground = Color(0xFFFAFAFA); + final darkMode = brightness == Brightness.dark; - final primary = primaryColor ?? const Color(0xFF0073cf); - final foreground = foregroundColor ?? (darkMode ? const Color(0xffffffff) : const Color(0xFF000000)); - final element = elementColor ?? (darkMode ? const Color(0xFF353535) : const Color(0xFFD9D9D9)); + primaryColor ??= const Color(0xFF0073cf); + backgroundColor ??= darkMode ? darkBackground : lightBackground; + foregroundColor ??= darkMode ? darkForeground : lightForeground; + surfaceColor ??= darkMode ? const Color(0xFF272727) : const Color(0xFFE6E6E6); + final textTheme = createTextTheme(foregroundColor); // AppBar Theme final appBarTheme = AppBarTheme( backgroundColor: surfaceColor, titleTextStyle: TextStyle( - color: foreground, + color: foregroundColor, fontSize: 17, fontWeight: FontWeight.w600, ), ); // Icon Theme - final iconTheme = IconThemeData(color: foreground); + final iconTheme = IconThemeData(color: foregroundColor); // Card Theme const cardTheme = CardTheme( @@ -110,13 +99,22 @@ ThemeData createZenitTheme({ // FloatingActionButton Theme final floatingActionButtonTheme = FloatingActionButtonThemeData( - backgroundColor: primaryColor, - foregroundColor: backgroundColor, + backgroundColor: surfaceColor, + foregroundColor: foregroundColor, elevation: 0, focusElevation: 0, hoverElevation: 0, disabledElevation: 0, highlightElevation: 0, + sizeConstraints: BoxConstraints.tight(const Size.square(52)), + smallSizeConstraints: BoxConstraints.tight(const Size.square(44)), + largeSizeConstraints: BoxConstraints.tight(const Size.square(56)), + extendedSizeConstraints: const BoxConstraints.tightFor(height: 52), + iconSize: 24, + shape: RoundedRectangleBorder( + borderRadius: kDefaultBorderRadiusLarge, + side: BorderSide(color: foregroundColor.withOpacity(0.075)), + ), ); // PageTransitionsTheme @@ -133,89 +131,89 @@ ThemeData createZenitTheme({ // Tooltip Theme final tooltipTheme = TooltipThemeData( decoration: BoxDecoration( - color: surfaceColor, - borderRadius: kDefaultBorderRadiusMedium, - border: Border.all( - color: foreground.withOpacity(0.15), - ), + color: foregroundColor, + borderRadius: kDefaultBorderRadiusExtraLarge, ), - textStyle: TextStyle(color: foregroundColor), + textStyle: TextStyle(color: backgroundColor), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - waitDuration: const Duration(seconds: 1), + waitDuration: const Duration(milliseconds: 500), + showDuration: Duration.zero, + ); + + // Divider Theme + final dividerTheme = DividerThemeData( + color: foregroundColor.withOpacity(0.1), + space: 1, + ); + + // ListTile Theme + final listTileTheme = ListTileThemeData( + subtitleTextStyle: TextStyle( + color: foregroundColor.darken(0.4), + fontSize: textTheme.bodyMedium?.fontSize, + ), ); if (brightness == Brightness.light) { return ThemeData.from( colorScheme: ColorScheme.light( - primary: primary, - secondary: primary, - background: backgroundColor ?? const Color(0xFFFAFAFA), - onBackground: foreground, - surface: surfaceColor ?? const Color(0xFFEBEBEB), - onSurface: foreground, + primary: primaryColor, + onPrimary: primaryColor.computeLuminance() > 0.3 ? foregroundColor : backgroundColor, + secondary: primaryColor, + onSecondary: primaryColor.computeLuminance() > 0.3 ? foregroundColor : backgroundColor, + background: backgroundColor, + onBackground: foregroundColor, + surface: surfaceColor, + onSurface: foregroundColor, + outline: foregroundColor.withOpacity(0.2), ), ).copyWith( - useMaterial3: false, - appBarTheme: appBarTheme, - primaryColor: primary, - scaffoldBackgroundColor: backgroundColor ?? const Color(0xFFFAFAFA), + primaryColor: primaryColor, + scaffoldBackgroundColor: backgroundColor, iconTheme: iconTheme, cardTheme: cardTheme, floatingActionButtonTheme: floatingActionButtonTheme, pageTransitionsTheme: pageTransitionsTheme, tooltipTheme: tooltipTheme, - extensions: [ - ZenitElementColor(elementColor: element), - ], + dividerTheme: dividerTheme, + listTileTheme: listTileTheme, + textTheme: textTheme, + hoverColor: foregroundColor.withOpacity(0.05), ); } else { return ThemeData.from( colorScheme: ColorScheme.dark( - primary: primary, - secondary: primary, - background: backgroundColor ?? const Color(0xFF1C1C1E), - onBackground: foreground, - surface: surfaceColor ?? const Color(0xFF252528), - onSurface: foreground, + primary: primaryColor, + onPrimary: primaryColor.computeLuminance() > 0.3 ? backgroundColor : foregroundColor, + secondary: primaryColor, + onSecondary: primaryColor.computeLuminance() > 0.3 ? backgroundColor : foregroundColor, + background: backgroundColor, + onBackground: foregroundColor, + surface: surfaceColor, + onSurface: foregroundColor, + outline: foregroundColor.withOpacity(0.2), ), ).copyWith( - useMaterial3: false, - primaryColor: primary, - scaffoldBackgroundColor: backgroundColor ?? const Color(0xFF1C1C1E), + primaryColor: primaryColor, + scaffoldBackgroundColor: backgroundColor, appBarTheme: appBarTheme, iconTheme: iconTheme, cardTheme: cardTheme, floatingActionButtonTheme: floatingActionButtonTheme, pageTransitionsTheme: pageTransitionsTheme, tooltipTheme: tooltipTheme, - extensions: [ - ZenitElementColor(elementColor: element), - ], + dividerTheme: dividerTheme, + listTileTheme: listTileTheme, + textTheme: textTheme, + hoverColor: foregroundColor.withOpacity(0.05), ); } } -extension on Map> { - T? maybeGet() { - return this[T] as T?; - } - - T get() { - final element = maybeGet(); - - if (element != null) return element; - - throw Exception("No theme extension $T found in current ThemeData"); - } -} - extension ZenitThemeData on ThemeData { - Color get surfaceColor => colorScheme.surface; - Color get elementColor => extensions.get().elementColor ?? colorScheme.background; - Color get foregroundColor => textTheme.button?.color ?? Colors.white; - Color get primaryColor => colorScheme.primary; bool get darkMode => brightness == Brightness.dark; - Color get accentForegroundColor => primaryColor.computeLuminance() > 0.4 ? Colors.black : Colors.white; + Color get accentForegroundColor => + colorScheme.primary.computeLuminance() > 0.3 ? Colors.black : Colors.white; Color computedForegroundColor(Color color) => color.computeLuminance() > 0.4 ? Colors.black : Colors.white; } @@ -226,6 +224,7 @@ class ZenitSwitchTheme { final Color inactiveThumbColor; final Color disabledTrackColor; final Color disabledThumbColor; + final Color outlineColor; const ZenitSwitchTheme({ required this.activeTrackColor, @@ -234,33 +233,88 @@ class ZenitSwitchTheme { required this.inactiveThumbColor, required this.disabledTrackColor, required this.disabledThumbColor, + required this.outlineColor, }); + + ZenitSwitchTheme copyWith({ + Color? activeTrackColor, + Color? inactiveTrackColor, + Color? activeThumbColor, + Color? inactiveThumbColor, + Color? disabledTrackColor, + Color? disabledThumbColor, + Color? outlineColor, + }) { + return ZenitSwitchTheme( + activeTrackColor: activeTrackColor ?? this.activeTrackColor, + inactiveTrackColor: inactiveTrackColor ?? this.inactiveTrackColor, + activeThumbColor: activeThumbColor ?? this.activeThumbColor, + inactiveThumbColor: inactiveThumbColor ?? this.inactiveThumbColor, + disabledTrackColor: disabledTrackColor ?? this.disabledTrackColor, + disabledThumbColor: disabledThumbColor ?? this.disabledThumbColor, + outlineColor: outlineColor ?? this.outlineColor, + ); + } } class ZenitSliderTheme { final Color activeTrackColor; final Color trackColor; + final Color outlineColor; + final Color thumbColor; const ZenitSliderTheme({ required this.activeTrackColor, required this.trackColor, + required this.outlineColor, + required this.thumbColor, }); + + ZenitSliderTheme copyWith({ + Color? activeTrackColor, + Color? trackColor, + Color? outlineColor, + Color? thumbColor, + }) { + return ZenitSliderTheme( + activeTrackColor: activeTrackColor ?? this.activeTrackColor, + trackColor: trackColor ?? this.trackColor, + outlineColor: outlineColor ?? this.outlineColor, + thumbColor: thumbColor ?? this.thumbColor, + ); + } } class ZenitRadioButtonTheme { final Color activeBackgroundColor; final Color inactiveBackgroundColor; - final Color activeThumbColor; - final Color inactiveThumbColor; final Color disabledBackgroundColor; + final Color thumbColor; + final Color outlineColor; const ZenitRadioButtonTheme({ required this.activeBackgroundColor, required this.inactiveBackgroundColor, - required this.activeThumbColor, - required this.inactiveThumbColor, required this.disabledBackgroundColor, + required this.thumbColor, + required this.outlineColor, }); + + ZenitRadioButtonTheme copyWith({ + Color? activeBackgroundColor, + Color? inactiveBackgroundColor, + Color? disabledBackgroundColor, + Color? thumbColor, + Color? outlineColor, + }) { + return ZenitRadioButtonTheme( + activeBackgroundColor: activeBackgroundColor ?? this.activeBackgroundColor, + inactiveBackgroundColor: inactiveBackgroundColor ?? this.inactiveBackgroundColor, + disabledBackgroundColor: disabledBackgroundColor ?? this.disabledBackgroundColor, + thumbColor: thumbColor ?? this.thumbColor, + outlineColor: outlineColor ?? this.outlineColor, + ); + } } class ZenitCheckboxTheme { @@ -268,11 +322,29 @@ class ZenitCheckboxTheme { final Color inactiveBackgroundColor; final Color foregroundColor; final Color disabledBackgroundColor; + final Color outlineColor; const ZenitCheckboxTheme({ required this.activeBackgroundColor, required this.inactiveBackgroundColor, required this.foregroundColor, required this.disabledBackgroundColor, + required this.outlineColor, }); + + ZenitCheckboxTheme copyWith({ + Color? activeBackgroundColor, + Color? inactiveBackgroundColor, + Color? foregroundColor, + Color? disabledBackgroundColor, + Color? outlineColor, + }) { + return ZenitCheckboxTheme( + activeBackgroundColor: activeBackgroundColor ?? this.activeBackgroundColor, + inactiveBackgroundColor: inactiveBackgroundColor ?? this.inactiveBackgroundColor, + foregroundColor: foregroundColor ?? this.foregroundColor, + disabledBackgroundColor: disabledBackgroundColor ?? this.disabledBackgroundColor, + outlineColor: outlineColor ?? this.outlineColor, + ); + } } diff --git a/lib/src/window/titlebar_gesture_detector.dart b/lib/src/window/titlebar_gesture_detector.dart new file mode 100644 index 0000000..76ec40e --- /dev/null +++ b/lib/src/window/titlebar_gesture_detector.dart @@ -0,0 +1,105 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +// Credits to the Ubuntu Yaru Flutter Developers +// https://github.com/ubuntu/yaru_widgets.dart/blob/main/lib/src/widgets/yaru_title_bar_gesture_detector.dart + +class TitleBarGestureDetector extends StatelessWidget { + const TitleBarGestureDetector({ + super.key, + this.onDrag, + this.onDoubleTap, + this.onSecondaryTap, + this.behavior = HitTestBehavior.translucent, + this.child, + }); + + final GestureDragStartCallback? onDrag; + final GestureDoubleTapCallback? onDoubleTap; + final GestureTapCallback? onSecondaryTap; + final HitTestBehavior? behavior; + final Widget? child; + + @override + Widget build(BuildContext context) { + final settings = MediaQuery.maybeOf(context)?.gestureSettings; + return RawGestureDetector( + behavior: behavior, + gestures: { + PanGestureRecognizer: + GestureRecognizerFactoryWithHandlers( + PanGestureRecognizer.new, + (instance) => instance + ..onStart = onDrag + ..gestureSettings = settings, + ), + _PassiveTapGestureRecognizer: + GestureRecognizerFactoryWithHandlers<_PassiveTapGestureRecognizer>( + _PassiveTapGestureRecognizer.new, + (instance) => instance + ..onDoubleTap = onDoubleTap + ..onSecondaryTap = onSecondaryTap + ..gestureSettings = settings, + ), + }, + child: child, + ); + } +} + +class _PassiveTapGestureRecognizer extends TapGestureRecognizer { + _PassiveTapGestureRecognizer() { + onTapUp = (_) {}; + onTapCancel = () {}; + } + + GestureDoubleTapCallback? onDoubleTap; + + PointerDownEvent? _firstTapDown; + PointerUpEvent? _firstTapUp; + + @protected + @override + void handleTapUp({ + required PointerDownEvent down, + required PointerUpEvent up, + }) { + super.handleTapUp(down: down, up: up); + if (onDoubleTap != null && + _firstTapDown != null && + _firstTapUp != null && + down.buttons == kPrimaryButton) { + // the time from the first tap down to the second tap down + final interval = down.timeStamp - _firstTapDown!.timeStamp; + // the time from the first tap up to the second tap down + final timeBetween = down.timeStamp - _firstTapUp!.timeStamp; + // the distance between the first tap down and the first tap up + final slop = (_firstTapDown!.position - _firstTapUp!.position).distance; + // the distance between the first tap down and the second tap down + final secondSlop = (_firstTapDown!.position - down.position).distance; + if (interval < kDoubleTapTimeout && + timeBetween >= kDoubleTapMinTime && + slop <= kDoubleTapTouchSlop && + secondSlop <= kDoubleTapSlop) { + invokeCallback('onDoubleTap', onDoubleTap!); + _firstTapDown = null; + _firstTapUp = null; + return; + } + } + _firstTapDown = down; + _firstTapUp = up; + } + + @protected + @override + void handleTapCancel({ + required PointerDownEvent down, + PointerCancelEvent? cancel, + required String reason, + }) { + super.handleTapCancel(down: down, cancel: cancel, reason: reason); + _firstTapDown = null; + _firstTapUp = null; + } +} diff --git a/lib/src/window/window_backdrop.dart b/lib/src/window/window_backdrop.dart new file mode 100644 index 0000000..77c1c54 --- /dev/null +++ b/lib/src/window/window_backdrop.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; +import 'package:yaru_window/yaru_window.dart'; + +class ZenitWindowBackdropEffect extends StatelessWidget { + final Widget child; + final YaruWindowState? state; + + const ZenitWindowBackdropEffect({super.key, required this.child, this.state}); + + static final _windowStates = {}; + + @override + Widget build(BuildContext context) { + if (state == null) { + final YaruWindowInstance window = YaruWindow.of(context); + return StreamBuilder( + stream: window.states(), + initialData: _windowStates[window], + builder: (context, snapshot) { + final state = snapshot.data; + return _animatedOpacity(state); + }, + ); + } else { + return _animatedOpacity(state); + } + } + + AnimatedOpacity _animatedOpacity(YaruWindowState? state) { + return AnimatedOpacity( + opacity: state?.isActive == true ? 1 : 0.75, + duration: const Duration(milliseconds: 100), + child: child, + ); + } +} diff --git a/lib/src/window/window_buttons.dart b/lib/src/window/window_buttons.dart new file mode 100644 index 0000000..fb52a34 --- /dev/null +++ b/lib/src/window/window_buttons.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; + +const kAnimationDuration = Duration(milliseconds: 300); + +enum ZenitWindowButtonType { close, maximize, minimize, restore } + +class ZenitWindowButton extends StatefulWidget { + final ZenitWindowButtonType type; + final VoidCallback? onPressed; + const ZenitWindowButton({ + super.key, + required this.type, + this.onPressed, + }); + + @override + State createState() => _ZenitWindowButtonState(); +} + +class _ZenitWindowButtonState extends State { + bool hover = false; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SizedBox.square( + child: MouseRegion( + onEnter: (_) => setState(() => hover = true), + onExit: (_) => setState(() => hover = false), + child: GestureDetector( + onTap: widget.onPressed, + child: Padding( + padding: const EdgeInsets.all(4), + child: RepaintBoundary( + child: CustomPaint( + size: const Size(32, 32), + painter: _ZenitWindowButtonPainter( + type: widget.type, + backgroundColor: Theme.of(context).colorScheme.surface, + foregroundColor: Theme.of(context).colorScheme.onSurface, + hover: hover, + darkMode: Theme.of(context).brightness == Brightness.dark, + ), + ), + ), + ), + ), + ), + ); + } +} + +class _ZenitWindowButtonPainter extends CustomPainter { + final ZenitWindowButtonType type; + final Color backgroundColor; + final Color foregroundColor; + final bool hover; + final bool darkMode; + + const _ZenitWindowButtonPainter({ + required this.type, + required this.backgroundColor, + required this.foregroundColor, + required this.hover, + required this.darkMode, + }); + + @override + void paint(Canvas canvas, Size size) { + /* final Paint backgroundPaint = Paint() + ..color = backgroundColor + ..style = PaintingStyle.fill; */ + + final Paint foregroundPaint = Paint() + ..color = foregroundColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1.2; + + final Paint hoverPaint = Paint() + ..color = foregroundColor.withOpacity(0.1) + ..style = PaintingStyle.fill; + + final Offset center = Offset(size.height / 2, size.width / 2); + + final Rect buttonRect = Rect.fromCenter( + center: center, + width: size.width * 0.3, + height: size.height * 0.3, + ); + + /* canvas.drawCircle(center, 16, backgroundPaint); */ + + if (hover) { + canvas.drawCircle(center, 16, hoverPaint); + } + + switch (type) { + case ZenitWindowButtonType.close: + drawClose(canvas, buttonRect, foregroundPaint); + case ZenitWindowButtonType.maximize: + drawMaximize(canvas, buttonRect, foregroundPaint, foregroundColor); + case ZenitWindowButtonType.restore: + drawRestore(canvas, buttonRect, foregroundPaint, foregroundColor); + case ZenitWindowButtonType.minimize: + drawMinimize(canvas, buttonRect, foregroundPaint); + default: + drawClose(canvas, buttonRect, foregroundPaint); + } + } + + @override + bool shouldRepaint(covariant _ZenitWindowButtonPainter old) { + return old.hover != hover || + old.darkMode != darkMode || + old.foregroundColor != foregroundColor || + old.backgroundColor != backgroundColor || + old.type != type; + } +} + +void drawClose(Canvas canvas, Rect buttonRect, Paint foregroundPaint) { + canvas.drawLine(buttonRect.topLeft, buttonRect.bottomRight, foregroundPaint); + canvas.drawLine(buttonRect.topRight, buttonRect.bottomLeft, foregroundPaint); +} + +void drawRestoreMaximize( + Canvas canvas, + bool maximized, + Rect buttonRect, + Paint foregroundPaint, + Color foregroundColor, +) { + const gap = 2; + final max = maximized ? 0 : 1; + + final rect = Rect.fromLTRB( + buttonRect.left, + buttonRect.top + gap * max, + buttonRect.right - gap * max, + buttonRect.bottom, + ); + + final path = Path() + ..moveTo( + buttonRect.topLeft.dx + (1 + 0.5), + buttonRect.topLeft.dy, + ) + ..lineTo( + buttonRect.topRight.dx, + buttonRect.topRight.dy, + ) + ..lineTo( + buttonRect.bottomRight.dx, + buttonRect.bottomRight.dy - (1 + 0.5), + ); + + canvas.drawRect(rect, foregroundPaint); + canvas.drawPath( + path, + foregroundPaint..color = foregroundColor.withOpacity(0.5 * max), + ); +} + +void drawRestore(Canvas canvas, Rect buttonRect, Paint foregroundPaint, Color foregroundColor) => + drawRestoreMaximize(canvas, false, buttonRect, foregroundPaint, foregroundColor); + +void drawMaximize(Canvas canvas, Rect buttonRect, Paint foregroundPaint, Color foregroundColor) => + drawRestoreMaximize(canvas, true, buttonRect, foregroundPaint, foregroundColor); + +void drawMinimize(Canvas canvas, Rect buttonRect, Paint foregroundPaint) { + canvas.drawLine( + Offset(buttonRect.bottomLeft.dx, buttonRect.bottomLeft.dy - 1.0), + Offset(buttonRect.bottomRight.dx, buttonRect.bottomRight.dy - 1.0), + foregroundPaint, + ); +} diff --git a/lib/src/window/window_titlebar.dart b/lib/src/window/window_titlebar.dart new file mode 100644 index 0000000..1126124 --- /dev/null +++ b/lib/src/window/window_titlebar.dart @@ -0,0 +1,133 @@ +import 'package:flutter/foundation.dart'; +import 'package:yaru_window/yaru_window.dart'; +import 'package:zenit_ui/src/window/titlebar_gesture_detector.dart'; +import 'package:zenit_ui/zenit_ui.dart'; + +class ZenitWindowTitlebar extends StatefulWidget implements PreferredSizeWidget { + final Color? backgroundColor; + final Border? border; + + const ZenitWindowTitlebar({ + super.key, + this.backgroundColor = Colors.transparent, + this.border, + }); + + static Future ensureInitialized() async { + _windowStates.clear(); + await YaruWindow.ensureInitialized().then((window) => window.hideTitle()); + } + + static final _windowStates = {}; + + @override + State createState() => _ZenitWindowTitlebarState(); + + @override + Size get preferredSize => const Size.fromHeight(48); +} + +class _ZenitWindowTitlebarState extends State { + @override + Widget build(BuildContext context) { + final YaruWindowInstance window = YaruWindow.of(context); + final zenitNavigatorMessenger = ZenitNavigatorMessenger.maybeOf(context); + return StreamBuilder( + stream: window.states(), + initialData: ZenitWindowTitlebar._windowStates[window], + builder: (context, snapshot) { + final state = snapshot.data; + return TitleBarGestureDetector( + onDrag: (data) async => window.drag(), + onDoubleTap: () async { + if ((state?.isMaximized ?? false) && (state?.isRestorable ?? false)) { + await window.restore(); + } else if (!(state?.isMaximized ?? false) && (state?.isMaximizable ?? false)) { + await window.maximize(); + } + }, + onSecondaryTap: () async => window.showMenu(), + child: ZenitWindowBackdropEffect( + state: state, + child: ZenitToolbar( + border: widget.border, + backgroundColor: widget.backgroundColor, + leadingActions: (zenitNavigatorMessenger?.canPop ?? false) + ? [ + const SizedBox(width: 6), + ZenitNavigatorMessengerHost( + child: Material( + type: MaterialType.transparency, + child: ZenitIconButton( + icon: Icons.chevron_left_rounded, + onPressed: () { + if (zenitNavigatorMessenger?.canPop ?? false) { + zenitNavigatorMessenger?.requestPopTransaction(); + } + }, + buttonSize: 36, + ), + ), + ), + ] + : [], + trailingActions: [ + if (state?.isMinimizable ?? false) + ZenitWindowButton( + type: ZenitWindowButtonType.minimize, + onPressed: () async => window.minimize(), + ), + const SizedBox(width: 4), + if (!(state?.isMaximized ?? false) && (state?.isMaximizable ?? false)) + ZenitWindowButton( + type: ZenitWindowButtonType.maximize, + onPressed: () async => window.maximize(), + ), + if ((state?.isMaximized ?? false) && (state?.isRestorable ?? false)) + ZenitWindowButton( + type: ZenitWindowButtonType.restore, + onPressed: () async => window.restore(), + ), + const SizedBox(width: 4), + ZenitWindowButton( + type: ZenitWindowButtonType.close, + onPressed: () async => window.close(), + ), + ], + ), + ), + ); + }, + ); + } +} + +class ZenitWindowTitle extends StatelessWidget { + const ZenitWindowTitle({ + super.key, + this.fallback = "", + }); + + /// Fallback title if the window title is not available (eg. Web) + final String fallback; + + static final _windowStates = {}; + + @override + Widget build(BuildContext context) { + if (kIsWeb) return Text(fallback); + return Builder( + builder: (context) { + final YaruWindowInstance window = YaruWindow.of(context); + return StreamBuilder( + stream: window.states(), + initialData: _windowStates[window], + builder: (context, snapshot) { + final state = snapshot.data; + return Text(state?.title ?? ""); + }, + ); + }, + ); + } +} diff --git a/lib/zenit_ui.dart b/lib/zenit_ui.dart index 33fa5b9..8b882c1 100644 --- a/lib/zenit_ui.dart +++ b/lib/zenit_ui.dart @@ -1,17 +1,39 @@ -import 'package:flutter/material.dart' as material; +/// Flutter widgets and theme implementing the ZenitUI design language. +/// +/// To use, `import 'package:zenit_ui/zenit_ui.dart';` +/// +/// This library is mainly designed for desktop applications, but it can be used +/// for mobile applications as well. +library zenit_ui; // Zenit exports // +// Material +export 'package:flutter/material.dart'; +// Flutter base Widgets +export 'package:flutter/widgets.dart'; // Zenit buttons export 'package:zenit_ui/src/components/buttons/buttons.dart'; // Zenit checkbox export 'package:zenit_ui/src/components/checkbox/checkbox.dart'; +// Zenit context menu +export 'package:zenit_ui/src/components/context_menu/context_menu.dart'; +export 'package:zenit_ui/src/components/context_menu/context_menu_item.dart'; +export 'package:zenit_ui/src/components/context_menu/context_menu_region.dart'; +// Zenit Dialog +export 'package:zenit_ui/src/components/dialog/dialog.dart'; // Zenit Flat Button -export 'package:zenit_ui/src/components/icon_button/zenit_icon_button.dart'; +export 'package:zenit_ui/src/components/icon_button/icon_button.dart'; +// Zenit checkbox list tile +export 'package:zenit_ui/src/components/list_tile/checkbox_list_tile.dart'; +// Zenit radio button list tile +export 'package:zenit_ui/src/components/list_tile/radio_list_tile.dart'; // Zenit switch list tile export 'package:zenit_ui/src/components/list_tile/switch_list_tile.dart'; // Zenit radio button export 'package:zenit_ui/src/components/radio_button/radio_button.dart'; +// Zenit section +export 'package:zenit_ui/src/components/section/section.dart'; // Zenit slider export 'package:zenit_ui/src/components/slider/slider.dart'; // Zenit switch @@ -35,28 +57,11 @@ export 'package:zenit_ui/src/layout/tab_view/tab.dart'; export 'package:zenit_ui/src/layout/tab_view/tab_data.dart'; // Zenit tab view export 'package:zenit_ui/src/layout/tab_view/zenit_tab_bar.dart'; +// Zenit Toolbar +export 'package:zenit_ui/src/layout/toolbar/toolbar.dart'; // Zenit theme export 'package:zenit_ui/src/theme/theme.dart'; - -//Type Definitions - -//Switch -typedef MaterialSwitch = material.Switch; - -//Theme -typedef MaterialTheme = material.Theme; - -//TextField -typedef MaterialTextField = material.TextField; - -//Slider -typedef MaterialSlider = material.Slider; - -//RadioButton -typedef MaterialRadioButton = material.Radio; - -//SwitchListTile -typedef MaterialSwitchListTile = material.SwitchListTile; - -//Checkbox -typedef MaterialCheckbox = material.Checkbox; +// Zenit window +export 'package:zenit_ui/src/window/window_backdrop.dart'; +export 'package:zenit_ui/src/window/window_buttons.dart'; +export 'package:zenit_ui/src/window/window_titlebar.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index d6fb23e..b0a1408 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,32 @@ environment: dependencies: flutter: sdk: flutter + handy_window: ^0.3.1 + yaru_window: ^0.1.3 dev_dependencies: lint: ^2.0.0 + + +flutter: + fonts: + - family: Inter + fonts: + - asset: packages/zenit_ui/src/fonts/Inter-Thin.ttf + weight: 100 + - asset: packages/zenit_ui/src/fonts/Inter-ExtraLight.ttf + weight: 200 + - asset: packages/zenit_ui/src/fonts/Inter-Light.ttf + weight: 300 + - asset: packages/zenit_ui/src/fonts/Inter-Regular.ttf + weight: 400 + - asset: packages/zenit_ui/src/fonts/Inter-Medium.ttf + weight: 500 + - asset: packages/zenit_ui/src/fonts/Inter-SemiBold.ttf + weight: 600 + - asset: packages/zenit_ui/src/fonts/Inter-Bold.ttf + weight: 700 + - asset: packages/zenit_ui/src/fonts/Inter-ExtraBold.ttf + weight: 800 + - asset: packages/zenit_ui/src/fonts/Inter-Black.ttf + weight: 900 \ No newline at end of file