From b18a178dd156099860937daae21ddf8500d747ef Mon Sep 17 00:00:00 2001 From: Dave Friedel Date: Sun, 21 Dec 2025 13:40:42 -0500 Subject: [PATCH] Initial samples: TodoApp and ShellDemo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two sample applications demonstrating OpenMaui Linux: TodoApp: - Full task manager with NavigationPage - CollectionView with XAML data binding - DisplayAlert dialogs - Grid layouts with star sizing ShellDemo: - Comprehensive control showcase - Shell with flyout navigation - All core MAUI controls demonstrated - Real-time event logging Both samples reference OpenMaui.Controls.Linux via NuGet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .gitignore | 53 +++ LICENSE | 21 + README.md | 76 ++++ ShellDemo/App.cs | 78 ++++ ShellDemo/MauiProgram.cs | 24 ++ ShellDemo/Pages/AboutPage.cs | 115 ++++++ ShellDemo/Pages/ButtonsPage.cs | 229 +++++++++++ ShellDemo/Pages/ControlsPage.cs | 203 +++++++++ ShellDemo/Pages/DetailPage.cs | 123 ++++++ ShellDemo/Pages/GridsPage.cs | 594 +++++++++++++++++++++++++++ ShellDemo/Pages/HomePage.cs | 265 ++++++++++++ ShellDemo/Pages/ListsPage.cs | 249 +++++++++++ ShellDemo/Pages/PickersPage.cs | 261 ++++++++++++ ShellDemo/Pages/ProgressPage.cs | 261 ++++++++++++ ShellDemo/Pages/SelectionPage.cs | 239 +++++++++++ ShellDemo/Pages/TextInputPage.cs | 166 ++++++++ ShellDemo/Platforms/Linux/Program.cs | 19 + ShellDemo/README.md | 157 +++++++ ShellDemo/ShellDemo.csproj | 15 + TodoApp/App.cs | 21 + TodoApp/MauiProgram.cs | 22 + TodoApp/NewTodoPage.xaml | 79 ++++ TodoApp/NewTodoPage.xaml.cs | 31 ++ TodoApp/Program.cs | 67 +++ TodoApp/README.md | 111 +++++ TodoApp/TodoApp.csproj | 15 + TodoApp/TodoDetailPage.xaml | 106 +++++ TodoApp/TodoDetailPage.xaml.cs | 91 ++++ TodoApp/TodoItem.cs | 81 ++++ TodoApp/TodoListPage.xaml | 129 ++++++ TodoApp/TodoListPage.xaml.cs | 174 ++++++++ TodoApp/TodoService.cs | 61 +++ docs/images/.gitkeep | 0 33 files changed, 4136 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 ShellDemo/App.cs create mode 100644 ShellDemo/MauiProgram.cs create mode 100644 ShellDemo/Pages/AboutPage.cs create mode 100644 ShellDemo/Pages/ButtonsPage.cs create mode 100644 ShellDemo/Pages/ControlsPage.cs create mode 100644 ShellDemo/Pages/DetailPage.cs create mode 100644 ShellDemo/Pages/GridsPage.cs create mode 100644 ShellDemo/Pages/HomePage.cs create mode 100644 ShellDemo/Pages/ListsPage.cs create mode 100644 ShellDemo/Pages/PickersPage.cs create mode 100644 ShellDemo/Pages/ProgressPage.cs create mode 100644 ShellDemo/Pages/SelectionPage.cs create mode 100644 ShellDemo/Pages/TextInputPage.cs create mode 100644 ShellDemo/Platforms/Linux/Program.cs create mode 100644 ShellDemo/README.md create mode 100644 ShellDemo/ShellDemo.csproj create mode 100644 TodoApp/App.cs create mode 100644 TodoApp/MauiProgram.cs create mode 100644 TodoApp/NewTodoPage.xaml create mode 100644 TodoApp/NewTodoPage.xaml.cs create mode 100644 TodoApp/Program.cs create mode 100644 TodoApp/README.md create mode 100644 TodoApp/TodoApp.csproj create mode 100644 TodoApp/TodoDetailPage.xaml create mode 100644 TodoApp/TodoDetailPage.xaml.cs create mode 100644 TodoApp/TodoItem.cs create mode 100644 TodoApp/TodoListPage.xaml create mode 100644 TodoApp/TodoListPage.xaml.cs create mode 100644 TodoApp/TodoService.cs create mode 100644 docs/images/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b6f6a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# Build results +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# .NET +*.nupkg +*.snupkg +*.user +*.suo +*.userprefs +*.cache +project.lock.json +project.fragment.lock.json +artifacts/ + +# Visual Studio +.vs/ +*.csproj.user +*.dbmdl +*.jfm + +# JetBrains Rider +.idea/ +*.sln.iml + +# Visual Studio Code +.vscode/ + +# macOS +.DS_Store + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# Test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +TestResults/ + +# NuGet +*.nupkg +**/[Pp]ackages/* +!**/[Pp]ackages/build/ + +# Publish output +publish/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d6481db --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 OpenMaui + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c94ba0 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# OpenMaui Linux Samples + +Sample applications demonstrating [OpenMaui Linux](https://github.com/open-maui/maui-linux) - .NET MAUI on Linux. + +## Samples + +| Sample | Description | +|--------|-------------| +| [TodoApp](./TodoApp/) | Full-featured task manager with NavigationPage, XAML data binding, and CollectionView | +| [ShellDemo](./ShellDemo/) | Comprehensive control showcase with Shell navigation and flyout menu | + +## Requirements + +- .NET 9.0 SDK +- Linux with X11 (Ubuntu, Fedora, etc.) +- SkiaSharp dependencies: `libfontconfig1-dev libfreetype6-dev` + +## Quick Start + +```bash +# Clone the samples +git clone https://github.com/open-maui/maui-linux-samples.git +cd maui-linux-samples + +# Run TodoApp +cd TodoApp +dotnet run + +# Or run ShellDemo +cd ../ShellDemo +dotnet run +``` + +## Building for Deployment + +```bash +# Build for Linux ARM64 +dotnet publish -c Release -r linux-arm64 + +# Build for Linux x64 +dotnet publish -c Release -r linux-x64 +``` + +## TodoApp + +A complete task management application demonstrating: +- NavigationPage with toolbar and back navigation +- CollectionView with data binding and selection +- XAML value converters for dynamic styling +- DisplayAlert dialogs +- Grid layouts with star sizing +- Entry and Editor text input + +![TodoApp Screenshot](docs/images/todoapp.png) + +## ShellDemo + +A comprehensive control gallery demonstrating: +- Shell with flyout menu navigation +- All core MAUI controls (Button, Entry, CheckBox, Switch, Slider, etc.) +- Picker, DatePicker, TimePicker +- CollectionView with various item types +- ProgressBar and ActivityIndicator +- Grid layouts +- Real-time event logging + +![ShellDemo Screenshot](docs/images/shelldemo.png) + +## Related + +- [OpenMaui Linux Framework](https://github.com/open-maui/maui-linux) - The core framework +- [NuGet Package](https://www.nuget.org/packages/OpenMaui.Controls.Linux) - Install via NuGet + +## License + +MIT License - See [LICENSE](LICENSE) for details. diff --git a/ShellDemo/App.cs b/ShellDemo/App.cs new file mode 100644 index 0000000..2615a41 --- /dev/null +++ b/ShellDemo/App.cs @@ -0,0 +1,78 @@ +// ShellDemo App - Comprehensive Control Demo + +using Microsoft.Maui.Controls; + +namespace ShellDemo; + +/// +/// Main application class with Shell navigation. +/// +public class App : Application +{ + public App() + { + MainPage = new AppShell(); + } +} + +/// +/// Shell definition with flyout menu - comprehensive control demo. +/// +public class AppShell : Shell +{ + public AppShell() + { + FlyoutBehavior = FlyoutBehavior.Flyout; + Title = "OpenMaui Controls Demo"; + + // Register routes for push navigation (pages not in flyout) + Routing.RegisterRoute("detail", typeof(DetailPage)); + + // Home + Items.Add(CreateFlyoutItem("Home", typeof(HomePage))); + + // Buttons Demo + Items.Add(CreateFlyoutItem("Buttons", typeof(ButtonsPage))); + + // Text Input Demo + Items.Add(CreateFlyoutItem("Text Input", typeof(TextInputPage))); + + // Selection Controls Demo + Items.Add(CreateFlyoutItem("Selection", typeof(SelectionPage))); + + // Pickers Demo + Items.Add(CreateFlyoutItem("Pickers", typeof(PickersPage))); + + // Lists Demo + Items.Add(CreateFlyoutItem("Lists", typeof(ListsPage))); + + // Progress Demo + Items.Add(CreateFlyoutItem("Progress", typeof(ProgressPage))); + + // Grids Demo + Items.Add(CreateFlyoutItem("Grids", typeof(GridsPage))); + + // About + Items.Add(CreateFlyoutItem("About", typeof(AboutPage))); + } + + private FlyoutItem CreateFlyoutItem(string title, Type pageType) + { + // Route is required for Shell.GoToAsync navigation to work + var route = title.Replace(" ", ""); + return new FlyoutItem + { + Title = title, + Route = route, + Items = + { + new ShellContent + { + Title = title, + Route = route, + ContentTemplate = new DataTemplate(pageType) + } + } + }; + } +} diff --git a/ShellDemo/MauiProgram.cs b/ShellDemo/MauiProgram.cs new file mode 100644 index 0000000..0ec2a7e --- /dev/null +++ b/ShellDemo/MauiProgram.cs @@ -0,0 +1,24 @@ +// MauiProgram.cs - Shared MAUI app configuration +// Works across all platforms (iOS, Android, Windows, Linux) + +using Microsoft.Maui.Hosting; +using Microsoft.Maui.Platform.Linux.Hosting; + +namespace ShellDemo; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + + // Configure the app (shared across all platforms) + builder.UseMauiApp(); + + // Add Linux platform support + // On other platforms, this would be iOS/Android/Windows specific + builder.UseLinux(); + + return builder.Build(); + } +} diff --git a/ShellDemo/Pages/AboutPage.cs b/ShellDemo/Pages/AboutPage.cs new file mode 100644 index 0000000..e38d027 --- /dev/null +++ b/ShellDemo/Pages/AboutPage.cs @@ -0,0 +1,115 @@ +// AboutPage - Information about OpenMaui Linux + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace ShellDemo; + +public class AboutPage : ContentPage +{ + public AboutPage() + { + Title = "About"; + + Content = new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 20, + Children = + { + new Label + { + Text = "OpenMaui Linux", + FontSize = 32, + FontAttributes = FontAttributes.Bold, + TextColor = Color.FromArgb("#1A237E"), + HorizontalOptions = LayoutOptions.Center + }, + new Label + { + Text = "Version 1.0.0", + FontSize = 16, + TextColor = Colors.Gray, + HorizontalOptions = LayoutOptions.Center + }, + new BoxView { HeightRequest = 1, Color = Colors.LightGray }, + new Label + { + Text = "OpenMaui Linux brings .NET MAUI to Linux desktops using SkiaSharp for rendering. " + + "It provides a native Linux experience while maintaining compatibility with MAUI's cross-platform API.", + FontSize = 14, + LineBreakMode = LineBreakMode.WordWrap + }, + CreateInfoCard("Platform", "Linux (X11/Wayland)"), + CreateInfoCard("Rendering", "SkiaSharp"), + CreateInfoCard("Framework", ".NET MAUI"), + CreateInfoCard("License", "MIT License"), + new BoxView { HeightRequest = 1, Color = Colors.LightGray }, + new Label + { + Text = "Features", + FontSize = 20, + FontAttributes = FontAttributes.Bold + }, + CreateFeatureItem("Full XAML support with styles and resources"), + CreateFeatureItem("Shell navigation with flyout menus"), + CreateFeatureItem("All standard MAUI controls"), + CreateFeatureItem("Data binding and MVVM"), + CreateFeatureItem("Keyboard and mouse input"), + CreateFeatureItem("High DPI support"), + new BoxView { HeightRequest = 1, Color = Colors.LightGray }, + new Label + { + Text = "https://github.com/pablotoledo/OpenMaui-Linux", + FontSize = 12, + TextColor = Colors.Blue, + HorizontalOptions = LayoutOptions.Center + } + } + } + }; + } + + private Frame CreateInfoCard(string label, string value) + { + return new Frame + { + CornerRadius = 8, + Padding = new Thickness(15), + BackgroundColor = Color.FromArgb("#F5F5F5"), + HasShadow = false, + Content = new HorizontalStackLayout + { + Children = + { + new Label + { + Text = label + ":", + FontAttributes = FontAttributes.Bold, + WidthRequest = 100 + }, + new Label + { + Text = value, + TextColor = Colors.Gray + } + } + } + }; + } + + private View CreateFeatureItem(string text) + { + return new HorizontalStackLayout + { + Spacing = 10, + Children = + { + new Label { Text = "✓", TextColor = Color.FromArgb("#4CAF50"), FontSize = 16 }, + new Label { Text = text, FontSize = 14 } + } + }; + } +} diff --git a/ShellDemo/Pages/ButtonsPage.cs b/ShellDemo/Pages/ButtonsPage.cs new file mode 100644 index 0000000..695ae97 --- /dev/null +++ b/ShellDemo/Pages/ButtonsPage.cs @@ -0,0 +1,229 @@ +// ButtonsPage - Comprehensive Button Control Demo + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace ShellDemo; + +public class ButtonsPage : ContentPage +{ + private readonly Label _eventLog; + private int _eventCount = 0; + + public ButtonsPage() + { + Title = "Buttons Demo"; + + _eventLog = new Label + { + Text = "Events will appear here...", + FontSize = 11, + TextColor = Colors.Gray, + LineBreakMode = LineBreakMode.WordWrap + }; + + Content = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }, + new RowDefinition { Height = new GridLength(120) } + }, + Children = + { + CreateMainContent(), + CreateEventLogPanel() + } + }; + + Grid.SetRow((View)((Grid)Content).Children[0], 0); + Grid.SetRow((View)((Grid)Content).Children[1], 1); + } + + private View CreateMainContent() + { + return new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 20, + Children = + { + new Label { Text = "Button Styles & Events", FontSize = 24, FontAttributes = FontAttributes.Bold }, + + // Basic Buttons + CreateSection("Basic Buttons", CreateBasicButtons()), + + // Styled Buttons + CreateSection("Styled Buttons", CreateStyledButtons()), + + // Button States + CreateSection("Button States", CreateButtonStates()), + + // Button with Icons (text simulation) + CreateSection("Button Variations", CreateButtonVariations()) + } + } + }; + } + + private View CreateBasicButtons() + { + var layout = new VerticalStackLayout { Spacing = 10 }; + + var defaultBtn = new Button { Text = "Default Button" }; + defaultBtn.Clicked += (s, e) => LogEvent("Default Button clicked"); + defaultBtn.Pressed += (s, e) => LogEvent("Default Button pressed"); + defaultBtn.Released += (s, e) => LogEvent("Default Button released"); + + var textBtn = new Button { Text = "Text Only", BackgroundColor = Colors.Transparent, TextColor = Colors.Blue }; + textBtn.Clicked += (s, e) => LogEvent("Text Button clicked"); + + layout.Children.Add(defaultBtn); + layout.Children.Add(textBtn); + + return layout; + } + + private View CreateStyledButtons() + { + var layout = new HorizontalStackLayout { Spacing = 10 }; + + var colors = new[] + { + ("#2196F3", "Primary"), + ("#4CAF50", "Success"), + ("#FF9800", "Warning"), + ("#F44336", "Danger"), + ("#9C27B0", "Purple") + }; + + foreach (var (color, name) in colors) + { + var btn = new Button + { + Text = name, + BackgroundColor = Color.FromArgb(color), + TextColor = Colors.White, + CornerRadius = 5 + }; + btn.Clicked += (s, e) => LogEvent($"{name} button clicked"); + layout.Children.Add(btn); + } + + return layout; + } + + private View CreateButtonStates() + { + var layout = new VerticalStackLayout { Spacing = 10 }; + + var enabledBtn = new Button { Text = "Enabled Button", IsEnabled = true }; + enabledBtn.Clicked += (s, e) => LogEvent("Enabled button clicked"); + + var disabledBtn = new Button { Text = "Disabled Button", IsEnabled = false }; + + var toggleBtn = new Button { Text = "Toggle Above Button" }; + toggleBtn.Clicked += (s, e) => + { + disabledBtn.IsEnabled = !disabledBtn.IsEnabled; + disabledBtn.Text = disabledBtn.IsEnabled ? "Now Enabled!" : "Disabled Button"; + LogEvent($"Toggled button to: {(disabledBtn.IsEnabled ? "Enabled" : "Disabled")}"); + }; + + layout.Children.Add(enabledBtn); + layout.Children.Add(disabledBtn); + layout.Children.Add(toggleBtn); + + return layout; + } + + private View CreateButtonVariations() + { + var layout = new VerticalStackLayout { Spacing = 10 }; + + var wideBtn = new Button + { + Text = "Wide Button", + HorizontalOptions = LayoutOptions.Fill, + BackgroundColor = Color.FromArgb("#673AB7"), + TextColor = Colors.White + }; + wideBtn.Clicked += (s, e) => LogEvent("Wide button clicked"); + + var tallBtn = new Button + { + Text = "Tall Button", + HeightRequest = 60, + BackgroundColor = Color.FromArgb("#009688"), + TextColor = Colors.White + }; + tallBtn.Clicked += (s, e) => LogEvent("Tall button clicked"); + + var roundBtn = new Button + { + Text = "Round", + WidthRequest = 80, + HeightRequest = 80, + CornerRadius = 40, + BackgroundColor = Color.FromArgb("#E91E63"), + TextColor = Colors.White + }; + roundBtn.Clicked += (s, e) => LogEvent("Round button clicked"); + + layout.Children.Add(wideBtn); + layout.Children.Add(tallBtn); + layout.Children.Add(new HorizontalStackLayout { Children = { roundBtn } }); + + return layout; + } + + private Frame CreateSection(string title, View content) + { + return new Frame + { + CornerRadius = 8, + Padding = new Thickness(15), + BackgroundColor = Colors.White, + Content = new VerticalStackLayout + { + Spacing = 10, + Children = + { + new Label { Text = title, FontSize = 16, FontAttributes = FontAttributes.Bold }, + content + } + } + }; + } + + private View CreateEventLogPanel() + { + return new Frame + { + BackgroundColor = Color.FromArgb("#F5F5F5"), + Padding = new Thickness(10), + CornerRadius = 0, + Content = new VerticalStackLayout + { + Children = + { + new Label { Text = "Event Log:", FontSize = 12, FontAttributes = FontAttributes.Bold }, + new ScrollView + { + HeightRequest = 80, + Content = _eventLog + } + } + } + }; + } + + private void LogEvent(string message) + { + _eventCount++; + var timestamp = DateTime.Now.ToString("HH:mm:ss"); + _eventLog.Text = $"[{timestamp}] {_eventCount}. {message}\n{_eventLog.Text}"; + } +} diff --git a/ShellDemo/Pages/ControlsPage.cs b/ShellDemo/Pages/ControlsPage.cs new file mode 100644 index 0000000..6478bdc --- /dev/null +++ b/ShellDemo/Pages/ControlsPage.cs @@ -0,0 +1,203 @@ +// ControlsPage - Demonstrates various MAUI controls + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace ShellDemo; + +public class ControlsPage : ContentPage +{ + public ControlsPage() + { + Title = "Controls"; + + Content = new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 15, + Children = + { + new Label + { + Text = "Control Gallery", + FontSize = 24, + FontAttributes = FontAttributes.Bold + }, + + // Buttons + CreateSection("Buttons", new View[] + { + CreateButtonRow() + }), + + // CheckBox & Switch + CreateSection("Selection", new View[] + { + CreateCheckBoxRow(), + CreateSwitchRow() + }), + + // Slider + CreateSection("Slider", new View[] + { + CreateSliderRow() + }), + + // Picker + CreateSection("Picker", new View[] + { + CreatePickerRow() + }), + + // Progress + CreateSection("Progress", new View[] + { + CreateProgressRow() + }) + } + } + }; + } + + private Frame CreateSection(string title, View[] content) + { + var layout = new VerticalStackLayout { Spacing = 10 }; + layout.Children.Add(new Label + { + Text = title, + FontSize = 18, + FontAttributes = FontAttributes.Bold + }); + + foreach (var view in content) + { + layout.Children.Add(view); + } + + return new Frame + { + CornerRadius = 8, + Padding = new Thickness(15), + BackgroundColor = Colors.White, + Content = layout + }; + } + + private View CreateButtonRow() + { + var resultLabel = new Label { TextColor = Colors.Gray, FontSize = 12 }; + + var layout = new VerticalStackLayout { Spacing = 10 }; + + var buttonRow = new HorizontalStackLayout { Spacing = 10 }; + + var primaryBtn = new Button { Text = "Primary", BackgroundColor = Color.FromArgb("#2196F3"), TextColor = Colors.White }; + primaryBtn.Clicked += (s, e) => resultLabel.Text = "Primary clicked!"; + + var successBtn = new Button { Text = "Success", BackgroundColor = Color.FromArgb("#4CAF50"), TextColor = Colors.White }; + successBtn.Clicked += (s, e) => resultLabel.Text = "Success clicked!"; + + var dangerBtn = new Button { Text = "Danger", BackgroundColor = Color.FromArgb("#F44336"), TextColor = Colors.White }; + dangerBtn.Clicked += (s, e) => resultLabel.Text = "Danger clicked!"; + + buttonRow.Children.Add(primaryBtn); + buttonRow.Children.Add(successBtn); + buttonRow.Children.Add(dangerBtn); + + layout.Children.Add(buttonRow); + layout.Children.Add(resultLabel); + + return layout; + } + + private View CreateCheckBoxRow() + { + var layout = new HorizontalStackLayout { Spacing = 20 }; + + var cb1 = new CheckBox { IsChecked = true }; + var cb2 = new CheckBox { IsChecked = false }; + + layout.Children.Add(cb1); + layout.Children.Add(new Label { Text = "Option 1", VerticalOptions = LayoutOptions.Center }); + layout.Children.Add(cb2); + layout.Children.Add(new Label { Text = "Option 2", VerticalOptions = LayoutOptions.Center }); + + return layout; + } + + private View CreateSwitchRow() + { + var label = new Label { Text = "Off", VerticalOptions = LayoutOptions.Center }; + var sw = new Switch { IsToggled = false }; + sw.Toggled += (s, e) => label.Text = e.Value ? "On" : "Off"; + + return new HorizontalStackLayout + { + Spacing = 10, + Children = { sw, label } + }; + } + + private View CreateSliderRow() + { + var label = new Label { Text = "Value: 50" }; + var slider = new Slider { Minimum = 0, Maximum = 100, Value = 50 }; + slider.ValueChanged += (s, e) => label.Text = $"Value: {(int)e.NewValue}"; + + return new VerticalStackLayout + { + Spacing = 5, + Children = { slider, label } + }; + } + + private View CreatePickerRow() + { + var label = new Label { Text = "Selected: (none)", TextColor = Colors.Gray }; + var picker = new Picker { Title = "Select a fruit" }; + picker.Items.Add("Apple"); + picker.Items.Add("Banana"); + picker.Items.Add("Cherry"); + picker.Items.Add("Date"); + picker.Items.Add("Elderberry"); + + picker.SelectedIndexChanged += (s, e) => + { + if (picker.SelectedIndex >= 0) + label.Text = $"Selected: {picker.Items[picker.SelectedIndex]}"; + }; + + return new VerticalStackLayout + { + Spacing = 5, + Children = { picker, label } + }; + } + + private View CreateProgressRow() + { + var progress = new ProgressBar { Progress = 0.7 }; + var activity = new ActivityIndicator { IsRunning = true }; + + return new VerticalStackLayout + { + Spacing = 10, + Children = + { + progress, + new Label { Text = "70% Complete", FontSize = 12, TextColor = Colors.Gray }, + new HorizontalStackLayout + { + Spacing = 10, + Children = + { + activity, + new Label { Text = "Loading...", VerticalOptions = LayoutOptions.Center, TextColor = Colors.Gray } + } + } + } + }; + } +} diff --git a/ShellDemo/Pages/DetailPage.cs b/ShellDemo/Pages/DetailPage.cs new file mode 100644 index 0000000..438751b --- /dev/null +++ b/ShellDemo/Pages/DetailPage.cs @@ -0,0 +1,123 @@ +// DetailPage - Demonstrates push/pop navigation + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform.Linux.Hosting; + +namespace ShellDemo; + +/// +/// A detail page that can be pushed onto the navigation stack. +/// +public class DetailPage : ContentPage +{ + private readonly string _itemName; + + public DetailPage() : this("Detail Item") + { + } + + public DetailPage(string itemName) + { + _itemName = itemName; + Title = "Detail Page"; + + Content = new VerticalStackLayout + { + Padding = new Thickness(30), + Spacing = 20, + VerticalOptions = LayoutOptions.Center, + Children = + { + new Label + { + Text = "Pushed Page", + FontSize = 28, + FontAttributes = FontAttributes.Bold, + HorizontalOptions = LayoutOptions.Center, + TextColor = Color.FromArgb("#9C27B0") + }, + + new Label + { + Text = $"You navigated to: {_itemName}", + FontSize = 16, + HorizontalOptions = LayoutOptions.Center + }, + + new Label + { + Text = "This page was pushed onto the navigation stack using Shell.Current.GoToAsync()", + FontSize = 14, + TextColor = Colors.Gray, + HorizontalTextAlignment = TextAlignment.Center, + LineBreakMode = LineBreakMode.WordWrap + }, + + new BoxView + { + HeightRequest = 2, + Color = Color.FromArgb("#E0E0E0"), + Margin = new Thickness(0, 20) + }, + + CreateBackButton(), + + new Label + { + Text = "Use the back button above or the hardware/gesture back to pop this page", + FontSize = 12, + TextColor = Colors.Gray, + HorizontalTextAlignment = TextAlignment.Center, + Margin = new Thickness(0, 20, 0, 0) + } + } + }; + } + + private Button CreateBackButton() + { + var backBtn = new Button + { + Text = "Go Back (Pop)", + BackgroundColor = Color.FromArgb("#9C27B0"), + TextColor = Colors.White, + HorizontalOptions = LayoutOptions.Center, + Padding = new Thickness(30, 10) + }; + + backBtn.Clicked += (s, e) => + { + // Pop this page off the navigation stack using LinuxViewRenderer + Console.WriteLine("[DetailPage] Go Back clicked"); + var success = LinuxViewRenderer.PopPage(); + Console.WriteLine($"[DetailPage] PopPage result: {success}"); + }; + + return backBtn; + } +} + +/// +/// Query property for passing data to DetailPage. +/// +[QueryProperty(nameof(ItemName), "item")] +public class DetailPageWithQuery : DetailPage +{ + private string _itemName = "Item"; + + public string ItemName + { + get => _itemName; + set + { + _itemName = value; + // Update the title when the property is set + Title = $"Detail: {value}"; + } + } + + public DetailPageWithQuery() : base() + { + } +} diff --git a/ShellDemo/Pages/GridsPage.cs b/ShellDemo/Pages/GridsPage.cs new file mode 100644 index 0000000..09cea44 --- /dev/null +++ b/ShellDemo/Pages/GridsPage.cs @@ -0,0 +1,594 @@ +// GridsPage - Demonstrates Grid layouts with various options + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace ShellDemo; + +public class GridsPage : ContentPage +{ + public GridsPage() + { + Title = "Grids"; + + Content = new ScrollView + { + Orientation = ScrollOrientation.Both, + Content = new VerticalStackLayout + { + Spacing = 25, + Children = + { + CreateSectionHeader("Basic Grid (2x2)"), + CreateBasicGrid(), + + CreateSectionHeader("Column Definitions"), + CreateColumnDefinitionsDemo(), + + CreateSectionHeader("Row Definitions"), + CreateRowDefinitionsDemo(), + + CreateSectionHeader("Auto Rows (Empty vs Content)"), + CreateAutoRowsDemo(), + + CreateSectionHeader("Star Sizing (Proportional)"), + CreateStarSizingDemo(), + + CreateSectionHeader("Row & Column Spacing"), + CreateSpacingDemo(), + + CreateSectionHeader("Row & Column Span"), + CreateSpanDemo(), + + CreateSectionHeader("Mixed Sizing"), + CreateMixedSizingDemo(), + + CreateSectionHeader("Nested Grids"), + CreateNestedGridDemo(), + + new BoxView { HeightRequest = 20 } // Bottom padding + } + } + }; + } + + private Label CreateSectionHeader(string text) + { + return new Label + { + Text = text, + FontSize = 18, + FontAttributes = FontAttributes.Bold, + TextColor = Color.FromArgb("#2196F3"), + Margin = new Thickness(0, 10, 0, 5) + }; + } + + private View CreateBasicGrid() + { + var grid = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star } + }, + BackgroundColor = Color.FromArgb("#F5F5F5") + }; + + var cell1 = CreateCell("Row 0, Col 0", "#E3F2FD"); + var cell2 = CreateCell("Row 0, Col 1", "#E8F5E9"); + var cell3 = CreateCell("Row 1, Col 0", "#FFF3E0"); + var cell4 = CreateCell("Row 1, Col 1", "#FCE4EC"); + + Grid.SetRow(cell1, 0); Grid.SetColumn(cell1, 0); + Grid.SetRow(cell2, 0); Grid.SetColumn(cell2, 1); + Grid.SetRow(cell3, 1); Grid.SetColumn(cell3, 0); + Grid.SetRow(cell4, 1); Grid.SetColumn(cell4, 1); + + grid.Children.Add(cell1); + grid.Children.Add(cell2); + grid.Children.Add(cell3); + grid.Children.Add(cell4); + + return CreateDemoContainer(grid, "Equal columns using Star sizing"); + } + + private View CreateColumnDefinitionsDemo() + { + var stack = new VerticalStackLayout { Spacing = 15 }; + + // Auto width columns + var autoGrid = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Auto }, + new ColumnDefinition { Width = GridLength.Auto }, + new ColumnDefinition { Width = GridLength.Auto } + }, + BackgroundColor = Color.FromArgb("#F5F5F5") + }; + + var a1 = CreateCell("Auto", "#BBDEFB"); + var a2 = CreateCell("Auto Width", "#C8E6C9"); + var a3 = CreateCell("A", "#FFECB3"); + Grid.SetColumn(a1, 0); + Grid.SetColumn(a2, 1); + Grid.SetColumn(a3, 2); + autoGrid.Children.Add(a1); + autoGrid.Children.Add(a2); + autoGrid.Children.Add(a3); + + stack.Children.Add(new Label { Text = "Auto: Sizes to content", FontSize = 12, TextColor = Colors.Gray }); + stack.Children.Add(autoGrid); + + // Absolute width columns + var absoluteGrid = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition { Width = new GridLength(50) }, + new ColumnDefinition { Width = new GridLength(100) }, + new ColumnDefinition { Width = new GridLength(150) } + }, + BackgroundColor = Color.FromArgb("#F5F5F5") + }; + + var b1 = CreateCell("50px", "#BBDEFB"); + var b2 = CreateCell("100px", "#C8E6C9"); + var b3 = CreateCell("150px", "#FFECB3"); + Grid.SetColumn(b1, 0); + Grid.SetColumn(b2, 1); + Grid.SetColumn(b3, 2); + absoluteGrid.Children.Add(b1); + absoluteGrid.Children.Add(b2); + absoluteGrid.Children.Add(b3); + + stack.Children.Add(new Label { Text = "Absolute: Fixed pixel widths (50, 100, 150)", FontSize = 12, TextColor = Colors.Gray, Margin = new Thickness(0, 10, 0, 0) }); + stack.Children.Add(absoluteGrid); + + return stack; + } + + private View CreateRowDefinitionsDemo() + { + var grid = new Grid + { + WidthRequest = 200, + RowDefinitions = + { + new RowDefinition { Height = new GridLength(30) }, + new RowDefinition { Height = new GridLength(50) }, + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = new GridLength(40) } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star } + }, + BackgroundColor = Color.FromArgb("#F5F5F5") + }; + + var r1 = CreateCell("30px height", "#BBDEFB"); + var r2 = CreateCell("50px height", "#C8E6C9"); + var r3 = CreateCell("Auto height\n(fits content)", "#FFECB3"); + var r4 = CreateCell("40px height", "#F8BBD9"); + + Grid.SetRow(r1, 0); + Grid.SetRow(r2, 1); + Grid.SetRow(r3, 2); + Grid.SetRow(r4, 3); + + grid.Children.Add(r1); + grid.Children.Add(r2); + grid.Children.Add(r3); + grid.Children.Add(r4); + + return CreateDemoContainer(grid, "Different row heights: 30px, 50px, Auto, 40px"); + } + + private View CreateAutoRowsDemo() + { + var stack = new VerticalStackLayout { Spacing = 15 }; + + // Grid with empty Auto row + var emptyAutoGrid = new Grid + { + WidthRequest = 250, + RowDefinitions = + { + new RowDefinition { Height = new GridLength(40) }, + new RowDefinition { Height = GridLength.Auto }, // Empty - should collapse + new RowDefinition { Height = new GridLength(40) } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star } + }, + BackgroundColor = Color.FromArgb("#E0E0E0") + }; + + var r1 = CreateCell("Row 0: 40px", "#BBDEFB"); + // Row 1 is Auto with NO content - should be 0 height + var r3 = CreateCell("Row 2: 40px", "#C8E6C9"); + + Grid.SetRow(r1, 0); + Grid.SetRow(r3, 2); // Skip row 1 + + emptyAutoGrid.Children.Add(r1); + emptyAutoGrid.Children.Add(r3); + + stack.Children.Add(new Label { Text = "Empty Auto row (Row 1) should collapse to 0 height:", FontSize = 12, TextColor = Colors.Gray }); + stack.Children.Add(emptyAutoGrid); + + // Grid with Auto row that has content + var contentAutoGrid = new Grid + { + WidthRequest = 250, + RowDefinitions = + { + new RowDefinition { Height = new GridLength(40) }, + new RowDefinition { Height = GridLength.Auto }, // Has content + new RowDefinition { Height = new GridLength(40) } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star } + }, + BackgroundColor = Color.FromArgb("#E0E0E0") + }; + + var c1 = CreateCell("Row 0: 40px", "#BBDEFB"); + var c2 = CreateCell("Row 1: Auto (sized to this content)", "#FFECB3"); + var c3 = CreateCell("Row 2: 40px", "#C8E6C9"); + + Grid.SetRow(c1, 0); + Grid.SetRow(c2, 1); + Grid.SetRow(c3, 2); + + contentAutoGrid.Children.Add(c1); + contentAutoGrid.Children.Add(c2); + contentAutoGrid.Children.Add(c3); + + stack.Children.Add(new Label { Text = "Auto row with content sizes to fit:", FontSize = 12, TextColor = Colors.Gray, Margin = new Thickness(0, 10, 0, 0) }); + stack.Children.Add(contentAutoGrid); + + return stack; + } + + private View CreateStarSizingDemo() + { + var grid = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }, + new ColumnDefinition { Width = new GridLength(2, GridUnitType.Star) }, + new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) } + }, + BackgroundColor = Color.FromArgb("#F5F5F5") + }; + + var s1 = CreateCell("1*", "#BBDEFB"); + var s2 = CreateCell("2* (double)", "#C8E6C9"); + var s3 = CreateCell("1*", "#FFECB3"); + + Grid.SetColumn(s1, 0); + Grid.SetColumn(s2, 1); + Grid.SetColumn(s3, 2); + + grid.Children.Add(s1); + grid.Children.Add(s2); + grid.Children.Add(s3); + + return CreateDemoContainer(grid, "Star proportions: 1* | 2* | 1* = 25% | 50% | 25%"); + } + + private View CreateSpacingDemo() + { + var stack = new VerticalStackLayout { Spacing = 15 }; + + // No spacing + var noSpacing = new Grid + { + RowSpacing = 0, + ColumnSpacing = 0, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star } + } + }; + AddFourCells(noSpacing); + stack.Children.Add(new Label { Text = "No spacing (RowSpacing=0, ColumnSpacing=0)", FontSize = 12, TextColor = Colors.Gray }); + stack.Children.Add(noSpacing); + + // With spacing + var withSpacing = new Grid + { + RowSpacing = 10, + ColumnSpacing = 10, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star } + } + }; + AddFourCells(withSpacing); + stack.Children.Add(new Label { Text = "With spacing (RowSpacing=10, ColumnSpacing=10)", FontSize = 12, TextColor = Colors.Gray, Margin = new Thickness(0, 10, 0, 0) }); + stack.Children.Add(withSpacing); + + // Different row/column spacing + var mixedSpacing = new Grid + { + RowSpacing = 5, + ColumnSpacing = 20, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star } + } + }; + AddFourCells(mixedSpacing); + stack.Children.Add(new Label { Text = "Mixed spacing (RowSpacing=5, ColumnSpacing=20)", FontSize = 12, TextColor = Colors.Gray, Margin = new Thickness(0, 10, 0, 0) }); + stack.Children.Add(mixedSpacing); + + return stack; + } + + private View CreateSpanDemo() + { + var grid = new Grid + { + RowSpacing = 5, + ColumnSpacing = 5, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star } + } + }; + + // Spanning header + var header = CreateCell("ColumnSpan=3 (Header)", "#1976D2", Colors.White); + Grid.SetRow(header, 0); + Grid.SetColumn(header, 0); + Grid.SetColumnSpan(header, 3); + + // Left sidebar spanning 2 rows + var sidebar = CreateCell("RowSpan=2\n(Sidebar)", "#388E3C", Colors.White); + Grid.SetRow(sidebar, 1); + Grid.SetColumn(sidebar, 0); + Grid.SetRowSpan(sidebar, 2); + + // Content cells + var content1 = CreateCell("Content 1", "#E3F2FD"); + Grid.SetRow(content1, 1); + Grid.SetColumn(content1, 1); + + var content2 = CreateCell("Content 2", "#E8F5E9"); + Grid.SetRow(content2, 1); + Grid.SetColumn(content2, 2); + + var content3 = CreateCell("Content 3", "#FFF3E0"); + Grid.SetRow(content3, 2); + Grid.SetColumn(content3, 1); + + var content4 = CreateCell("Content 4", "#FCE4EC"); + Grid.SetRow(content4, 2); + Grid.SetColumn(content4, 2); + + grid.Children.Add(header); + grid.Children.Add(sidebar); + grid.Children.Add(content1); + grid.Children.Add(content2); + grid.Children.Add(content3); + grid.Children.Add(content4); + + return CreateDemoContainer(grid, "Header spans 3 columns, Sidebar spans 2 rows"); + } + + private View CreateMixedSizingDemo() + { + var grid = new Grid + { + ColumnSpacing = 5, + ColumnDefinitions = + { + new ColumnDefinition { Width = new GridLength(60) }, // Fixed + new ColumnDefinition { Width = GridLength.Star }, // Fill + new ColumnDefinition { Width = GridLength.Auto }, // Auto + new ColumnDefinition { Width = new GridLength(60) } // Fixed + }, + BackgroundColor = Color.FromArgb("#F5F5F5") + }; + + var c1 = CreateCell("60px", "#BBDEFB"); + var c2 = CreateCell("Star (fills remaining)", "#C8E6C9"); + var c3 = CreateCell("Auto", "#FFECB3"); + var c4 = CreateCell("60px", "#F8BBD9"); + + Grid.SetColumn(c1, 0); + Grid.SetColumn(c2, 1); + Grid.SetColumn(c3, 2); + Grid.SetColumn(c4, 3); + + grid.Children.Add(c1); + grid.Children.Add(c2); + grid.Children.Add(c3); + grid.Children.Add(c4); + + return CreateDemoContainer(grid, "Mixed: 60px | Star | Auto | 60px"); + } + + private View CreateNestedGridDemo() + { + var outerGrid = new Grid + { + RowSpacing = 10, + ColumnSpacing = 10, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star } + }, + BackgroundColor = Color.FromArgb("#E0E0E0"), + Padding = new Thickness(10) + }; + + // Nested grid 1 + var innerGrid1 = new Grid + { + RowSpacing = 2, + ColumnSpacing = 2, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star } + } + }; + var i1a = CreateCell("A", "#BBDEFB", null, 8); + var i1b = CreateCell("B", "#90CAF9", null, 8); + var i1c = CreateCell("C", "#64B5F6", null, 8); + var i1d = CreateCell("D", "#42A5F5", null, 8); + Grid.SetRow(i1a, 0); Grid.SetColumn(i1a, 0); + Grid.SetRow(i1b, 0); Grid.SetColumn(i1b, 1); + Grid.SetRow(i1c, 1); Grid.SetColumn(i1c, 0); + Grid.SetRow(i1d, 1); Grid.SetColumn(i1d, 1); + innerGrid1.Children.Add(i1a); + innerGrid1.Children.Add(i1b); + innerGrid1.Children.Add(i1c); + innerGrid1.Children.Add(i1d); + + // Nested grid 2 + var innerGrid2 = new Grid + { + RowSpacing = 2, + ColumnSpacing = 2, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnDefinitions = + { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Star } + } + }; + var i2a = CreateCell("1", "#C8E6C9", null, 8); + var i2b = CreateCell("2", "#A5D6A7", null, 8); + var i2c = CreateCell("3", "#81C784", null, 8); + var i2d = CreateCell("4", "#66BB6A", null, 8); + Grid.SetRow(i2a, 0); Grid.SetColumn(i2a, 0); + Grid.SetRow(i2b, 0); Grid.SetColumn(i2b, 1); + Grid.SetRow(i2c, 1); Grid.SetColumn(i2c, 0); + Grid.SetRow(i2d, 1); Grid.SetColumn(i2d, 1); + innerGrid2.Children.Add(i2a); + innerGrid2.Children.Add(i2b); + innerGrid2.Children.Add(i2c); + innerGrid2.Children.Add(i2d); + + Grid.SetRow(innerGrid1, 0); Grid.SetColumn(innerGrid1, 0); + Grid.SetRow(innerGrid2, 0); Grid.SetColumn(innerGrid2, 1); + + var label1 = new Label { Text = "Outer Grid Row 1", HorizontalOptions = LayoutOptions.Center }; + var label2 = new Label { Text = "Spans both columns", HorizontalOptions = LayoutOptions.Center }; + Grid.SetRow(label1, 1); Grid.SetColumn(label1, 0); + Grid.SetRow(label2, 1); Grid.SetColumn(label2, 1); + + outerGrid.Children.Add(innerGrid1); + outerGrid.Children.Add(innerGrid2); + outerGrid.Children.Add(label1); + outerGrid.Children.Add(label2); + + return CreateDemoContainer(outerGrid, "Outer grid contains two nested 2x2 grids"); + } + + private Border CreateCell(string text, string bgColor, Color? textColor = null, float fontSize = 12) + { + return new Border + { + BackgroundColor = Color.FromArgb(bgColor), + Padding = new Thickness(10, 8), + StrokeThickness = 0, + Content = new Label + { + Text = text, + FontSize = fontSize, + TextColor = textColor ?? Colors.Black, + HorizontalTextAlignment = TextAlignment.Center, + VerticalTextAlignment = TextAlignment.Center + } + }; + } + + private void AddFourCells(Grid grid) + { + var c1 = CreateCell("0,0", "#BBDEFB"); + var c2 = CreateCell("0,1", "#C8E6C9"); + var c3 = CreateCell("1,0", "#FFECB3"); + var c4 = CreateCell("1,1", "#F8BBD9"); + + Grid.SetRow(c1, 0); Grid.SetColumn(c1, 0); + Grid.SetRow(c2, 0); Grid.SetColumn(c2, 1); + Grid.SetRow(c3, 1); Grid.SetColumn(c3, 0); + Grid.SetRow(c4, 1); Grid.SetColumn(c4, 1); + + grid.Children.Add(c1); + grid.Children.Add(c2); + grid.Children.Add(c3); + grid.Children.Add(c4); + } + + private View CreateDemoContainer(View content, string description) + { + return new VerticalStackLayout + { + Spacing = 5, + Children = + { + new Label { Text = description, FontSize = 12, TextColor = Colors.Gray }, + content + } + }; + } +} diff --git a/ShellDemo/Pages/HomePage.cs b/ShellDemo/Pages/HomePage.cs new file mode 100644 index 0000000..9a39e63 --- /dev/null +++ b/ShellDemo/Pages/HomePage.cs @@ -0,0 +1,265 @@ +// HomePage - Welcome page for the demo + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform.Linux.Hosting; + +namespace ShellDemo; + +public class HomePage : ContentPage +{ + public HomePage() + { + Title = "Home"; + + Content = new ScrollView + { + Orientation = ScrollOrientation.Both, // Enable horizontal scrolling when window is too narrow + Content = new VerticalStackLayout + { + Padding = new Thickness(30), + Spacing = 20, + Children = + { + new Label + { + Text = "OpenMaui Linux", + FontSize = 32, + FontAttributes = FontAttributes.Bold, + HorizontalOptions = LayoutOptions.Center, + TextColor = Color.FromArgb("#2196F3") + }, + + new Label + { + Text = "Controls Demo", + FontSize = 20, + HorizontalOptions = LayoutOptions.Center, + TextColor = Colors.Gray + }, + + new BoxView + { + HeightRequest = 2, + Color = Color.FromArgb("#E0E0E0"), + Margin = new Thickness(0, 10) + }, + + new Label + { + Text = "Welcome to the comprehensive controls demonstration for OpenMaui Linux. " + + "This app showcases all the major UI controls available in the framework.", + FontSize = 14, + LineBreakMode = LineBreakMode.WordWrap, + HorizontalTextAlignment = TextAlignment.Center + }, + + CreateFeatureSection(), + + new Label + { + Text = "Use the flyout menu (swipe from left or tap the hamburger icon) to navigate between different control demos.", + FontSize = 12, + TextColor = Colors.Gray, + LineBreakMode = LineBreakMode.WordWrap, + HorizontalTextAlignment = TextAlignment.Center, + Margin = new Thickness(0, 20, 0, 0) + }, + + CreateQuickLinksSection(), + + CreateNavigationDemoSection() + } + } + }; + } + + private View CreateFeatureSection() + { + var grid = new Grid + { + ColumnDefinitions = + { + new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }, + new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) } + }, + RowDefinitions = + { + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Auto } + }, + ColumnSpacing = 15, + RowSpacing = 15, + Margin = new Thickness(0, 20) + }; + + var features = new[] + { + ("Buttons", "Various button styles and events"), + ("Text Input", "Entry, Editor, SearchBar"), + ("Selection", "CheckBox, Switch, Slider"), + ("Pickers", "Picker, DatePicker, TimePicker"), + ("Lists", "CollectionView with selection"), + ("Progress", "ProgressBar, ActivityIndicator") + }; + + for (int i = 0; i < features.Length; i++) + { + var (title, desc) = features[i]; + var card = CreateFeatureCard(title, desc); + Grid.SetRow(card, i / 2); + Grid.SetColumn(card, i % 2); + grid.Children.Add(card); + } + + return grid; + } + + private Frame CreateFeatureCard(string title, string description) + { + return new Frame + { + CornerRadius = 8, + Padding = new Thickness(15), + BackgroundColor = Colors.White, + HasShadow = true, + Content = new VerticalStackLayout + { + Spacing = 5, + Children = + { + new Label + { + Text = title, + FontSize = 14, + FontAttributes = FontAttributes.Bold, + TextColor = Color.FromArgb("#2196F3") + }, + new Label + { + Text = description, + FontSize = 11, + TextColor = Colors.Gray, + LineBreakMode = LineBreakMode.WordWrap + } + } + } + }; + } + + private View CreateQuickLinksSection() + { + var layout = new VerticalStackLayout + { + Spacing = 10, + Margin = new Thickness(0, 20, 0, 0) + }; + + layout.Children.Add(new Label + { + Text = "Quick Actions", + FontSize = 16, + FontAttributes = FontAttributes.Bold, + HorizontalOptions = LayoutOptions.Center + }); + + var buttonRow = new HorizontalStackLayout + { + Spacing = 10, + HorizontalOptions = LayoutOptions.Center + }; + + var buttonsBtn = new Button + { + Text = "Try Buttons", + BackgroundColor = Color.FromArgb("#2196F3"), + TextColor = Colors.White + }; + buttonsBtn.Clicked += (s, e) => LinuxViewRenderer.NavigateToRoute("Buttons"); + + var listsBtn = new Button + { + Text = "Try Lists", + BackgroundColor = Color.FromArgb("#4CAF50"), + TextColor = Colors.White + }; + listsBtn.Clicked += (s, e) => LinuxViewRenderer.NavigateToRoute("Lists"); + + buttonRow.Children.Add(buttonsBtn); + buttonRow.Children.Add(listsBtn); + layout.Children.Add(buttonRow); + + return layout; + } + + private View CreateNavigationDemoSection() + { + var frame = new Frame + { + CornerRadius = 8, + Padding = new Thickness(20), + BackgroundColor = Color.FromArgb("#F3E5F5"), + Margin = new Thickness(0, 20, 0, 0), + Content = new VerticalStackLayout + { + Spacing = 15, + Children = + { + new Label + { + Text = "Navigation Stack Demo", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + TextColor = Color.FromArgb("#9C27B0"), + HorizontalOptions = LayoutOptions.Center + }, + + new Label + { + Text = "Demonstrate push/pop navigation using Shell.GoToAsync()", + FontSize = 12, + TextColor = Colors.Gray, + HorizontalTextAlignment = TextAlignment.Center + }, + + CreatePushButton("Push Detail Page", "detail"), + + new Label + { + Text = "Click the button to push a new page onto the navigation stack. " + + "Use the back button or 'Go Back' to pop it off.", + FontSize = 11, + TextColor = Colors.Gray, + HorizontalTextAlignment = TextAlignment.Center, + LineBreakMode = LineBreakMode.WordWrap + } + } + } + }; + + return frame; + } + + private Button CreatePushButton(string text, string route) + { + var btn = new Button + { + Text = text, + BackgroundColor = Color.FromArgb("#9C27B0"), + TextColor = Colors.White, + HorizontalOptions = LayoutOptions.Center, + Padding = new Thickness(30, 10) + }; + + btn.Clicked += (s, e) => + { + Console.WriteLine($"[HomePage] Push button clicked, navigating to {route}"); + // Use LinuxViewRenderer.PushPage for Skia-based navigation + var success = LinuxViewRenderer.PushPage(new DetailPage()); + Console.WriteLine($"[HomePage] PushPage result: {success}"); + }; + + return btn; + } +} diff --git a/ShellDemo/Pages/ListsPage.cs b/ShellDemo/Pages/ListsPage.cs new file mode 100644 index 0000000..1d93a67 --- /dev/null +++ b/ShellDemo/Pages/ListsPage.cs @@ -0,0 +1,249 @@ +// ListsPage - CollectionView and ListView Demo + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace ShellDemo; + +public class ListsPage : ContentPage +{ + private readonly Label _eventLog; + private int _eventCount = 0; + + public ListsPage() + { + Title = "Lists"; + + _eventLog = new Label + { + Text = "Events will appear here...", + FontSize = 11, + TextColor = Colors.Gray, + LineBreakMode = LineBreakMode.WordWrap + }; + + Content = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }, + new RowDefinition { Height = new GridLength(120) } + }, + Children = + { + CreateMainContent(), + CreateEventLogPanel() + } + }; + + Grid.SetRow((View)((Grid)Content).Children[0], 0); + Grid.SetRow((View)((Grid)Content).Children[1], 1); + } + + private View CreateMainContent() + { + return new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 20, + Children = + { + new Label { Text = "List Controls", FontSize = 24, FontAttributes = FontAttributes.Bold }, + + CreateSection("CollectionView - Fruits", CreateFruitsCollectionView()), + CreateSection("CollectionView - Colors", CreateColorsCollectionView()), + CreateSection("CollectionView - Contacts", CreateContactsCollectionView()) + } + } + }; + } + + private View CreateFruitsCollectionView() + { + var layout = new VerticalStackLayout { Spacing = 10 }; + + var fruits = new List + { + "Apple", "Banana", "Cherry", "Date", "Elderberry", + "Fig", "Grape", "Honeydew", "Kiwi", "Lemon", + "Mango", "Nectarine", "Orange", "Papaya", "Quince" + }; + + var selectedLabel = new Label { Text = "Tap a fruit to select", TextColor = Colors.Gray }; + + var collectionView = new CollectionView + { + ItemsSource = fruits, + HeightRequest = 200, + SelectionMode = SelectionMode.Single, + BackgroundColor = Color.FromArgb("#FAFAFA") + }; + + collectionView.SelectionChanged += (s, e) => + { + if (e.CurrentSelection.Count > 0) + { + var item = e.CurrentSelection[0]?.ToString(); + selectedLabel.Text = $"Selected: {item}"; + LogEvent($"Fruit selected: {item}"); + } + }; + + layout.Children.Add(collectionView); + layout.Children.Add(selectedLabel); + + return layout; + } + + private View CreateColorsCollectionView() + { + var layout = new VerticalStackLayout { Spacing = 10 }; + + var colors = new List + { + new("Red", "#F44336"), + new("Pink", "#E91E63"), + new("Purple", "#9C27B0"), + new("Deep Purple", "#673AB7"), + new("Indigo", "#3F51B5"), + new("Blue", "#2196F3"), + new("Cyan", "#00BCD4"), + new("Teal", "#009688"), + new("Green", "#4CAF50"), + new("Light Green", "#8BC34A"), + new("Lime", "#CDDC39"), + new("Yellow", "#FFEB3B"), + new("Amber", "#FFC107"), + new("Orange", "#FF9800"), + new("Deep Orange", "#FF5722") + }; + + var collectionView = new CollectionView + { + ItemsSource = colors, + HeightRequest = 180, + SelectionMode = SelectionMode.Single, + BackgroundColor = Colors.White + }; + + collectionView.SelectionChanged += (s, e) => + { + if (e.CurrentSelection.Count > 0 && e.CurrentSelection[0] is ColorItem item) + { + LogEvent($"Color selected: {item.Name} ({item.Hex})"); + } + }; + + layout.Children.Add(collectionView); + layout.Children.Add(new Label { Text = "Scroll to see all colors", FontSize = 11, TextColor = Colors.Gray }); + + return layout; + } + + private View CreateContactsCollectionView() + { + var layout = new VerticalStackLayout { Spacing = 10 }; + + var contacts = new List + { + new("Alice Johnson", "alice@example.com", "Engineering"), + new("Bob Smith", "bob@example.com", "Marketing"), + new("Carol Williams", "carol@example.com", "Design"), + new("David Brown", "david@example.com", "Sales"), + new("Eva Martinez", "eva@example.com", "Engineering"), + new("Frank Lee", "frank@example.com", "Support"), + new("Grace Kim", "grace@example.com", "HR"), + new("Henry Wilson", "henry@example.com", "Finance") + }; + + var collectionView = new CollectionView + { + ItemsSource = contacts, + HeightRequest = 200, + SelectionMode = SelectionMode.Single, + BackgroundColor = Colors.White + }; + + collectionView.SelectionChanged += (s, e) => + { + if (e.CurrentSelection.Count > 0 && e.CurrentSelection[0] is ContactItem contact) + { + LogEvent($"Contact: {contact.Name} - {contact.Department}"); + } + }; + + layout.Children.Add(collectionView); + + // Action buttons + var buttonRow = new HorizontalStackLayout { Spacing = 10 }; + var addBtn = new Button { Text = "Add Contact", BackgroundColor = Colors.Green, TextColor = Colors.White }; + addBtn.Clicked += (s, e) => LogEvent("Add contact clicked"); + var deleteBtn = new Button { Text = "Delete Selected", BackgroundColor = Colors.Red, TextColor = Colors.White }; + deleteBtn.Clicked += (s, e) => LogEvent("Delete contact clicked"); + buttonRow.Children.Add(addBtn); + buttonRow.Children.Add(deleteBtn); + layout.Children.Add(buttonRow); + + return layout; + } + + private Frame CreateSection(string title, View content) + { + return new Frame + { + CornerRadius = 8, + Padding = new Thickness(15), + BackgroundColor = Colors.White, + Content = new VerticalStackLayout + { + Spacing = 10, + Children = + { + new Label { Text = title, FontSize = 16, FontAttributes = FontAttributes.Bold }, + content + } + } + }; + } + + private View CreateEventLogPanel() + { + return new Frame + { + BackgroundColor = Color.FromArgb("#F5F5F5"), + Padding = new Thickness(10), + CornerRadius = 0, + Content = new VerticalStackLayout + { + Children = + { + new Label { Text = "Event Log:", FontSize = 12, FontAttributes = FontAttributes.Bold }, + new ScrollView + { + HeightRequest = 80, + Content = _eventLog + } + } + } + }; + } + + private void LogEvent(string message) + { + _eventCount++; + var timestamp = DateTime.Now.ToString("HH:mm:ss"); + _eventLog.Text = $"[{timestamp}] {_eventCount}. {message}\n{_eventLog.Text}"; + } +} + +public record ColorItem(string Name, string Hex) +{ + public override string ToString() => Name; +} + +public record ContactItem(string Name, string Email, string Department) +{ + public override string ToString() => $"{Name} ({Department})"; +} diff --git a/ShellDemo/Pages/PickersPage.cs b/ShellDemo/Pages/PickersPage.cs new file mode 100644 index 0000000..b5ae1d9 --- /dev/null +++ b/ShellDemo/Pages/PickersPage.cs @@ -0,0 +1,261 @@ +// PickersPage - Picker, DatePicker, TimePicker Demo + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace ShellDemo; + +public class PickersPage : ContentPage +{ + private readonly Label _eventLog; + private int _eventCount = 0; + + public PickersPage() + { + Title = "Pickers"; + + _eventLog = new Label + { + Text = "Events will appear here...", + FontSize = 11, + TextColor = Colors.Gray, + LineBreakMode = LineBreakMode.WordWrap + }; + + Content = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }, + new RowDefinition { Height = new GridLength(120) } + }, + Children = + { + CreateMainContent(), + CreateEventLogPanel() + } + }; + + Grid.SetRow((View)((Grid)Content).Children[0], 0); + Grid.SetRow((View)((Grid)Content).Children[1], 1); + } + + private View CreateMainContent() + { + return new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 20, + Children = + { + new Label { Text = "Picker Controls", FontSize = 24, FontAttributes = FontAttributes.Bold }, + + CreateSection("Picker", CreatePickerDemo()), + CreateSection("DatePicker", CreateDatePickerDemo()), + CreateSection("TimePicker", CreateTimePickerDemo()) + } + } + }; + } + + private View CreatePickerDemo() + { + var layout = new VerticalStackLayout { Spacing = 15 }; + + // Basic picker + var selectedLabel = new Label { Text = "Selected: (none)", TextColor = Colors.Gray }; + var picker1 = new Picker { Title = "Select a fruit" }; + picker1.Items.Add("Apple"); + picker1.Items.Add("Banana"); + picker1.Items.Add("Cherry"); + picker1.Items.Add("Date"); + picker1.Items.Add("Elderberry"); + picker1.Items.Add("Fig"); + picker1.Items.Add("Grape"); + picker1.SelectedIndexChanged += (s, e) => + { + if (picker1.SelectedIndex >= 0) + { + var item = picker1.Items[picker1.SelectedIndex]; + selectedLabel.Text = $"Selected: {item}"; + LogEvent($"Fruit selected: {item}"); + } + }; + layout.Children.Add(picker1); + layout.Children.Add(selectedLabel); + + // Picker with default selection + layout.Children.Add(new Label { Text = "With Default Selection:", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var picker2 = new Picker { Title = "Select a color" }; + picker2.Items.Add("Red"); + picker2.Items.Add("Green"); + picker2.Items.Add("Blue"); + picker2.Items.Add("Yellow"); + picker2.Items.Add("Purple"); + picker2.SelectedIndex = 2; // Blue + picker2.SelectedIndexChanged += (s, e) => + { + if (picker2.SelectedIndex >= 0) + LogEvent($"Color selected: {picker2.Items[picker2.SelectedIndex]}"); + }; + layout.Children.Add(picker2); + + // Styled picker + layout.Children.Add(new Label { Text = "Styled Picker:", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var picker3 = new Picker + { + Title = "Select size", + TextColor = Colors.DarkBlue, + TitleColor = Colors.Gray + }; + picker3.Items.Add("Small"); + picker3.Items.Add("Medium"); + picker3.Items.Add("Large"); + picker3.Items.Add("Extra Large"); + picker3.SelectedIndexChanged += (s, e) => + { + if (picker3.SelectedIndex >= 0) + LogEvent($"Size selected: {picker3.Items[picker3.SelectedIndex]}"); + }; + layout.Children.Add(picker3); + + return layout; + } + + private View CreateDatePickerDemo() + { + var layout = new VerticalStackLayout { Spacing = 15 }; + + // Basic date picker + var dateLabel = new Label { Text = $"Selected: {DateTime.Today:d}" }; + var datePicker1 = new DatePicker { Date = DateTime.Today }; + datePicker1.DateSelected += (s, e) => + { + dateLabel.Text = $"Selected: {e.NewDate:d}"; + LogEvent($"Date selected: {e.NewDate:d}"); + }; + layout.Children.Add(datePicker1); + layout.Children.Add(dateLabel); + + // Date picker with range + layout.Children.Add(new Label { Text = "With Date Range (this month only):", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var startOfMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1); + var endOfMonth = startOfMonth.AddMonths(1).AddDays(-1); + var datePicker2 = new DatePicker + { + MinimumDate = startOfMonth, + MaximumDate = endOfMonth, + Date = DateTime.Today + }; + datePicker2.DateSelected += (s, e) => LogEvent($"Date (limited): {e.NewDate:d}"); + layout.Children.Add(datePicker2); + + // Styled date picker + layout.Children.Add(new Label { Text = "Styled DatePicker:", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var datePicker3 = new DatePicker + { + Date = DateTime.Today.AddDays(7), + TextColor = Colors.DarkGreen + }; + datePicker3.DateSelected += (s, e) => LogEvent($"Styled date: {e.NewDate:d}"); + layout.Children.Add(datePicker3); + + return layout; + } + + private View CreateTimePickerDemo() + { + var layout = new VerticalStackLayout { Spacing = 15 }; + + // Basic time picker + var timeLabel = new Label { Text = $"Selected: {DateTime.Now:t}" }; + var timePicker1 = new TimePicker { Time = DateTime.Now.TimeOfDay }; + timePicker1.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(TimePicker.Time)) + { + var time = timePicker1.Time; + timeLabel.Text = $"Selected: {time:hh\\:mm}"; + LogEvent($"Time selected: {time:hh\\:mm}"); + } + }; + layout.Children.Add(timePicker1); + layout.Children.Add(timeLabel); + + // Styled time picker + layout.Children.Add(new Label { Text = "Styled TimePicker:", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var timePicker2 = new TimePicker + { + Time = new TimeSpan(14, 30, 0), + TextColor = Colors.DarkBlue + }; + timePicker2.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(TimePicker.Time)) + LogEvent($"Styled time: {timePicker2.Time:hh\\:mm}"); + }; + layout.Children.Add(timePicker2); + + // Morning alarm example + layout.Children.Add(new Label { Text = "Alarm Time:", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var alarmRow = new HorizontalStackLayout { Spacing = 10 }; + var alarmPicker = new TimePicker { Time = new TimeSpan(7, 0, 0) }; + var alarmBtn = new Button { Text = "Set Alarm", BackgroundColor = Colors.Orange, TextColor = Colors.White }; + alarmBtn.Clicked += (s, e) => LogEvent($"Alarm set for {alarmPicker.Time:hh\\:mm}"); + alarmRow.Children.Add(alarmPicker); + alarmRow.Children.Add(alarmBtn); + layout.Children.Add(alarmRow); + + return layout; + } + + private Frame CreateSection(string title, View content) + { + return new Frame + { + CornerRadius = 8, + Padding = new Thickness(15), + BackgroundColor = Colors.White, + Content = new VerticalStackLayout + { + Spacing = 10, + Children = + { + new Label { Text = title, FontSize = 16, FontAttributes = FontAttributes.Bold }, + content + } + } + }; + } + + private View CreateEventLogPanel() + { + return new Frame + { + BackgroundColor = Color.FromArgb("#F5F5F5"), + Padding = new Thickness(10), + CornerRadius = 0, + Content = new VerticalStackLayout + { + Children = + { + new Label { Text = "Event Log:", FontSize = 12, FontAttributes = FontAttributes.Bold }, + new ScrollView + { + HeightRequest = 80, + Content = _eventLog + } + } + } + }; + } + + private void LogEvent(string message) + { + _eventCount++; + var timestamp = DateTime.Now.ToString("HH:mm:ss"); + _eventLog.Text = $"[{timestamp}] {_eventCount}. {message}\n{_eventLog.Text}"; + } +} diff --git a/ShellDemo/Pages/ProgressPage.cs b/ShellDemo/Pages/ProgressPage.cs new file mode 100644 index 0000000..87e4828 --- /dev/null +++ b/ShellDemo/Pages/ProgressPage.cs @@ -0,0 +1,261 @@ +// ProgressPage - ProgressBar and ActivityIndicator Demo + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace ShellDemo; + +public class ProgressPage : ContentPage +{ + private readonly Label _eventLog; + private int _eventCount = 0; + private ProgressBar? _animatedProgress; + private bool _isAnimating = false; + + public ProgressPage() + { + Title = "Progress"; + + _eventLog = new Label + { + Text = "Events will appear here...", + FontSize = 11, + TextColor = Colors.Gray, + LineBreakMode = LineBreakMode.WordWrap + }; + + Content = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }, + new RowDefinition { Height = new GridLength(120) } + }, + Children = + { + CreateMainContent(), + CreateEventLogPanel() + } + }; + + Grid.SetRow((View)((Grid)Content).Children[0], 0); + Grid.SetRow((View)((Grid)Content).Children[1], 1); + } + + private View CreateMainContent() + { + return new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 20, + Children = + { + new Label { Text = "Progress Indicators", FontSize = 24, FontAttributes = FontAttributes.Bold }, + + CreateSection("ProgressBar", CreateProgressBarDemo()), + CreateSection("ActivityIndicator", CreateActivityIndicatorDemo()), + CreateSection("Interactive Demo", CreateInteractiveDemo()) + } + } + }; + } + + private View CreateProgressBarDemo() + { + var layout = new VerticalStackLayout { Spacing = 15 }; + + // Various progress values + var values = new[] { 0.0, 0.25, 0.5, 0.75, 1.0 }; + foreach (var value in values) + { + var row = new HorizontalStackLayout { Spacing = 10 }; + var progress = new ProgressBar { Progress = value, WidthRequest = 200 }; + var label = new Label { Text = $"{value * 100:0}%", VerticalOptions = LayoutOptions.Center, WidthRequest = 50 }; + row.Children.Add(progress); + row.Children.Add(label); + layout.Children.Add(row); + } + + // Colored progress bars + layout.Children.Add(new Label { Text = "Colored Progress Bars:", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + + var colors = new[] { Colors.Red, Colors.Green, Colors.Blue, Colors.Orange, Colors.Purple }; + foreach (var color in colors) + { + var progress = new ProgressBar { Progress = 0.7, ProgressColor = color }; + layout.Children.Add(progress); + } + + return layout; + } + + private View CreateActivityIndicatorDemo() + { + var layout = new VerticalStackLayout { Spacing = 15 }; + + // Running indicator + var runningRow = new HorizontalStackLayout { Spacing = 15 }; + var runningIndicator = new ActivityIndicator { IsRunning = true }; + runningRow.Children.Add(runningIndicator); + runningRow.Children.Add(new Label { Text = "Loading...", VerticalOptions = LayoutOptions.Center }); + layout.Children.Add(runningRow); + + // Toggle indicator + var toggleRow = new HorizontalStackLayout { Spacing = 15 }; + var toggleIndicator = new ActivityIndicator { IsRunning = false }; + var toggleBtn = new Button { Text = "Start/Stop" }; + toggleBtn.Clicked += (s, e) => + { + toggleIndicator.IsRunning = !toggleIndicator.IsRunning; + LogEvent($"ActivityIndicator: {(toggleIndicator.IsRunning ? "Started" : "Stopped")}"); + }; + toggleRow.Children.Add(toggleIndicator); + toggleRow.Children.Add(toggleBtn); + layout.Children.Add(toggleRow); + + // Colored indicators + layout.Children.Add(new Label { Text = "Colored Indicators:", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var colorRow = new HorizontalStackLayout { Spacing = 20 }; + var indicatorColors = new[] { Colors.Red, Colors.Green, Colors.Blue, Colors.Orange }; + foreach (var color in indicatorColors) + { + var indicator = new ActivityIndicator { IsRunning = true, Color = color }; + colorRow.Children.Add(indicator); + } + layout.Children.Add(colorRow); + + return layout; + } + + private View CreateInteractiveDemo() + { + var layout = new VerticalStackLayout { Spacing = 15 }; + + // Slider-controlled progress + var progressLabel = new Label { Text = "Progress: 50%" }; + _animatedProgress = new ProgressBar { Progress = 0.5 }; + + var slider = new Slider { Minimum = 0, Maximum = 100, Value = 50 }; + slider.ValueChanged += (s, e) => + { + var value = e.NewValue / 100.0; + _animatedProgress.Progress = value; + progressLabel.Text = $"Progress: {e.NewValue:0}%"; + }; + + layout.Children.Add(_animatedProgress); + layout.Children.Add(slider); + layout.Children.Add(progressLabel); + + // Animated progress buttons + var buttonRow = new HorizontalStackLayout { Spacing = 10, Margin = new Thickness(0, 10, 0, 0) }; + + var resetBtn = new Button { Text = "Reset", BackgroundColor = Colors.Gray, TextColor = Colors.White }; + resetBtn.Clicked += async (s, e) => + { + _animatedProgress.Progress = 0; + slider.Value = 0; + LogEvent("Progress reset to 0%"); + }; + + var animateBtn = new Button { Text = "Animate to 100%", BackgroundColor = Colors.Blue, TextColor = Colors.White }; + animateBtn.Clicked += async (s, e) => + { + if (_isAnimating) return; + _isAnimating = true; + LogEvent("Animation started"); + + for (int i = (int)(slider.Value); i <= 100; i += 5) + { + _animatedProgress.Progress = i / 100.0; + slider.Value = i; + await Task.Delay(100); + } + + _isAnimating = false; + LogEvent("Animation completed"); + }; + + var simulateBtn = new Button { Text = "Simulate Download", BackgroundColor = Colors.Green, TextColor = Colors.White }; + simulateBtn.Clicked += async (s, e) => + { + if (_isAnimating) return; + _isAnimating = true; + LogEvent("Download simulation started"); + + _animatedProgress.Progress = 0; + slider.Value = 0; + + var random = new Random(); + double progress = 0; + while (progress < 1.0) + { + progress += random.NextDouble() * 0.1; + if (progress > 1.0) progress = 1.0; + _animatedProgress.Progress = progress; + slider.Value = progress * 100; + await Task.Delay(200 + random.Next(300)); + } + + _isAnimating = false; + LogEvent("Download simulation completed"); + }; + + buttonRow.Children.Add(resetBtn); + buttonRow.Children.Add(animateBtn); + buttonRow.Children.Add(simulateBtn); + layout.Children.Add(buttonRow); + + return layout; + } + + private Frame CreateSection(string title, View content) + { + return new Frame + { + CornerRadius = 8, + Padding = new Thickness(15), + BackgroundColor = Colors.White, + Content = new VerticalStackLayout + { + Spacing = 10, + Children = + { + new Label { Text = title, FontSize = 16, FontAttributes = FontAttributes.Bold }, + content + } + } + }; + } + + private View CreateEventLogPanel() + { + return new Frame + { + BackgroundColor = Color.FromArgb("#F5F5F5"), + Padding = new Thickness(10), + CornerRadius = 0, + Content = new VerticalStackLayout + { + Children = + { + new Label { Text = "Event Log:", FontSize = 12, FontAttributes = FontAttributes.Bold }, + new ScrollView + { + HeightRequest = 80, + Content = _eventLog + } + } + } + }; + } + + private void LogEvent(string message) + { + _eventCount++; + var timestamp = DateTime.Now.ToString("HH:mm:ss"); + _eventLog.Text = $"[{timestamp}] {_eventCount}. {message}\n{_eventLog.Text}"; + } +} diff --git a/ShellDemo/Pages/SelectionPage.cs b/ShellDemo/Pages/SelectionPage.cs new file mode 100644 index 0000000..e247af6 --- /dev/null +++ b/ShellDemo/Pages/SelectionPage.cs @@ -0,0 +1,239 @@ +// SelectionPage - CheckBox, Switch, Slider Demo + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace ShellDemo; + +public class SelectionPage : ContentPage +{ + private readonly Label _eventLog; + private int _eventCount = 0; + + public SelectionPage() + { + Title = "Selection Controls"; + + _eventLog = new Label + { + Text = "Events will appear here...", + FontSize = 11, + TextColor = Colors.Gray, + LineBreakMode = LineBreakMode.WordWrap + }; + + Content = new Grid + { + RowDefinitions = + { + new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }, + new RowDefinition { Height = new GridLength(120) } + }, + Children = + { + CreateMainContent(), + CreateEventLogPanel() + } + }; + + Grid.SetRow((View)((Grid)Content).Children[0], 0); + Grid.SetRow((View)((Grid)Content).Children[1], 1); + } + + private View CreateMainContent() + { + return new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 20, + Children = + { + new Label { Text = "Selection Controls", FontSize = 24, FontAttributes = FontAttributes.Bold }, + + CreateSection("CheckBox", CreateCheckBoxDemo()), + CreateSection("Switch", CreateSwitchDemo()), + CreateSection("Slider", CreateSliderDemo()) + } + } + }; + } + + private View CreateCheckBoxDemo() + { + var layout = new VerticalStackLayout { Spacing = 15 }; + + // Basic checkboxes + var basicRow = new HorizontalStackLayout { Spacing = 20 }; + + var cb1 = new CheckBox { IsChecked = false }; + cb1.CheckedChanged += (s, e) => LogEvent($"Checkbox 1: {(e.Value ? "Checked" : "Unchecked")}"); + basicRow.Children.Add(cb1); + basicRow.Children.Add(new Label { Text = "Option 1", VerticalOptions = LayoutOptions.Center }); + + var cb2 = new CheckBox { IsChecked = true }; + cb2.CheckedChanged += (s, e) => LogEvent($"Checkbox 2: {(e.Value ? "Checked" : "Unchecked")}"); + basicRow.Children.Add(cb2); + basicRow.Children.Add(new Label { Text = "Option 2 (default checked)", VerticalOptions = LayoutOptions.Center }); + + layout.Children.Add(basicRow); + + // Colored checkboxes + var colorRow = new HorizontalStackLayout { Spacing = 20 }; + var colors = new[] { Colors.Red, Colors.Green, Colors.Blue, Colors.Purple }; + foreach (var color in colors) + { + var cb = new CheckBox { Color = color, IsChecked = true }; + cb.CheckedChanged += (s, e) => LogEvent($"{color} checkbox: {(e.Value ? "Checked" : "Unchecked")}"); + colorRow.Children.Add(cb); + } + layout.Children.Add(new Label { Text = "Colored Checkboxes:", FontSize = 12 }); + layout.Children.Add(colorRow); + + // Disabled checkbox + var disabledRow = new HorizontalStackLayout { Spacing = 10 }; + var disabledCb = new CheckBox { IsChecked = true, IsEnabled = false }; + disabledRow.Children.Add(disabledCb); + disabledRow.Children.Add(new Label { Text = "Disabled (checked)", VerticalOptions = LayoutOptions.Center, TextColor = Colors.Gray }); + layout.Children.Add(disabledRow); + + return layout; + } + + private View CreateSwitchDemo() + { + var layout = new VerticalStackLayout { Spacing = 15 }; + + // Basic switch + var basicRow = new HorizontalStackLayout { Spacing = 15 }; + var statusLabel = new Label { Text = "Off", VerticalOptions = LayoutOptions.Center, WidthRequest = 50 }; + var sw1 = new Switch { IsToggled = false }; + sw1.Toggled += (s, e) => + { + statusLabel.Text = e.Value ? "On" : "Off"; + LogEvent($"Switch toggled: {(e.Value ? "ON" : "OFF")}"); + }; + basicRow.Children.Add(sw1); + basicRow.Children.Add(statusLabel); + layout.Children.Add(basicRow); + + // Colored switches + var colorRow = new HorizontalStackLayout { Spacing = 20 }; + var switchColors = new[] { Colors.Green, Colors.Orange, Colors.Purple }; + foreach (var color in switchColors) + { + var sw = new Switch { IsToggled = true, OnColor = color }; + sw.Toggled += (s, e) => LogEvent($"{color} switch: {(e.Value ? "ON" : "OFF")}"); + colorRow.Children.Add(sw); + } + layout.Children.Add(new Label { Text = "Colored Switches:", FontSize = 12 }); + layout.Children.Add(colorRow); + + // Disabled switch + var disabledRow = new HorizontalStackLayout { Spacing = 10 }; + var disabledSw = new Switch { IsToggled = true, IsEnabled = false }; + disabledRow.Children.Add(disabledSw); + disabledRow.Children.Add(new Label { Text = "Disabled (on)", VerticalOptions = LayoutOptions.Center, TextColor = Colors.Gray }); + layout.Children.Add(disabledRow); + + return layout; + } + + private View CreateSliderDemo() + { + var layout = new VerticalStackLayout { Spacing = 15 }; + + // Basic slider + var valueLabel = new Label { Text = "Value: 50" }; + var slider1 = new Slider { Minimum = 0, Maximum = 100, Value = 50 }; + slider1.ValueChanged += (s, e) => + { + valueLabel.Text = $"Value: {(int)e.NewValue}"; + LogEvent($"Slider value: {(int)e.NewValue}"); + }; + layout.Children.Add(slider1); + layout.Children.Add(valueLabel); + + // Slider with custom range + layout.Children.Add(new Label { Text = "Temperature (0-40°C):", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var tempLabel = new Label { Text = "20°C" }; + var tempSlider = new Slider { Minimum = 0, Maximum = 40, Value = 20 }; + tempSlider.ValueChanged += (s, e) => + { + tempLabel.Text = $"{(int)e.NewValue}°C"; + LogEvent($"Temperature: {(int)e.NewValue}°C"); + }; + layout.Children.Add(tempSlider); + layout.Children.Add(tempLabel); + + // Colored slider + layout.Children.Add(new Label { Text = "Colored Slider:", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var colorSlider = new Slider + { + Minimum = 0, + Maximum = 100, + Value = 75, + MinimumTrackColor = Colors.Green, + MaximumTrackColor = Colors.LightGray, + ThumbColor = Colors.DarkGreen + }; + colorSlider.ValueChanged += (s, e) => LogEvent($"Colored slider: {(int)e.NewValue}"); + layout.Children.Add(colorSlider); + + // Disabled slider + layout.Children.Add(new Label { Text = "Disabled Slider:", FontSize = 12, Margin = new Thickness(0, 10, 0, 0) }); + var disabledSlider = new Slider { Minimum = 0, Maximum = 100, Value = 30, IsEnabled = false }; + layout.Children.Add(disabledSlider); + + return layout; + } + + private Frame CreateSection(string title, View content) + { + return new Frame + { + CornerRadius = 8, + Padding = new Thickness(15), + BackgroundColor = Colors.White, + Content = new VerticalStackLayout + { + Spacing = 10, + Children = + { + new Label { Text = title, FontSize = 16, FontAttributes = FontAttributes.Bold }, + content + } + } + }; + } + + private View CreateEventLogPanel() + { + return new Frame + { + BackgroundColor = Color.FromArgb("#F5F5F5"), + Padding = new Thickness(10), + CornerRadius = 0, + Content = new VerticalStackLayout + { + Children = + { + new Label { Text = "Event Log:", FontSize = 12, FontAttributes = FontAttributes.Bold }, + new ScrollView + { + HeightRequest = 80, + Content = _eventLog + } + } + } + }; + } + + private void LogEvent(string message) + { + _eventCount++; + var timestamp = DateTime.Now.ToString("HH:mm:ss"); + _eventLog.Text = $"[{timestamp}] {_eventCount}. {message}\n{_eventLog.Text}"; + } +} diff --git a/ShellDemo/Pages/TextInputPage.cs b/ShellDemo/Pages/TextInputPage.cs new file mode 100644 index 0000000..95c4e28 --- /dev/null +++ b/ShellDemo/Pages/TextInputPage.cs @@ -0,0 +1,166 @@ +// TextInputPage - Demonstrates text input controls + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace ShellDemo; + +public class TextInputPage : ContentPage +{ + private Label _entryOutput; + private Label _searchOutput; + private Label _editorOutput; + + public TextInputPage() + { + Title = "Text Input"; + + _entryOutput = new Label { TextColor = Colors.Gray, FontSize = 12 }; + _searchOutput = new Label { TextColor = Colors.Gray, FontSize = 12 }; + _editorOutput = new Label { TextColor = Colors.Gray, FontSize = 12 }; + + Content = new ScrollView + { + Content = new VerticalStackLayout + { + Padding = new Thickness(20), + Spacing = 15, + Children = + { + new Label + { + Text = "Text Input Controls", + FontSize = 24, + FontAttributes = FontAttributes.Bold + }, + new Label + { + Text = "Click on any field and start typing. All keyboard input is handled by the framework.", + FontSize = 14, + TextColor = Colors.Gray + }, + + // Entry Section + new BoxView { HeightRequest = 1, Color = Colors.LightGray }, + new Label { Text = "Entry (Single Line)", FontSize = 18, FontAttributes = FontAttributes.Bold }, + CreateEntry("Enter your name...", e => _entryOutput.Text = $"You typed: {e.Text}"), + _entryOutput, + + CreateEntry("Enter your email...", null, Keyboard.Email), + new Label { Text = "Email keyboard type", FontSize = 12, TextColor = Colors.Gray }, + + CreatePasswordEntry("Enter password..."), + new Label { Text = "Password field (text hidden)", FontSize = 12, TextColor = Colors.Gray }, + + // SearchBar Section + new BoxView { HeightRequest = 1, Color = Colors.LightGray }, + new Label { Text = "SearchBar", FontSize = 18, FontAttributes = FontAttributes.Bold }, + CreateSearchBar(), + _searchOutput, + + // Editor Section + new BoxView { HeightRequest = 1, Color = Colors.LightGray }, + new Label { Text = "Editor (Multi-line)", FontSize = 18, FontAttributes = FontAttributes.Bold }, + CreateEditor(), + _editorOutput, + + // Instructions + new BoxView { HeightRequest = 1, Color = Colors.LightGray }, + new Frame + { + BackgroundColor = Color.FromArgb("#E3F2FD"), + CornerRadius = 8, + Padding = new Thickness(15), + Content = new VerticalStackLayout + { + Spacing = 5, + Children = + { + new Label + { + Text = "Keyboard Shortcuts", + FontAttributes = FontAttributes.Bold + }, + new Label { Text = "Ctrl+A: Select all" }, + new Label { Text = "Ctrl+C: Copy" }, + new Label { Text = "Ctrl+V: Paste" }, + new Label { Text = "Ctrl+X: Cut" }, + new Label { Text = "Home/End: Move to start/end" }, + new Label { Text = "Shift+Arrow: Select text" } + } + } + } + } + } + }; + } + + private Entry CreateEntry(string placeholder, Action? onTextChanged, Keyboard? keyboard = null) + { + var entry = new Entry + { + Placeholder = placeholder, + FontSize = 14 + }; + + if (keyboard != null) + { + entry.Keyboard = keyboard; + } + + if (onTextChanged != null) + { + entry.TextChanged += (s, e) => onTextChanged(entry); + } + + return entry; + } + + private Entry CreatePasswordEntry(string placeholder) + { + return new Entry + { + Placeholder = placeholder, + FontSize = 14, + IsPassword = true + }; + } + + private SearchBar CreateSearchBar() + { + var searchBar = new SearchBar + { + Placeholder = "Search for items..." + }; + + searchBar.TextChanged += (s, e) => + { + _searchOutput.Text = $"Searching: {e.NewTextValue}"; + }; + + searchBar.SearchButtonPressed += (s, e) => + { + _searchOutput.Text = $"Search submitted: {searchBar.Text}"; + }; + + return searchBar; + } + + private Editor CreateEditor() + { + var editor = new Editor + { + Placeholder = "Enter multiple lines of text here...\nPress Enter to create new lines.", + HeightRequest = 120, + FontSize = 14 + }; + + editor.TextChanged += (s, e) => + { + var lineCount = string.IsNullOrEmpty(e.NewTextValue) ? 0 : e.NewTextValue.Split('\n').Length; + _editorOutput.Text = $"Lines: {lineCount}, Characters: {e.NewTextValue?.Length ?? 0}"; + }; + + return editor; + } +} diff --git a/ShellDemo/Platforms/Linux/Program.cs b/ShellDemo/Platforms/Linux/Program.cs new file mode 100644 index 0000000..4330c0f --- /dev/null +++ b/ShellDemo/Platforms/Linux/Program.cs @@ -0,0 +1,19 @@ +// Platforms/Linux/Program.cs - Linux platform entry point +// Same pattern as Android's MainActivity or iOS's AppDelegate + +using Microsoft.Maui.Platform.Linux; +using Microsoft.Maui.Platform.Linux.Hosting; + +namespace ShellDemo; + +class Program +{ + static void Main(string[] args) + { + // Create the shared MAUI app + var app = MauiProgram.CreateMauiApp(); + + // Run on Linux platform + LinuxApplication.Run(app, args); + } +} diff --git a/ShellDemo/README.md b/ShellDemo/README.md new file mode 100644 index 0000000..b734932 --- /dev/null +++ b/ShellDemo/README.md @@ -0,0 +1,157 @@ +# ShellDemo Sample + +A comprehensive control showcase application demonstrating all OpenMaui Linux controls with Shell navigation and flyout menu. + +## Features + +- **Shell Navigation** - Flyout menu with multiple pages +- **Route-Based Navigation** - Push navigation with registered routes +- **All Core Controls** - Button, Entry, Editor, CheckBox, Switch, Slider, Picker, etc. +- **CollectionView** - Lists with selection and data binding +- **Progress Indicators** - ProgressBar and ActivityIndicator with animations +- **Grid Layouts** - Complex multi-column/row layouts +- **Event Logging** - Real-time event feedback panel + +## Pages + +| Page | Controls Demonstrated | +|------|----------------------| +| **Home** | Welcome screen, navigation overview | +| **Buttons** | Button styles, colors, states, click/press/release events | +| **Text Input** | Entry, Editor, SearchBar, password fields, keyboard types | +| **Selection** | CheckBox, Switch, Slider with colors and states | +| **Pickers** | Picker, DatePicker, TimePicker with styling | +| **Lists** | CollectionView with selection, custom items | +| **Progress** | ProgressBar, ActivityIndicator, animated demos | +| **Grids** | Grid layouts with row/column definitions | +| **About** | App information | + +## Architecture + +``` +ShellDemo/ +├── App.cs # AppShell definition with flyout +├── Program.cs # Linux platform bootstrap +├── MauiProgram.cs # MAUI app builder +└── Pages/ + ├── HomePage.cs # Welcome page + ├── ButtonsPage.cs # Button demonstrations + ├── TextInputPage.cs # Entry, Editor, SearchBar + ├── SelectionPage.cs # CheckBox, Switch, Slider + ├── PickersPage.cs # Picker, DatePicker, TimePicker + ├── ListsPage.cs # CollectionView demos + ├── ProgressPage.cs # ProgressBar, ActivityIndicator + ├── GridsPage.cs # Grid layout demos + ├── DetailPage.cs # Push navigation target + └── AboutPage.cs # About information +``` + +## Shell Configuration + +```csharp +public class AppShell : Shell +{ + public AppShell() + { + FlyoutBehavior = FlyoutBehavior.Flyout; + Title = "OpenMaui Controls Demo"; + + // Register routes for push navigation + Routing.RegisterRoute("detail", typeof(DetailPage)); + + // Add flyout items + Items.Add(CreateFlyoutItem("Home", typeof(HomePage))); + Items.Add(CreateFlyoutItem("Buttons", typeof(ButtonsPage))); + // ...more items + } +} +``` + +## Control Demonstrations + +### Buttons Page +- Default, styled, and transparent buttons +- Color variations (Primary, Success, Warning, Danger) +- Enabled/disabled state toggling +- Wide, tall, and round button shapes +- Pressed, clicked, released event handling + +### Text Input Page +- Entry with placeholder and text change events +- Password entry with hidden text +- Email keyboard type +- SearchBar with search button +- Multi-line Editor +- Keyboard shortcuts guide + +### Selection Page +- CheckBox with colors and disabled state +- Switch with OnColor customization +- Slider with min/max range and track colors + +### Pickers Page +- Picker with items and selection events +- DatePicker with date range limits +- TimePicker with time selection +- Styled pickers with custom colors + +### Lists Page +- CollectionView with string items +- CollectionView with custom data types (ColorItem, ContactItem) +- Selection handling and event feedback + +### Progress Page +- ProgressBar at various percentages +- Colored progress bars +- ActivityIndicator running/stopped states +- Colored activity indicators +- Interactive slider-controlled progress +- Animated progress simulation + +## Building and Running + +```bash +# From the maui-linux-push directory +cd samples/ShellDemo +dotnet publish -c Release -r linux-arm64 + +# Run on Linux +./bin/Release/net9.0/linux-arm64/publish/ShellDemo +``` + +## Event Logging + +Each page features an event log panel that displays control interactions in real-time: + +``` +[14:32:15] 3. Button clicked: Primary +[14:32:12] 2. Slider value: 75 +[14:32:08] 1. CheckBox: Checked +``` + +## Controls Reference + +| Control | Properties Demonstrated | +|---------|------------------------| +| Button | Text, BackgroundColor, TextColor, CornerRadius, IsEnabled, WidthRequest, HeightRequest | +| Entry | Placeholder, Text, IsPassword, Keyboard, FontSize | +| Editor | Placeholder, Text, HeightRequest | +| SearchBar | Placeholder, Text, SearchButtonPressed | +| CheckBox | IsChecked, Color, IsEnabled | +| Switch | IsToggled, OnColor, IsEnabled | +| Slider | Minimum, Maximum, Value, MinimumTrackColor, MaximumTrackColor, ThumbColor | +| Picker | Title, Items, SelectedIndex, TextColor, TitleColor | +| DatePicker | Date, MinimumDate, MaximumDate, TextColor | +| TimePicker | Time, TextColor | +| CollectionView | ItemsSource, SelectionMode, SelectionChanged, HeightRequest | +| ProgressBar | Progress, ProgressColor | +| ActivityIndicator | IsRunning, Color | +| Label | Text, FontSize, FontAttributes, TextColor | +| Frame | CornerRadius, Padding, BackgroundColor | +| Grid | RowDefinitions, ColumnDefinitions, RowSpacing, ColumnSpacing | +| StackLayout | Spacing, Padding, Orientation | +| ScrollView | Content scrolling | + +## License + +MIT License - See repository root for details. diff --git a/ShellDemo/ShellDemo.csproj b/ShellDemo/ShellDemo.csproj new file mode 100644 index 0000000..2ffc793 --- /dev/null +++ b/ShellDemo/ShellDemo.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + true + + + + + + + diff --git a/TodoApp/App.cs b/TodoApp/App.cs new file mode 100644 index 0000000..7ffbb8d --- /dev/null +++ b/TodoApp/App.cs @@ -0,0 +1,21 @@ +// TodoApp - Main Application with NavigationPage + +using Microsoft.Maui.Controls; + +namespace TodoApp; + +public class App : Application +{ + public static NavigationPage? NavigationPage { get; private set; } + + public App() + { + NavigationPage = new NavigationPage(new TodoListPage()) + { + Title = "OpenMaui Todo App", + BarBackgroundColor = Color.FromArgb("#2196F3"), + BarTextColor = Colors.White + }; + MainPage = NavigationPage; + } +} diff --git a/TodoApp/MauiProgram.cs b/TodoApp/MauiProgram.cs new file mode 100644 index 0000000..94b7677 --- /dev/null +++ b/TodoApp/MauiProgram.cs @@ -0,0 +1,22 @@ +// MauiProgram.cs - MAUI app configuration + +using Microsoft.Maui.Hosting; +using Microsoft.Maui.Platform.Linux.Hosting; + +namespace TodoApp; + +public static class MauiProgram +{ + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + + // Configure the app + builder.UseMauiApp(); + + // Add Linux platform support with all handlers + builder.UseLinux(); + + return builder.Build(); + } +} diff --git a/TodoApp/NewTodoPage.xaml b/TodoApp/NewTodoPage.xaml new file mode 100644 index 0000000..7f4f1a4 --- /dev/null +++ b/TodoApp/NewTodoPage.xaml @@ -0,0 +1,79 @@ + + + + + #5C6BC0 + #26A69A + #212121 + #757575 + #FFFFFF + #E8EAF6 + + + + + + + + + + + + + + + + + + + diff --git a/TodoApp/NewTodoPage.xaml.cs b/TodoApp/NewTodoPage.xaml.cs new file mode 100644 index 0000000..ed1c90b --- /dev/null +++ b/TodoApp/NewTodoPage.xaml.cs @@ -0,0 +1,31 @@ +// NewTodoPage - Create a new todo item + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace TodoApp; + +public partial class NewTodoPage : ContentPage +{ + private readonly TodoService _service = TodoService.Instance; + + public NewTodoPage() + { + InitializeComponent(); + } + + private async void OnSaveClicked(object? sender, EventArgs e) + { + var title = TitleEntry.Text?.Trim(); + + if (string.IsNullOrEmpty(title)) + { + TitleEntry.Placeholder = "Title is required!"; + TitleEntry.PlaceholderColor = Colors.Red; + return; + } + + _service.AddTodo(title, NotesEditor.Text ?? ""); + await Navigation.PopAsync(); + } +} diff --git a/TodoApp/Program.cs b/TodoApp/Program.cs new file mode 100644 index 0000000..1602090 --- /dev/null +++ b/TodoApp/Program.cs @@ -0,0 +1,67 @@ +// Program.cs - Linux platform entry point + +using Microsoft.Maui.Platform.Linux; + +namespace TodoApp; + +class Program +{ + static void Main(string[] args) + { + // Redirect console output to a log file for debugging + var logPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "todoapp.log"); + using var logWriter = new StreamWriter(logPath, append: false) { AutoFlush = true }; + var multiWriter = new MultiTextWriter(Console.Out, logWriter); + Console.SetOut(multiWriter); + Console.SetError(multiWriter); + + // Global exception handler + AppDomain.CurrentDomain.UnhandledException += (sender, e) => + { + var ex = e.ExceptionObject as Exception; + Console.WriteLine($"[FATAL] Unhandled exception: {ex?.GetType().Name}: {ex?.Message}"); + Console.WriteLine($"[FATAL] Stack trace: {ex?.StackTrace}"); + if (ex?.InnerException != null) + { + Console.WriteLine($"[FATAL] Inner exception: {ex.InnerException.GetType().Name}: {ex.InnerException.Message}"); + Console.WriteLine($"[FATAL] Inner stack trace: {ex.InnerException.StackTrace}"); + } + }; + + TaskScheduler.UnobservedTaskException += (sender, e) => + { + Console.WriteLine($"[FATAL] Unobserved task exception: {e.Exception?.GetType().Name}: {e.Exception?.Message}"); + Console.WriteLine($"[FATAL] Stack trace: {e.Exception?.StackTrace}"); + e.SetObserved(); // Prevent crash + }; + + Console.WriteLine($"[Program] Starting TodoApp at {DateTime.Now}"); + Console.WriteLine($"[Program] Log file: {logPath}"); + + try + { + // Create the MAUI app with all handlers registered + var app = MauiProgram.CreateMauiApp(); + + // Run on Linux platform + LinuxApplication.Run(app, args); + } + catch (Exception ex) + { + Console.WriteLine($"[FATAL] Exception in Main: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[FATAL] Stack trace: {ex.StackTrace}"); + throw; + } + } +} + +// Helper to write to both console and file +class MultiTextWriter : TextWriter +{ + private readonly TextWriter[] _writers; + public MultiTextWriter(params TextWriter[] writers) => _writers = writers; + public override System.Text.Encoding Encoding => System.Text.Encoding.UTF8; + public override void Write(char value) { foreach (var w in _writers) w.Write(value); } + public override void WriteLine(string? value) { foreach (var w in _writers) w.WriteLine(value); } + public override void Flush() { foreach (var w in _writers) w.Flush(); } +} diff --git a/TodoApp/README.md b/TodoApp/README.md new file mode 100644 index 0000000..67c5f85 --- /dev/null +++ b/TodoApp/README.md @@ -0,0 +1,111 @@ +# TodoApp Sample + +A complete task management application demonstrating OpenMaui Linux capabilities with real-world XAML patterns. + +## Features + +- **NavigationPage** - Full page navigation with back button support +- **CollectionView** - Scrollable list with data binding and selection +- **XAML Data Binding** - Value converters for dynamic styling +- **DisplayAlert Dialogs** - Confirmation dialogs for delete actions +- **Grid Layouts** - Complex layouts with star sizing for expanding content +- **Entry & Editor** - Single and multi-line text input +- **Border with RoundRectangle** - Modern card-style UI +- **ToolbarItems** - Navigation bar actions + +## Screenshots + +The app consists of three pages: + +1. **TodoListPage** - Shows all tasks with completion status indicators +2. **NewTodoPage** - Create a new task with title and notes +3. **TodoDetailPage** - View/edit task details, mark complete, or delete + +## Architecture + +``` +TodoApp/ +├── App.cs # Application entry with NavigationPage +├── Program.cs # Linux platform bootstrap +├── MauiProgram.cs # MAUI app builder +├── TodoItem.cs # Data model +├── TodoService.cs # In-memory data store +├── TodoListPage.xaml(.cs) # Main list view +├── NewTodoPage.xaml(.cs) # Create task page +└── TodoDetailPage.xaml(.cs) # Task detail/edit page +``` + +## XAML Highlights + +### Value Converters +The app uses custom converters for dynamic styling based on completion status: +- `CompletedToColorConverter` - Gray text for completed items +- `CompletedToTextDecorationsConverter` - Strikethrough for completed items +- `CompletedToOpacityConverter` - Fade completed items +- `AlternatingRowColorConverter` - Alternating background colors + +### ResourceDictionary +```xml + + #5C6BC0 + #26A69A + #212121 + +``` + +### CollectionView with DataTemplate +```xml + + + + + + + + +``` + +### Grid with Star Rows (Expanding Editor) +```xml + + + + + + + +``` + +## Building and Running + +```bash +# From the maui-linux-push directory +cd samples/TodoApp +dotnet publish -c Release -r linux-arm64 + +# Run on Linux +./bin/Release/net9.0/linux-arm64/publish/TodoApp +``` + +## Controls Demonstrated + +| Control | Usage | +|---------|-------| +| NavigationPage | App navigation container | +| ContentPage | Individual screens | +| CollectionView | Task list with selection | +| Grid | Page layouts | +| VerticalStackLayout | Vertical grouping | +| HorizontalStackLayout | Horizontal grouping | +| Label | Text display | +| Entry | Single-line input | +| Editor | Multi-line input | +| Button | Toolbar actions | +| Border | Card styling with rounded corners | +| CheckBox | Completion toggle | +| BoxView | Visual separators | + +## License + +MIT License - See repository root for details. diff --git a/TodoApp/TodoApp.csproj b/TodoApp/TodoApp.csproj new file mode 100644 index 0000000..2ffc793 --- /dev/null +++ b/TodoApp/TodoApp.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + true + + + + + + + diff --git a/TodoApp/TodoDetailPage.xaml b/TodoApp/TodoDetailPage.xaml new file mode 100644 index 0000000..ea8037a --- /dev/null +++ b/TodoApp/TodoDetailPage.xaml @@ -0,0 +1,106 @@ + + + + + #5C6BC0 + #26A69A + #EF5350 + #212121 + #757575 + #FFFFFF + #E8EAF6 + + + + + + + + + + + + + + + + diff --git a/TodoApp/TodoDetailPage.xaml.cs b/TodoApp/TodoDetailPage.xaml.cs new file mode 100644 index 0000000..d0d1df5 --- /dev/null +++ b/TodoApp/TodoDetailPage.xaml.cs @@ -0,0 +1,91 @@ +// TodoDetailPage - View and edit a todo item + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Platform; + +namespace TodoApp; + +public partial class TodoDetailPage : ContentPage +{ + private readonly TodoItem _todo; + private readonly TodoService _service = TodoService.Instance; + + // Colors for status label + private static readonly Color AccentColor = Color.FromArgb("#26A69A"); + private static readonly Color TextPrimary = Color.FromArgb("#212121"); + + public TodoDetailPage(TodoItem todo) + { + try + { + Console.WriteLine($"[TodoDetailPage] Constructor starting for: {todo.Title}"); + InitializeComponent(); + Console.WriteLine($"[TodoDetailPage] InitializeComponent complete"); + + _todo = todo; + + // Populate fields + Console.WriteLine($"[TodoDetailPage] Setting TitleEntry.Text"); + TitleEntry.Text = _todo.Title; + Console.WriteLine($"[TodoDetailPage] Setting NotesEditor.Text"); + NotesEditor.Text = _todo.Notes; + Console.WriteLine($"[TodoDetailPage] Setting CompletedCheckBox.IsChecked"); + CompletedCheckBox.IsChecked = _todo.IsCompleted; + Console.WriteLine($"[TodoDetailPage] Calling UpdateStatusLabel"); + UpdateStatusLabel(_todo.IsCompleted); + Console.WriteLine($"[TodoDetailPage] Setting CreatedLabel.Text"); + CreatedLabel.Text = $"Created {_todo.CreatedAt:MMMM d, yyyy} at {_todo.CreatedAt:h:mm tt}"; + Console.WriteLine($"[TodoDetailPage] Constructor complete"); + } + catch (Exception ex) + { + Console.WriteLine($"[TodoDetailPage] EXCEPTION in constructor: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[TodoDetailPage] Stack trace: {ex.StackTrace}"); + throw; + } + } + + private void OnCompletedChanged(object? sender, Microsoft.Maui.Controls.CheckedChangedEventArgs e) + { + Console.WriteLine($"[TodoDetailPage] OnCompletedChanged: {e.Value}"); + UpdateStatusLabel(e.Value); + } + + private void UpdateStatusLabel(bool isCompleted) + { + if (StatusLabel == null) + { + Console.WriteLine($"[TodoDetailPage] UpdateStatusLabel: StatusLabel is null, skipping"); + return; + } + Console.WriteLine($"[TodoDetailPage] UpdateStatusLabel: setting to {(isCompleted ? "Completed" : "In Progress")}"); + StatusLabel.Text = isCompleted ? "Completed" : "In Progress"; + StatusLabel.TextColor = isCompleted ? AccentColor : TextPrimary; + } + + private async void OnSaveClicked(object? sender, EventArgs e) + { + _todo.Title = TitleEntry.Text ?? ""; + _todo.Notes = NotesEditor.Text ?? ""; + _todo.IsCompleted = CompletedCheckBox.IsChecked; + + await Navigation.PopAsync(); + } + + private async void OnDeleteClicked(object? sender, EventArgs e) + { + // Show confirmation dialog + var confirmed = await LinuxDialogService.ShowAlertAsync( + "Delete Task", + $"Are you sure you want to delete \"{_todo.Title}\"? This action cannot be undone.", + "Delete", + "Cancel"); + + if (confirmed) + { + _service.DeleteTodo(_todo); + await Navigation.PopAsync(); + } + } +} diff --git a/TodoApp/TodoItem.cs b/TodoApp/TodoItem.cs new file mode 100644 index 0000000..e7ab47c --- /dev/null +++ b/TodoApp/TodoItem.cs @@ -0,0 +1,81 @@ +// TodoItem - Data model for a todo item + +using System.ComponentModel; + +namespace TodoApp; + +public class TodoItem : INotifyPropertyChanged +{ + private string _title = ""; + private string _notes = ""; + private bool _isCompleted; + private DateTime _dueDate; + + public int Id { get; set; } + + /// + /// Index in the collection for alternating row colors. + /// + public int Index { get; set; } + + public string Title + { + get => _title; + set + { + if (_title != value) + { + _title = value; + OnPropertyChanged(nameof(Title)); + } + } + } + + public string Notes + { + get => _notes; + set + { + if (_notes != value) + { + _notes = value; + OnPropertyChanged(nameof(Notes)); + } + } + } + + public bool IsCompleted + { + get => _isCompleted; + set + { + if (_isCompleted != value) + { + _isCompleted = value; + OnPropertyChanged(nameof(IsCompleted)); + } + } + } + + public DateTime DueDate + { + get => _dueDate; + set + { + if (_dueDate != value) + { + _dueDate = value; + OnPropertyChanged(nameof(DueDate)); + } + } + } + + public DateTime CreatedAt { get; set; } = DateTime.Now; + + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} diff --git a/TodoApp/TodoListPage.xaml b/TodoApp/TodoListPage.xaml new file mode 100644 index 0000000..9299260 --- /dev/null +++ b/TodoApp/TodoListPage.xaml @@ -0,0 +1,129 @@ + + + + + + + #5C6BC0 + #3949AB + #26A69A + #212121 + #757575 + #FFFFFF + #E0E0E0 + #9E9E9E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TodoApp/TodoListPage.xaml.cs b/TodoApp/TodoListPage.xaml.cs new file mode 100644 index 0000000..204541b --- /dev/null +++ b/TodoApp/TodoListPage.xaml.cs @@ -0,0 +1,174 @@ +// TodoListPage - Main page for viewing todos with XAML support + +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using System.Globalization; + +namespace TodoApp; + +public partial class TodoListPage : ContentPage +{ + private readonly TodoService _service = TodoService.Instance; + + public TodoListPage() + { + Console.WriteLine("[TodoListPage] Constructor starting"); + InitializeComponent(); + + TodoCollectionView.ItemsSource = _service.Todos; + UpdateStats(); + + Console.WriteLine("[TodoListPage] Constructor finished"); + } + + protected override void OnAppearing() + { + Console.WriteLine("[TodoListPage] OnAppearing called - refreshing CollectionView"); + base.OnAppearing(); + + // Refresh indexes for alternating row colors + _service.RefreshIndexes(); + + // Refresh the collection view + TodoCollectionView.ItemsSource = null; + TodoCollectionView.ItemsSource = _service.Todos; + Console.WriteLine($"[TodoListPage] ItemsSource set with {_service.Todos.Count} items"); + UpdateStats(); + } + + private async void OnAddClicked(object sender, EventArgs e) + { + await Navigation.PushAsync(new NewTodoPage()); + } + + private async void OnSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + try + { + Console.WriteLine($"[TodoListPage] OnSelectionChanged: {e.CurrentSelection.Count} items selected"); + if (e.CurrentSelection.FirstOrDefault() is TodoItem todo) + { + Console.WriteLine($"[TodoListPage] Navigating to TodoDetailPage for: {todo.Title}"); + TodoCollectionView.SelectedItem = null; // Deselect + var detailPage = new TodoDetailPage(todo); + Console.WriteLine($"[TodoListPage] Created TodoDetailPage, pushing..."); + await Navigation.PushAsync(detailPage); + Console.WriteLine($"[TodoListPage] Navigation complete"); + } + } + catch (Exception ex) + { + Console.WriteLine($"[TodoListPage] EXCEPTION in OnSelectionChanged: {ex.GetType().Name}: {ex.Message}"); + Console.WriteLine($"[TodoListPage] Stack trace: {ex.StackTrace}"); + } + } + + private void UpdateStats() + { + var completed = _service.CompletedCount; + var total = _service.TotalCount; + + if (total == 0) + { + StatsLabel.Text = ""; + } + else + { + StatsLabel.Text = $"{completed} of {total} completed"; + } + } +} + +/// +/// Converter for alternating row background colors. +/// +public class AlternatingRowColorConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int index) + { + return index % 2 == 0 ? Colors.White : Color.FromArgb("#F5F5F5"); + } + return Colors.White; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +/// +/// Converter for completed task text color and indicator color. +/// +public class CompletedToColorConverter : IValueConverter +{ + // Define colors + private static readonly Color PrimaryColor = Color.FromArgb("#5C6BC0"); + private static readonly Color AccentColor = Color.FromArgb("#26A69A"); + private static readonly Color CompletedColor = Color.FromArgb("#9E9E9E"); + private static readonly Color TextPrimary = Color.FromArgb("#212121"); + private static readonly Color TextSecondary = Color.FromArgb("#757575"); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + bool isCompleted = value is bool b && b; + string param = parameter as string ?? ""; + + // Indicator bar color + if (param == "indicator") + { + return isCompleted ? CompletedColor : AccentColor; + } + + // Text colors + if (isCompleted) + { + return CompletedColor; + } + else + { + return param == "notes" ? TextSecondary : TextPrimary; + } + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +/// +/// Converter for completed task text decorations (strikethrough). +/// +public class CompletedToTextDecorationsConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + bool isCompleted = value is bool b && b; + return isCompleted ? TextDecorations.Strikethrough : TextDecorations.None; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} + +/// +/// Converter for completed task opacity (slightly faded when complete). +/// +public class CompletedToOpacityConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + bool isCompleted = value is bool b && b; + return isCompleted ? 0.7 : 1.0; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} diff --git a/TodoApp/TodoService.cs b/TodoApp/TodoService.cs new file mode 100644 index 0000000..f03844b --- /dev/null +++ b/TodoApp/TodoService.cs @@ -0,0 +1,61 @@ +// TodoService - Manages todo items + +using System.Collections.ObjectModel; + +namespace TodoApp; + +public class TodoService +{ + private static TodoService? _instance; + public static TodoService Instance => _instance ??= new TodoService(); + + private int _nextId = 1; + + public ObservableCollection Todos { get; } = new(); + + private TodoService() + { + // Add sample todos with varying lengths to test MaxLines=2 with ellipsis + AddTodo("Learn OpenMaui Linux", "Explore the SkiaSharp-based rendering engine for .NET MAUI on Linux desktop. This is a very long description that should wrap to multiple lines and demonstrate the ellipsis truncation feature when MaxLines is set to 2."); + AddTodo("Build amazing apps", "Create cross-platform applications that run on Windows, macOS, iOS, Android, and Linux! With OpenMaui, you can write once and deploy everywhere."); + AddTodo("Share with the community", "Contribute to the open-source project and help others build great Linux apps. Join our growing community of developers who are passionate about bringing .NET MAUI to Linux."); + } + + public TodoItem AddTodo(string title, string notes = "") + { + var todo = new TodoItem + { + Id = _nextId++, + Index = Todos.Count, // Set index for alternating row colors + Title = title, + Notes = notes, + DueDate = DateTime.Today.AddDays(7) + }; + Todos.Add(todo); + return todo; + } + + /// + /// Refreshes the Index property on all items for alternating row colors. + /// + public void RefreshIndexes() + { + for (int i = 0; i < Todos.Count; i++) + { + Todos[i].Index = i; + } + } + + public TodoItem? GetTodo(int id) + { + return Todos.FirstOrDefault(t => t.Id == id); + } + + public void DeleteTodo(TodoItem todo) + { + Todos.Remove(todo); + } + + public int CompletedCount => Todos.Count(t => t.IsCompleted); + public int TotalCount => Todos.Count; +} diff --git a/docs/images/.gitkeep b/docs/images/.gitkeep new file mode 100644 index 0000000..e69de29