diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..b775de0
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,18 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(dotnet build:*)",
+ "Bash(dotnet test:*)",
+ "Bash(dotnet clean:*)",
+ "Bash(Remove-Item -Recurse -Force CodeSoupCafe.Mauiobj,CodeSoupCafe.Mauibin -ErrorAction SilentlyContinue)",
+ "Bash(Remove-Item -Recurse -Force obj,bin -ErrorAction SilentlyContinue)",
+ "Bash(dir:*)",
+ "Bash(dotnet pack:*)",
+ "Bash(dotnet restore:*)",
+ "Bash(dotnet sln:*)",
+ "Bash(findstr:*)",
+ "Bash(Get-ChildItem -Recurse -Filter \"*carousel*\")",
+ "Bash(Select-Object FullName)"
+ ]
+ }
+}
diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml
index fac0a1a..166f7ed 100644
--- a/.github/workflows/dotnet-desktop.yml
+++ b/.github/workflows/dotnet-desktop.yml
@@ -15,21 +15,32 @@ jobs:
runs-on: windows-latest # Or macos-latest for iOS/Mac Catalyst builds
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v4
- - name: Setup .NET SDK
- uses: actions/setup-dotnet@v4
- with:
- dotnet-version: '10.0.x'
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "10.0.x"
- - name: Install MAUI Workloads
- run: dotnet workload install maui
+ - name: Install MAUI Workloads
+ run: dotnet workload install maui
- - name: Restore NuGet packages
- run: dotnet restore LunaDraw.csproj
+ - name: Cache NuGet packages
+ uses: actions/cache@v4
+ with:
+ path: ~/.nuget/packages
+ key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json', '**/*.csproj') }}
+ restore-keys: |
+ ${{ runner.os }}-nuget-
- - name: Build MAUI App (Windows)
- run: dotnet build LunaDraw.csproj -c Release -f net10.0-windows10.0.19041.0
+ # - name: Remove Local Source for CI
+ # run: dotnet nuget remove source LocalNuGetPackages --configfile nuget.config
- - name: Run Unit Tests
- run: dotnet test tests/LunaDraw.Tests/LunaDraw.Tests.csproj
+ - name: Restore NuGet packages
+ run: dotnet restore LunaDraw.csproj --configfile nuget.config
+
+ - name: Build MAUI App (Windows)
+ run: dotnet build LunaDraw.csproj -c Release -f net10.0-windows10.0.19041.0
+
+ - name: Run Unit Tests
+ run: dotnet test tests/LunaDraw.Tests/LunaDraw.Tests.csproj
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..787b44f
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,7 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": []
+}
\ No newline at end of file
diff --git a/App.xaml b/App.xaml
index fc81ac9..9e45f6d 100644
--- a/App.xaml
+++ b/App.xaml
@@ -12,6 +12,9 @@
+
+
+
diff --git a/App.xaml.cs b/App.xaml.cs
index e597ea3..fc4f826 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -21,17 +21,27 @@
*
*/
+using LunaDraw.Logic.Utils;
+
namespace LunaDraw;
public partial class App : Application
{
- public App()
- {
- InitializeComponent();
- }
+ public App(IPreferencesFacade preferencesFacade)
+ {
+ InitializeComponent();
+
+ var theme = preferencesFacade.Get(AppPreference.AppTheme);
+ UserAppTheme = theme switch
+ {
+ "Light" => AppTheme.Light,
+ "Dark" => AppTheme.Dark,
+ _ => AppTheme.Unspecified
+ };
+ }
- protected override Window CreateWindow(IActivationState? activationState)
- {
- return new Window(new AppShell());
- }
+ protected override Window CreateWindow(IActivationState? activationState)
+ {
+ return new Window(new AppShell());
+ }
}
\ No newline at end of file
diff --git a/AppShell.xaml b/AppShell.xaml
index e290a30..e973e1a 100644
--- a/AppShell.xaml
+++ b/AppShell.xaml
@@ -4,7 +4,8 @@
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:LunaDraw.Pages"
- Title="LunaDraw">
+ Title="LunaDraw"
+ Shell.BackgroundColor="Transparent">
`) for efficient spatial queries and rendering
+ - No bitmap tiling - renders directly from vector elements
+ - Auto-assigns ZIndex to new elements to maintain draw order
+ - Supports masking modes, visibility, locking
+
+- **ILayerFacade**: Abstraction for layer management operations
+ - Manages `ObservableCollection` and current layer state
+ - Integrates with `HistoryMemento` for undo/redo
+ - Methods: `AddLayer()`, `RemoveLayer()`, `MoveLayer()`, `MoveElementsToLayer()`, `SaveState()`
+
+#### Tool System
+
+- **IDrawingTool**: Interface for all drawing/editing tools
+- Tool implementations: `FreehandTool`, `EraserTool`, `EraserBrushTool`, `FillTool`, `SelectTool`, `LineTool`, `RectangleTool`, `EllipseTool`, `ShapeTool`
+- Tools receive `ToolContext` with canvas state, navigation, layers, and brush settings
+- Input handling delegated to `CanvasInputHandler` which dispatches to active tool
+
+#### View & Viewport Management
+
+- **NavigationModel**: Manages pan/zoom transformations via `ViewMatrix` (SKMatrix)
+- **CanvasInputHandler**: Central touch/mouse input processor
+ - Handles multi-touch gestures (pan, zoom, rotate) on canvas and selection
+ - Delegates single-touch drawing to active tool
+ - Right-click switches to Select tool
+ - Applies smoothing to gestures for fluid interaction
+- **MainPage**: Primary page hosting `SKCanvasView` for rendering
+ - Subscribes to `CanvasInvalidateMessage` to trigger redraws
+ - Manages context menus and flyout panels (brushes, shapes, settings)
+
+#### State Management & History
+
+- **HistoryMemento**: Undo/redo stack for layer states
+- **ClipboardMemento**: Copy/paste buffer for drawable elements
+- **DrawingStorageMomento**: Serialization/deserialization of drawings to file
+- **QuadTreeMemento**: Generic spatial partitioning for efficient hit-testing and culling
+
+## Code Quality & Testing Standards
+
+### Testing with xUnit, Moq
+
+- **Test Format**: Arrange-Act-Assert (AAA)
+ - If no `// Arrange` needed, start with `// Act`
+- **Naming**: `Should_When_Returns` format
+ - Example: `Should_Set_Sliding_Issued_At_Time_When_Valid_Credentials_Expired_Or_Invalid_Returns_Logout`
+- **Test Instances**: Use class name for instance, NOT 'sut' or arbitrary names
+ - Mocks: `mockClassName` (e.g., `mockLayerFacade`)
+- **Assertions**: One assertion per line
+- **Test Types**: Prefer `[Theory]`, `[InlineData]`, `[MemberData]` over multiple `[Fact]` methods. Include negative test cases
+
+### Bug Fixing Workflow
+
+1. **Write test first** to validate the bug exists
+2. Implement the fix
+3. Run test to confirm bug elimination
+
+### Code Style Rules (from .clinerules)
+
+- **NO underscores** in names
+- **NO regions**
+- **NO abbreviations** in variable names or otherwise (use full descriptive names)
+- **NO legacy or duplicate code** - refactor to clean state, remove obsolete code
+- **Static extensions**: Use ONLY for reusable logic (see `Logic/Extensions/`)
+
+### SOLID & Design Principles
+
+Ensure adherence to:
+
+- Single Responsibility Principle (SRP)
+- Open/Closed Principle (OCP)
+- Liskov Substitution Principle (LSP)
+- Interface Segregation Principle (ISP)
+- Dependency Inversion Principle (DIP)
+- DRY (Don't Repeat Yourself)
+- Low Coupling / High Cohesion
+- Separation of Concerns & Modularity
+
+## Project Structure
+
+```
+LunaDraw/
+├── Components/ # Reusable UI components and controls
+│ ├── Carousel/ # Gallery carousel implementation
+│ ├── *FlyoutPanel.xaml # Brush, shape, settings panels
+│ └── *.cs # Custom controls (BrushPreview, ShapePreview, etc.)
+├── Converters/ # XAML value converters
+├── Documentation/ # Architecture, features, missing features
+├── Logic/ # Core business logic (non-UI)
+│ ├── Constants/ # App-wide constants
+│ ├── Extensions/ # Static extension methods (SkiaSharp, Preferences)
+│ ├── Handlers/ # Input handling (CanvasInputHandler)
+│ ├── Messages/ # MessageBus message types
+│ ├── Models/ # Domain models (IDrawableElement, Layer, ToolContext, etc.)
+│ ├── Tools/ # IDrawingTool implementations
+│ ├── Utils/ # Utilities (LayerFacade, Mementos, BitmapCache, etc.)
+│ └── ViewModels/ # ReactiveUI ViewModels
+├── Pages/ # MAUI pages (MainPage)
+├── Platforms/ # Platform-specific code
+├── Resources/ # Images, fonts, splash, raw assets
+├── tests/ # Unit tests
+│ └── LunaDraw.Tests/
+└── MauiProgram.cs # DI registration and app configuration
+```
+
+## Important Technical Notes
+
+### SkiaSharp Rendering
+
+- All graphics rendered via SkiaSharp (`SKCanvas`, `SKPaint`, `SKPath`)
+- `MainPage.OnCanvasViewPaintSurface`: Main rendering loop
+ - Applies `NavigationModel.ViewMatrix` for pan/zoom
+ - Iterates layers, uses QuadTree to cull off-screen elements
+ - Elements sorted by ZIndex before drawing
+
+### Brush Effects
+
+- 24+ brush effects with custom shaders and blending modes
+- Examples: Glow/Neon (additive blending, bloom), Star Sparkles, Rainbow, Fireworks, Crayon, Spray, Ribbon
+- Brush settings stored in `ToolbarViewModel` and passed via `ToolContext`
+
+### Movie Mode (Time-Lapse)
+
+- Records drawing process in background
+- Playback animates creation of drawing
+
+### Child-Friendly UX Requirements
+
+- Large touch targets (min 2cm x 2cm)
+- Multi-sensory feedback (sounds, animations)
+- Icon-driven, minimal text
+- Visual/audio guidance over explicit instructions
+
+## Legacy & Migration Notes
+
+- Canvas functionality migrated from `\Legacy\SurfaceBurnCalc` (previous working app)
+- Current branch `reactive-carousel-v2` is refactoring carousel infrastructure from SBC to MAUI
+- Code is fragile in places due to AI generation - test thoroughly
+
+## Additional Resources
+
+- **README.md**: Project overview, features, screenshots, setup
+- **Documentation/ArchitectureDesign.md**: Detailed architecture and design requirements
+- **Documentation/Features.md**: Feature specifications
+- **Documentation/MissingFeatures.md**: Pending features and known issues
+- **.clinerules/**: Coding standards and SPARC methodology guidelines
diff --git a/Components/AdvancedSettingsPopup.xaml b/Components/AdvancedSettingsPopup.xaml
new file mode 100644
index 0000000..9dda415
--- /dev/null
+++ b/Components/AdvancedSettingsPopup.xaml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Components/AdvancedSettingsPopup.xaml.cs b/Components/AdvancedSettingsPopup.xaml.cs
new file mode 100644
index 0000000..72bbac5
--- /dev/null
+++ b/Components/AdvancedSettingsPopup.xaml.cs
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2025 CodeSoupCafe LLC
+ *
+ * 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.
+ *
+ */
+
+using CommunityToolkit.Maui.Views;
+using LunaDraw.Logic.ViewModels;
+
+namespace LunaDraw.Components;
+
+public partial class AdvancedSettingsPopup : Popup
+{
+ public AdvancedSettingsPopup(MainViewModel viewModel)
+ {
+ InitializeComponent();
+ BindingContext = viewModel;
+ }
+
+ private void OnCloseClicked(object sender, EventArgs e)
+ {
+ this.CloseAsync();
+ }
+}
diff --git a/Components/BrushPreviewControl.cs b/Components/BrushPreviewControl.cs
index 50b8816..fc02806 100644
--- a/Components/BrushPreviewControl.cs
+++ b/Components/BrushPreviewControl.cs
@@ -27,136 +27,135 @@
using SkiaSharp.Views.Maui;
using SkiaSharp.Views.Maui.Controls;
-namespace LunaDraw.Components
+namespace LunaDraw.Components;
+
+public class BrushPreviewControl : SKCanvasView
{
- public class BrushPreviewControl : SKCanvasView
- {
- public static readonly BindableProperty BrushShapeProperty =
- BindableProperty.Create(nameof(BrushShape), typeof(BrushShape), typeof(BrushPreviewControl), null, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty BrushShapeProperty =
+ BindableProperty.Create(nameof(BrushShape), typeof(BrushShape), typeof(BrushPreviewControl), null, propertyChanged: OnPropertyChanged);
- public BrushShape BrushShape
- {
- get => (BrushShape)GetValue(BrushShapeProperty);
- set => SetValue(BrushShapeProperty, value);
- }
+ public BrushShape BrushShape
+ {
+ get => (BrushShape)GetValue(BrushShapeProperty);
+ set => SetValue(BrushShapeProperty, value);
+ }
- public static readonly BindableProperty ColorProperty =
- BindableProperty.Create(nameof(Color), typeof(Color), typeof(BrushPreviewControl), Colors.Black, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty ColorProperty =
+ BindableProperty.Create(nameof(Color), typeof(Color), typeof(BrushPreviewControl), Colors.Black, propertyChanged: OnPropertyChanged);
- public Color Color
- {
- get => (Color)GetValue(ColorProperty);
- set => SetValue(ColorProperty, value);
- }
+ public Color Color
+ {
+ get => (Color)GetValue(ColorProperty);
+ set => SetValue(ColorProperty, value);
+ }
- public static readonly BindableProperty StrokeColorProperty =
- BindableProperty.Create(nameof(StrokeColor), typeof(SKColor), typeof(BrushPreviewControl), SKColors.Empty, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty StrokeColorProperty =
+ BindableProperty.Create(nameof(StrokeColor), typeof(SKColor), typeof(BrushPreviewControl), SKColors.Empty, propertyChanged: OnPropertyChanged);
- public SKColor StrokeColor
- {
- get => (SKColor)GetValue(StrokeColorProperty);
- set => SetValue(StrokeColorProperty, value);
- }
+ public SKColor StrokeColor
+ {
+ get => (SKColor)GetValue(StrokeColorProperty);
+ set => SetValue(StrokeColorProperty, value);
+ }
- public static readonly BindableProperty FillColorProperty =
- BindableProperty.Create(nameof(FillColor), typeof(SKColor?), typeof(BrushPreviewControl), null, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty FillColorProperty =
+ BindableProperty.Create(nameof(FillColor), typeof(SKColor?), typeof(BrushPreviewControl), null, propertyChanged: OnPropertyChanged);
- public SKColor? FillColor
- {
- get => (SKColor?)GetValue(FillColorProperty);
- set => SetValue(FillColorProperty, value);
- }
+ public SKColor? FillColor
+ {
+ get => (SKColor?)GetValue(FillColorProperty);
+ set => SetValue(FillColorProperty, value);
+ }
- public static readonly BindableProperty IsGlowEnabledProperty =
- BindableProperty.Create(nameof(IsGlowEnabled), typeof(bool), typeof(BrushPreviewControl), false, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty IsGlowEnabledProperty =
+ BindableProperty.Create(nameof(IsGlowEnabled), typeof(bool), typeof(BrushPreviewControl), false, propertyChanged: OnPropertyChanged);
- public bool IsGlowEnabled
- {
- get => (bool)GetValue(IsGlowEnabledProperty);
- set => SetValue(IsGlowEnabledProperty, value);
- }
+ public bool IsGlowEnabled
+ {
+ get => (bool)GetValue(IsGlowEnabledProperty);
+ set => SetValue(IsGlowEnabledProperty, value);
+ }
- public static readonly BindableProperty GlowColorProperty =
- BindableProperty.Create(nameof(GlowColor), typeof(SKColor), typeof(BrushPreviewControl), SKColors.Yellow, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty GlowColorProperty =
+ BindableProperty.Create(nameof(GlowColor), typeof(SKColor), typeof(BrushPreviewControl), SKColors.Yellow, propertyChanged: OnPropertyChanged);
- public SKColor GlowColor
- {
- get => (SKColor)GetValue(GlowColorProperty);
- set => SetValue(GlowColorProperty, value);
- }
+ public SKColor GlowColor
+ {
+ get => (SKColor)GetValue(GlowColorProperty);
+ set => SetValue(GlowColorProperty, value);
+ }
- public static readonly BindableProperty GlowRadiusProperty =
- BindableProperty.Create(nameof(GlowRadius), typeof(float), typeof(BrushPreviewControl), 10f, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty GlowRadiusProperty =
+ BindableProperty.Create(nameof(GlowRadius), typeof(float), typeof(BrushPreviewControl), 10f, propertyChanged: OnPropertyChanged);
- public float GlowRadius
- {
- get => (float)GetValue(GlowRadiusProperty);
- set => SetValue(GlowRadiusProperty, value);
- }
+ public float GlowRadius
+ {
+ get => (float)GetValue(GlowRadiusProperty);
+ set => SetValue(GlowRadiusProperty, value);
+ }
- private static void OnPropertyChanged(BindableObject bindable, object oldValue, object newValue)
- {
- ((BrushPreviewControl)bindable).InvalidateSurface();
- }
+ private static void OnPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ ((BrushPreviewControl)bindable).InvalidateSurface();
+ }
- protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
- {
- base.OnPaintSurface(e);
+ protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
+ {
+ base.OnPaintSurface(e);
- var canvas = e.Surface.Canvas;
- canvas.Clear();
+ var canvas = e.Surface.Canvas;
+ canvas.Clear();
- if (BrushShape?.Path == null) return;
+ if (BrushShape?.Path == null) return;
- var info = e.Info;
- var center = new SKPoint(info.Width / 2, info.Height / 2);
+ var info = e.Info;
+ var center = new SKPoint(info.Width / 2, info.Height / 2);
- // Calculate scale to fit in the view (with padding)
- var bounds = BrushShape.Path.TightBounds;
- float maxDim = Math.Max(bounds.Width, bounds.Height);
- if (maxDim <= 0) maxDim = 1;
+ // Calculate scale to fit in the view (with padding)
+ var bounds = BrushShape.Path.TightBounds;
+ float maxDim = Math.Max(bounds.Width, bounds.Height);
+ if (maxDim <= 0) maxDim = 1;
- float availableSize = Math.Min(info.Width, info.Height) * 0.6f; // 60% padding
- float scale = availableSize / maxDim;
+ float availableSize = Math.Min(info.Width, info.Height) * 0.6f; // 60% padding
+ float scale = availableSize / maxDim;
- SKColor drawColor;
- if (StrokeColor != SKColors.Empty)
- {
- drawColor = StrokeColor;
- }
- else
- {
- drawColor = new SKColor((byte)(Color.Red * 255), (byte)(Color.Green * 255), (byte)(Color.Blue * 255), (byte)(Color.Alpha * 255));
- }
+ SKColor drawColor;
+ if (StrokeColor != SKColors.Empty)
+ {
+ drawColor = StrokeColor;
+ }
+ else
+ {
+ drawColor = new SKColor((byte)(Color.Red * 255), (byte)(Color.Green * 255), (byte)(Color.Blue * 255), (byte)(Color.Alpha * 255));
+ }
- canvas.Save();
- canvas.Translate(center.X, center.Y);
- canvas.Scale(scale);
- // Center the shape itself (if not centered at 0,0)
- canvas.Translate(-bounds.MidX, -bounds.MidY);
+ canvas.Save();
+ canvas.Translate(center.X, center.Y);
+ canvas.Scale(scale);
+ // Center the shape itself (if not centered at 0,0)
+ canvas.Translate(-bounds.MidX, -bounds.MidY);
- // Draw Glow if enabled
- if (IsGlowEnabled)
- {
- using var glowPaint = new SKPaint
- {
- Style = SKPaintStyle.StrokeAndFill,
- Color = GlowColor,
- IsAntialias = true,
- MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, GlowRadius)
- };
- canvas.DrawPath(BrushShape.Path, glowPaint);
- }
-
- using var paint = new SKPaint
+ // Draw Glow if enabled
+ if (IsGlowEnabled)
+ {
+ using var glowPaint = new SKPaint
{
- Style = SKPaintStyle.Fill,
- Color = drawColor,
- IsAntialias = true
+ Style = SKPaintStyle.StrokeAndFill,
+ Color = GlowColor,
+ IsAntialias = true,
+ MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, GlowRadius)
};
-
- canvas.DrawPath(BrushShape.Path, paint);
- canvas.Restore();
+ canvas.DrawPath(BrushShape.Path, glowPaint);
}
+
+ using var paint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = drawColor,
+ IsAntialias = true
+ };
+
+ canvas.DrawPath(BrushShape.Path, paint);
+ canvas.Restore();
}
}
diff --git a/Components/BrushesFlyoutPanel.xaml b/Components/BrushesFlyoutPanel.xaml
index 931c6e0..f146ea1 100644
--- a/Components/BrushesFlyoutPanel.xaml
+++ b/Components/BrushesFlyoutPanel.xaml
@@ -5,11 +5,12 @@
xmlns:models="clr-namespace:LunaDraw.Logic.Models"
xmlns:components="clr-namespace:LunaDraw.Components"
x:Class="LunaDraw.Components.BrushesFlyoutPanel"
- x:DataType="viewModels:ToolbarViewModel">
-
+
@@ -19,9 +20,9 @@
HorizontalTextAlignment="Center"/>
-
+
+ HorizontalItemSpacing="10"/>
-
+
+ VerticalOptions="Fill"/>
+ LineBreakMode="TailTruncation"/>
diff --git a/Components/BrushesFlyoutPanel.xaml.cs b/Components/BrushesFlyoutPanel.xaml.cs
index 3f44b7f..7a9e5c6 100644
--- a/Components/BrushesFlyoutPanel.xaml.cs
+++ b/Components/BrushesFlyoutPanel.xaml.cs
@@ -23,22 +23,23 @@
using LunaDraw.Logic.ViewModels;
-namespace LunaDraw.Components
+namespace LunaDraw.Components;
+
+public partial class BrushesFlyoutPanel : ContentView
{
- public partial class BrushesFlyoutPanel : ContentView
+ public ToolbarViewModel? ViewModel => BindingContext as ToolbarViewModel;
+
+ public BrushesFlyoutPanel()
{
- public BrushesFlyoutPanel()
- {
- InitializeComponent();
- this.Loaded += OnBrushesFlyoutPanelLoaded;
- }
+ InitializeComponent();
+ this.Loaded += OnBrushesFlyoutPanelLoaded;
+ }
- private void OnBrushesFlyoutPanelLoaded(object? sender, EventArgs e)
+ private void OnBrushesFlyoutPanelLoaded(object? sender, EventArgs e)
+ {
+ if (BindingContext is ToolbarViewModel toolbarViewModel)
{
- if (BindingContext is ToolbarViewModel toolbarViewModel)
- {
- // No settings to load here anymore, just brush shapes which are data bound
- }
+ // No settings to load here anymore, just brush shapes which are data bound
}
}
}
diff --git a/Components/DrawingGalleryPopup.xaml b/Components/DrawingGalleryPopup.xaml
new file mode 100644
index 0000000..6a69d6c
--- /dev/null
+++ b/Components/DrawingGalleryPopup.xaml
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Components/DrawingGalleryPopup.xaml.cs b/Components/DrawingGalleryPopup.xaml.cs
new file mode 100644
index 0000000..3c48592
--- /dev/null
+++ b/Components/DrawingGalleryPopup.xaml.cs
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2025 CodeSoupCafe LLC
+ *
+ * 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.
+ *
+ */
+
+using CommunityToolkit.Maui.Views;
+using LunaDraw.Logic.ViewModels;
+
+namespace LunaDraw.Components;
+
+public partial class DrawingGalleryPopup : Popup
+{
+ private readonly DrawingGalleryPopupViewModel viewModel;
+
+ public DrawingGalleryPopup(DrawingGalleryPopupViewModel viewModel)
+ {
+ this.viewModel = viewModel;
+ InitializeComponent();
+ BindingContext = viewModel;
+
+ viewModel.DrawingItems.CollectionChanged += (s, e) =>
+ {
+ GalleryView.ItemsSource = null;
+ GalleryView.ItemsSource = viewModel.DrawingItems;
+ };
+
+ viewModel.RequestClose += OnRequestClose;
+ }
+
+ private void OnDrawingItemTapped(object? sender, EventArgs e)
+ {
+ if (sender is Grid grid && grid.BindingContext is DrawingItemViewModel item)
+ {
+ viewModel.OpenDrawingCommand.Execute(item).Subscribe();
+ }
+ }
+
+ private void OnThumbnailImageLoaded(object? sender, EventArgs e)
+ {
+ }
+
+ private void OnThumbnailImagePropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ }
+
+ private async void OnRequestClose(object? sender, EventArgs e)
+ {
+ await this.CloseAsync();
+ }
+
+ protected override void OnHandlerChanged()
+ {
+ base.OnHandlerChanged();
+
+ // Unsubscribe when handler is removed to prevent memory leaks
+ if (Handler == null && BindingContext is DrawingGalleryPopupViewModel vm)
+ {
+ vm.RequestClose -= OnRequestClose;
+
+ if (vm is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+}
diff --git a/Components/FlyoutPanel.xaml b/Components/FlyoutPanel.xaml
index 272cf77..244fcca 100644
--- a/Components/FlyoutPanel.xaml
+++ b/Components/FlyoutPanel.xaml
@@ -1,11 +1,12 @@
-
+
\ No newline at end of file
diff --git a/Components/FlyoutPanel.xaml.cs b/Components/FlyoutPanel.xaml.cs
index 0566ee2..024311d 100644
--- a/Components/FlyoutPanel.xaml.cs
+++ b/Components/FlyoutPanel.xaml.cs
@@ -23,266 +23,264 @@
using System.Windows.Input;
-namespace LunaDraw.Components
+namespace LunaDraw.Components;
+
+public partial class FlyoutPanel : ContentView
{
- public partial class FlyoutPanel : ContentView
+ private bool isOpen;
+ private View? targetElement;
+
+ public static readonly BindableProperty FlyoutContentProperty =
+ BindableProperty.Create(
+ nameof(FlyoutContent),
+ typeof(View),
+ typeof(FlyoutPanel),
+ default(View),
+ propertyChanged: OnFlyoutContentChanged);
+
+ public static readonly BindableProperty IsOpenProperty =
+ BindableProperty.Create(
+ nameof(IsOpen),
+ typeof(bool),
+ typeof(FlyoutPanel),
+ default(bool),
+ propertyChanged: OnIsOpenChanged);
+
+ public static readonly BindableProperty TargetElementProperty =
+ BindableProperty.Create(
+ nameof(TargetElement),
+ typeof(View),
+ typeof(FlyoutPanel),
+ default(View),
+ propertyChanged: OnTargetElementChanged);
+
+ // Optional: name of a child inside TargetElement to use as the anchor (e.g., a specific button inside the toolbar)
+ public static readonly BindableProperty AnchorNameProperty =
+ BindableProperty.Create(
+ nameof(AnchorName),
+ typeof(string),
+ typeof(FlyoutPanel),
+ default(string));
+
+ public FlyoutPanel()
{
- private bool isOpen;
- private View? targetElement;
-
- public static readonly BindableProperty FlyoutContentProperty =
- BindableProperty.Create(
- nameof(FlyoutContent),
- typeof(View),
- typeof(FlyoutPanel),
- default(View),
- propertyChanged: OnFlyoutContentChanged);
-
- public static readonly BindableProperty IsOpenProperty =
- BindableProperty.Create(
- nameof(IsOpen),
- typeof(bool),
- typeof(FlyoutPanel),
- default(bool),
- propertyChanged: OnIsOpenChanged);
-
- public static readonly BindableProperty TargetElementProperty =
- BindableProperty.Create(
- nameof(TargetElement),
- typeof(View),
- typeof(FlyoutPanel),
- default(View),
- propertyChanged: OnTargetElementChanged);
-
- // Optional: name of a child inside TargetElement to use as the anchor (e.g., a specific button inside the toolbar)
- public static readonly BindableProperty AnchorNameProperty =
- BindableProperty.Create(
- nameof(AnchorName),
- typeof(string),
- typeof(FlyoutPanel),
- default(string));
-
- public FlyoutPanel()
- {
- InitializeComponent();
+ InitializeComponent();
- // Initialize to hidden state
- this.Opacity = 0;
- this.Scale = 0.9;
- AbsoluteLayout.SetLayoutBounds(this, new Rect(-1000, -1000, -1, -1));
- }
+ // Initialize to hidden state
+ this.Opacity = 0;
+ this.Scale = 0.9;
+ AbsoluteLayout.SetLayoutBounds(this, new Rect(-1000, -1000, -1, -1));
+ }
+ public View FlyoutContent
+ {
+ get => (View)GetValue(FlyoutContentProperty);
+ set => SetValue(FlyoutContentProperty, value);
+ }
- public View FlyoutContent
- {
- get => (View)GetValue(FlyoutContentProperty);
- set => SetValue(FlyoutContentProperty, value);
- }
+ public bool IsOpen
+ {
+ get => (bool)GetValue(IsOpenProperty);
+ set => SetValue(IsOpenProperty, value);
+ }
- public bool IsOpen
- {
- get => (bool)GetValue(IsOpenProperty);
- set => SetValue(IsOpenProperty, value);
- }
+ public View TargetElement
+ {
+ get => (View)GetValue(TargetElementProperty);
+ set => SetValue(TargetElementProperty, value);
+ }
- public View TargetElement
- {
- get => (View)GetValue(TargetElementProperty);
- set => SetValue(TargetElementProperty, value);
- }
+ public string AnchorName
+ {
+ get => (string)GetValue(AnchorNameProperty);
+ set => SetValue(AnchorNameProperty, value);
+ }
- public string AnchorName
- {
- get => (string)GetValue(AnchorNameProperty);
- set => SetValue(AnchorNameProperty, value);
- }
+ public ICommand? CloseCommand { get; private set; }
- public ICommand? CloseCommand { get; private set; }
+ private static void OnFlyoutContentChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var panel = (FlyoutPanel)bindable;
+ // Content is handled by the ContentPresenter in XAML
+ }
- private static void OnFlyoutContentChanged(BindableObject bindable, object oldValue, object newValue)
- {
- var panel = (FlyoutPanel)bindable;
- // Content is handled by the ContentPresenter in XAML
- }
+ private static void OnIsOpenChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var panel = (FlyoutPanel)bindable;
+ panel.isOpen = (bool)newValue;
+ panel.UpdateVisibility();
+ }
- private static void OnIsOpenChanged(BindableObject bindable, object oldValue, object newValue)
+ private static void OnTargetElementChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var panel = (FlyoutPanel)bindable;
+ panel.targetElement = (View)newValue;
+ }
+
+ private async void UpdateVisibility()
+ {
+ if (isOpen)
{
- var panel = (FlyoutPanel)bindable;
- panel.isOpen = (bool)newValue;
- panel.UpdateVisibility();
+ await ShowFlyout();
}
-
- private static void OnTargetElementChanged(BindableObject bindable, object oldValue, object newValue)
+ else
{
- var panel = (FlyoutPanel)bindable;
- panel.targetElement = (View)newValue;
+ await HideFlyout();
}
+ }
- private async void UpdateVisibility()
+ private async Task ShowFlyout()
+ {
+ if (targetElement != null)
{
- if (isOpen)
- {
- await ShowFlyout();
- }
- else
- {
- await HideFlyout();
- }
+ await PositionFlyout();
}
- private async Task ShowFlyout()
- {
- if (targetElement != null)
- {
- await PositionFlyout();
- }
+ // Animate in - run fade and scale simultaneously
+ var fadeTask = this.FadeToAsync(1, 200, Easing.CubicOut);
+ var scaleTask = this.ScaleToAsync(1, 200, Easing.CubicOut);
- // Animate in - run fade and scale simultaneously
- var fadeTask = this.FadeToAsync(1, 200, Easing.CubicOut);
- var scaleTask = this.ScaleToAsync(1, 200, Easing.CubicOut);
+ await Task.WhenAll(fadeTask, scaleTask);
+ }
- await Task.WhenAll(fadeTask, scaleTask);
- }
+ private async Task HideFlyout()
+ {
+ // Animate out - run fade and scale simultaneously
+ var fadeTask = this.FadeToAsync(0, 150, Easing.CubicIn);
+ var scaleTask = this.ScaleToAsync(0.9, 150, Easing.CubicIn);
- private async Task HideFlyout()
- {
- // Animate out - run fade and scale simultaneously
- var fadeTask = this.FadeToAsync(0, 150, Easing.CubicIn);
- var scaleTask = this.ScaleToAsync(0.9, 150, Easing.CubicIn);
+ await Task.WhenAll(fadeTask, scaleTask);
- await Task.WhenAll(fadeTask, scaleTask);
+ // Move off-screen when hidden
+ AbsoluteLayout.SetLayoutBounds(this, new Rect(-1000, -1000, -1, -1));
+ }
- // Move off-screen when hidden
- AbsoluteLayout.SetLayoutBounds(this, new Rect(-1000, -1000, -1, -1));
- }
+ public static Rect GetCoordinatesWithinPage(VisualElement element)
+ {
+ double x = 0;
+ double y = 0;
- public static Rect GetCoordinatesWithinPage(VisualElement element)
- {
- double x = 0;
- double y = 0;
+ VisualElement? currentElement = element;
- VisualElement? currentElement = element;
+ // Traverse up the visual tree until a Page or null is encountered
+ while (currentElement != null && !(currentElement is Page))
+ {
+ x += currentElement.X;
+ y += currentElement.Y;
+ currentElement = currentElement.Parent as VisualElement;
- // Traverse up the visual tree until a Page or null is encountered
- while (currentElement != null && !(currentElement is Page))
+ if (currentElement is ScrollView scrollView)
{
- x += currentElement.X;
- y += currentElement.Y;
- currentElement = currentElement.Parent as VisualElement;
-
- if (currentElement is ScrollView scrollView)
+ if (scrollView.Orientation == ScrollOrientation.Both || scrollView.Orientation == ScrollOrientation.Horizontal)
{
- if (scrollView.Orientation == ScrollOrientation.Both || scrollView.Orientation == ScrollOrientation.Horizontal)
- {
- x -= scrollView.ScrollX;
- }
- if (scrollView.Orientation == ScrollOrientation.Both || scrollView.Orientation == ScrollOrientation.Vertical)
- {
- y -= scrollView.ScrollY;
- }
+ x -= scrollView.ScrollX;
+ }
+ if (scrollView.Orientation == ScrollOrientation.Both || scrollView.Orientation == ScrollOrientation.Vertical)
+ {
+ y -= scrollView.ScrollY;
}
}
-
- // If the element is within a Page, add the Page's X and Y
- if (currentElement is Page page)
- {
- x += page.X;
- y += page.Y;
- }
-
- return new Rect(x, y, element.Width, element.Height);
}
- private async Task PositionFlyout()
+ // If the element is within a Page, add the Page's X and Y
+ if (currentElement is Page page)
{
- if (targetElement == null) return;
+ x += page.X;
+ y += page.Y;
+ }
- // Find the actual element to anchor to, if an AnchorName is provided
- View anchorElement = targetElement;
- if (!string.IsNullOrEmpty(AnchorName))
- {
- anchorElement = anchorElement.FindByName(AnchorName) as View ?? targetElement;
- }
+ return new Rect(x, y, element.Width, element.Height);
+ }
- // Compute target bounds relative to the page
- var targetBounds = GetCoordinatesWithinPage(anchorElement); // Changed to use anchorElement
- if (targetBounds == Rect.Zero) return;
+ private async Task PositionFlyout()
+ {
+ if (targetElement == null) return;
- // Position to the right of the target element with a small gap
- var x = targetBounds.Right + 10;
- var y = targetBounds.Top;
+ // Find the actual element to anchor to, if an AnchorName is provided
+ View anchorElement = targetElement;
+ if (!string.IsNullOrEmpty(AnchorName))
+ {
+ anchorElement = anchorElement.FindByName(AnchorName) as View ?? targetElement;
+ }
+
+ // Compute target bounds relative to the page
+ var targetBounds = GetCoordinatesWithinPage(anchorElement); // Changed to use anchorElement
+ if (targetBounds == Rect.Zero) return;
- // Set initial bounds using -1 to indicate AutoSize for width/height
- AbsoluteLayout.SetLayoutBounds(this, new Rect(x, y, -1, -1));
+ // Position to the right of the target element with a small gap
+ var x = targetBounds.Right + 10;
+ var y = targetBounds.Top;
- // Wait one layout cycle so the FlyoutContainer can size itself
- await Task.Yield();
+ // Set initial bounds using -1 to indicate AutoSize for width/height
+ AbsoluteLayout.SetLayoutBounds(this, new Rect(x, y, -1, -1));
- var flyoutBounds = this.Bounds;
+ // Wait one layout cycle so the FlyoutContainer can size itself
+ await Task.Yield();
- // Get parent page dimensions (assume top-level layout fills the page)
- if (!(this.Parent is VisualElement parentPage)) return;
- var screenWidth = parentPage.Width;
- var screenHeight = parentPage.Height;
- var margin = 10.0;
+ var flyoutBounds = this.Bounds;
- double finalX = x;
- double finalY = y;
- double finalWidth = -1;
- double finalHeight = -1;
+ // Get parent page dimensions (assume top-level layout fills the page)
+ if (!(this.Parent is VisualElement parentPage)) return;
+ var screenWidth = parentPage.Width;
+ var screenHeight = parentPage.Height;
+ var margin = 10.0;
- // --- Horizontal Positioning Strategy ---
+ double finalX = x;
+ double finalY = y;
+ double finalWidth = -1;
+ double finalHeight = -1;
- // 1. Try positioning to the right of the target (preferred)
- if (flyoutBounds.Right > screenWidth - margin)
+ // --- Horizontal Positioning Strategy ---
+
+ // 1. Try positioning to the right of the target (preferred)
+ if (flyoutBounds.Right > screenWidth - margin)
+ {
+ // It overflows right. Try positioning to the left of the target.
+ double leftX = targetBounds.Left - flyoutBounds.Width - 10;
+ if (leftX >= margin)
{
- // It overflows right. Try positioning to the left of the target.
- double leftX = targetBounds.Left - flyoutBounds.Width - 10;
- if (leftX >= margin)
- {
- finalX = leftX;
- }
- else
- {
- // Neither side fits perfectly.
- // Position at the left-most valid position or right-most valid position?
- // Let's constrain to the screen width.
- finalX = Math.Max(margin, Math.Min(x, screenWidth - flyoutBounds.Width - margin));
-
- // If the flyout is wider than the screen (minus margins), constrain width.
- if (flyoutBounds.Width > screenWidth - 2 * margin)
- {
- finalX = margin;
- finalWidth = screenWidth - 2 * margin;
- }
- }
+ finalX = leftX;
}
-
- // --- Vertical Positioning Strategy ---
-
- // If the flyout overflows the bottom edge
- if (flyoutBounds.Bottom > screenHeight - margin)
+ else
{
- // Try moving it up
- double diff = flyoutBounds.Bottom - (screenHeight - margin);
- double newY = y - diff;
-
- if (newY >= margin)
- {
- finalY = newY;
- }
- else
- {
- // Moving up hits the top edge. Constrain Height.
- finalY = margin;
- finalHeight = screenHeight - 2 * margin;
- }
+ // Neither side fits perfectly.
+ // Position at the left-most valid position or right-most valid position?
+ // Let's constrain to the screen width.
+ finalX = Math.Max(margin, Math.Min(x, screenWidth - flyoutBounds.Width - margin));
+
+ // If the flyout is wider than the screen (minus margins), constrain width.
+ if (flyoutBounds.Width > screenWidth - 2 * margin)
+ {
+ finalX = margin;
+ finalWidth = screenWidth - 2 * margin;
+ }
}
+ }
+
+ // --- Vertical Positioning Strategy ---
- // Re-apply the adjusted bounds
- // Note: AbsoluteLayout in MAUI handles -1 as AutoSize.
- // If we set a specific width/height, it will respect it.
- AbsoluteLayout.SetLayoutBounds(this, new Rect(finalX, finalY, finalWidth, finalHeight));
+ // If the flyout overflows the bottom edge
+ if (flyoutBounds.Bottom > screenHeight - margin)
+ {
+ // Try moving it up
+ double diff = flyoutBounds.Bottom - (screenHeight - margin);
+ double newY = y - diff;
+
+ if (newY >= margin)
+ {
+ finalY = newY;
+ }
+ else
+ {
+ // Moving up hits the top edge. Constrain Height.
+ finalY = margin;
+ finalHeight = screenHeight - 2 * margin;
+ }
}
+
+ // Re-apply the adjusted bounds
+ // Note: AbsoluteLayout in MAUI handles -1 as AutoSize.
+ // If we set a specific width/height, it will respect it.
+ AbsoluteLayout.SetLayoutBounds(this, new Rect(finalX, finalY, finalWidth, finalHeight));
}
}
\ No newline at end of file
diff --git a/Components/LayerControlView.xaml b/Components/LayerControlView.xaml
index e4809ea..aaf1a7c 100644
--- a/Components/LayerControlView.xaml
+++ b/Components/LayerControlView.xaml
@@ -4,152 +4,225 @@
xmlns:viewModels="clr-namespace:LunaDraw.Logic.ViewModels"
xmlns:models="clr-namespace:LunaDraw.Logic.Models"
xmlns:converters="clr-namespace:LunaDraw.Converters"
+ xmlns:components="clr-namespace:LunaDraw.Components"
+ xmlns:strings="clr-namespace:LunaDraw.Resources.Strings"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
x:Class="LunaDraw.Components.LayerControlView"
x:DataType="viewModels:MainViewModel"
x:Name="Root">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Components/LayerControlView.xaml.cs b/Components/LayerControlView.xaml.cs
index 5663615..79664b6 100644
--- a/Components/LayerControlView.xaml.cs
+++ b/Components/LayerControlView.xaml.cs
@@ -24,68 +24,69 @@
using LunaDraw.Logic.Models;
using LunaDraw.Logic.ViewModels;
-namespace LunaDraw.Components
+namespace LunaDraw.Components;
+
+public partial class LayerControlView : ContentView
{
- public partial class LayerControlView : ContentView
- {
- public static readonly BindableProperty IsLayerPanelExpandedProperty =
- BindableProperty.Create(nameof(IsLayerPanelExpanded), typeof(bool), typeof(LayerControlView), false, propertyChanged: OnIsLayerPanelExpandedChanged);
+ public MainViewModel? ViewModel => BindingContext as MainViewModel;
- public bool IsLayerPanelExpanded
- {
- get => (bool)GetValue(IsLayerPanelExpandedProperty);
- set => SetValue(IsLayerPanelExpandedProperty, value);
- }
+ public static readonly BindableProperty IsLayerPanelExpandedProperty =
+ BindableProperty.Create(nameof(IsLayerPanelExpanded), typeof(bool), typeof(LayerControlView), false, propertyChanged: OnIsLayerPanelExpandedChanged);
- public List MaskingModes { get; } = Enum.GetValues().Cast().ToList();
+ public bool IsLayerPanelExpanded
+ {
+ get => (bool)GetValue(IsLayerPanelExpandedProperty);
+ set => SetValue(IsLayerPanelExpandedProperty, value);
+ }
- public LayerControlView()
- {
- InitializeComponent();
- }
+ public List MaskingModes { get; } = Enum.GetValues().Cast().ToList();
- private static void OnIsLayerPanelExpandedChanged(BindableObject bindable, object oldValue, object newValue)
- {
- var control = (LayerControlView)bindable;
- control.ContentGrid.IsVisible = (bool)newValue;
- control.CollapseButton.Text = (bool)newValue ? "▼" : "▶";
- }
+ public LayerControlView()
+ {
+ InitializeComponent();
+ }
- private void OnCollapseClicked(object sender, EventArgs e)
- {
- IsLayerPanelExpanded = !IsLayerPanelExpanded;
- }
+ private static void OnIsLayerPanelExpandedChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var control = (LayerControlView)bindable;
+ control.ContentGrid.IsVisible = (bool)newValue;
+ control.CollapseButton.Text = (bool)newValue ? "▼" : "▶";
+ }
- private void OnDragStarting(object sender, DragStartingEventArgs e)
- {
- if (sender is Element element && element.BindingContext is Layer layer)
- {
- e.Data.Properties["SourceLayer"] = layer;
- // Ensure the dragged layer is selected
- if (this.BindingContext is MainViewModel viewModel)
- {
- viewModel.CurrentLayer = layer;
- }
- }
- }
+ private void OnCollapseClicked(object sender, EventArgs e)
+ {
+ IsLayerPanelExpanded = !IsLayerPanelExpanded;
+ }
- private void OnDragOver(object sender, DragEventArgs e)
- {
- e.AcceptedOperation = DataPackageOperation.Copy;
- }
+ private void OnDragStarting(object sender, DragStartingEventArgs e)
+ {
+ if (sender is Element element && element.BindingContext is Layer layer)
+ {
+ e.Data.Properties["SourceLayer"] = layer;
+ // Ensure the dragged layer is selected
+ if (this.BindingContext is MainViewModel viewModel)
+ {
+ viewModel.CurrentLayer = layer;
+ }
+ }
+ }
- private void OnDrop(object sender, DropEventArgs e)
+ private void OnDragOver(object sender, DragEventArgs e)
+ {
+ e.AcceptedOperation = DataPackageOperation.Copy;
+ }
+
+ private void OnDrop(object sender, DropEventArgs e)
+ {
+ if (e.Data.Properties.TryGetValue("SourceLayer", out var sourceObj) && sourceObj is Layer sourceLayer)
+ {
+ if (sender is Element element && element.BindingContext is Layer targetLayer)
+ {
+ if (this.BindingContext is MainViewModel viewModel)
{
- if (e.Data.Properties.TryGetValue("SourceLayer", out var sourceObj) && sourceObj is Layer sourceLayer)
- {
- if (sender is Element element && element.BindingContext is Layer targetLayer)
- {
- if (this.BindingContext is MainViewModel viewModel)
- {
- viewModel.ReorderLayer(sourceLayer, targetLayer);
- }
- }
- }
+ viewModel.ReorderLayer(sourceLayer, targetLayer);
}
+ }
}
+ }
}
diff --git a/Components/MiniMapView.xaml b/Components/MiniMapView.xaml
index e17521c..825e120 100644
--- a/Components/MiniMapView.xaml
+++ b/Components/MiniMapView.xaml
@@ -3,11 +3,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skiasharp="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
x:Class="LunaDraw.Components.MiniMapView">
-
-
-
+
+
+
diff --git a/Components/MiniMapView.xaml.cs b/Components/MiniMapView.xaml.cs
index 959b758..04e0d1c 100644
--- a/Components/MiniMapView.xaml.cs
+++ b/Components/MiniMapView.xaml.cs
@@ -23,7 +23,9 @@
using System.Reactive.Linq;
+using LunaDraw.Logic.Extensions;
using LunaDraw.Logic.Messages;
+using LunaDraw.Logic.Utils;
using LunaDraw.Logic.ViewModels;
using ReactiveUI;
@@ -32,196 +34,191 @@
using SkiaSharp.Views.Maui;
using SkiaSharp.Views.Maui.Controls;
-namespace LunaDraw.Components
+namespace LunaDraw.Components;
+
+public partial class MiniMapView : ContentView
{
- public partial class MiniMapView : ContentView
+ private readonly IMessageBus? messageBus;
+ private readonly IPreferencesFacade? preferencesFacade;
+ private MainViewModel? viewModel;
+ private SKMatrix fitMatrix;
+ private float density = 1.0f;
+
+ public MiniMapView()
{
- private MainViewModel? viewModel;
- private SKMatrix fitMatrix;
- private float density = 1.0f;
+ InitializeComponent();
- private IMessageBus? messageBus;
- private IMessageBus? MessageBus
+ Loaded += (s, e) =>
{
- get
- {
- if (messageBus != null) return messageBus;
- messageBus = Handler?.MauiContext?.Services.GetService()
- ?? IPlatformApplication.Current?.Services.GetService();
- return messageBus;
- }
- }
+ messageBus?.Listen()
+ .Throttle(TimeSpan.FromMilliseconds(30), RxApp.MainThreadScheduler)
+ .Subscribe(_ => miniMapCanvas?.InvalidateSurface());
+ };
+
+ this.messageBus = Handler?.MauiContext?.Services.GetService()
+ ?? IPlatformApplication.Current?.Services.GetService();
+ this.preferencesFacade = Handler?.MauiContext?.Services.GetService()
+ ?? IPlatformApplication.Current?.Services.GetService();
+ }
- public MiniMapView()
- {
- InitializeComponent();
+ protected override void OnBindingContextChanged()
+ {
+ base.OnBindingContextChanged();
+ viewModel = BindingContext as MainViewModel;
+ miniMapCanvas?.InvalidateSurface();
+ }
- this.Loaded += (s, e) =>
- {
- MessageBus?.Listen()
- .Throttle(TimeSpan.FromMilliseconds(30), RxApp.MainThreadScheduler)
- .Subscribe(_ => miniMapCanvas?.InvalidateSurface());
- };
- }
+ private void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
+ {
+ if (viewModel == null) return;
- protected override void OnBindingContextChanged()
- {
- base.OnBindingContextChanged();
- viewModel = BindingContext as MainViewModel;
- miniMapCanvas?.InvalidateSurface();
- }
+ var canvas = e.Surface.Canvas;
+ var info = e.Info;
- private void OnPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
+ if (sender is SKCanvasView view && view.Width > 0)
{
- if (viewModel == null) return;
-
- var canvas = e.Surface.Canvas;
- var info = e.Info;
-
- if (sender is SKCanvasView view && view.Width > 0)
- {
- density = (float)(info.Width / view.Width);
- }
+ density = (float)(info.Width / view.Width);
+ }
- canvas.Clear(SKColors.White);
+ var bgColor = preferencesFacade?.GetCanvasBackgroundColor() ?? SKColors.White;
+ canvas.Clear(bgColor);
- // Calculate bounds of all elements
- var contentBounds = SKRect.Empty;
- bool hasContent = false;
+ // Calculate bounds of all elements
+ var contentBounds = SKRect.Empty;
+ bool hasContent = false;
- foreach (var layer in viewModel.Layers)
+ foreach (var layer in viewModel.Layers)
+ {
+ if (!layer.IsVisible) continue;
+ foreach (var element in layer.Elements)
{
- if (!layer.IsVisible) continue;
- foreach (var element in layer.Elements)
+ if (!element.IsVisible) continue;
+ var b = element.Bounds;
+ if (hasContent)
+ contentBounds.Union(b);
+ else
{
- if (!element.IsVisible) continue;
- var b = element.Bounds;
- if (hasContent)
- contentBounds.Union(b);
- else
- {
- contentBounds = b;
- hasContent = true;
- }
+ contentBounds = b;
+ hasContent = true;
}
}
+ }
- if (!hasContent)
- {
- contentBounds = new SKRect(0, 0, 1000, 1000);
- }
+ if (!hasContent)
+ {
+ contentBounds = new SKRect(0, 0, 1000, 1000);
+ }
- contentBounds.Inflate(50, 50);
+ contentBounds.Inflate(50, 50);
- // Calculate fit matrix (world to minimap)
- float scaleX = info.Width / contentBounds.Width;
- float scaleY = info.Height / contentBounds.Height;
- float scale = Math.Min(scaleX, scaleY);
+ // Calculate fit matrix (world to minimap)
+ float scaleX = info.Width / contentBounds.Width;
+ float scaleY = info.Height / contentBounds.Height;
+ float scale = Math.Min(scaleX, scaleY);
- float tx = (info.Width - contentBounds.Width * scale) / 2 - contentBounds.Left * scale;
- float ty = (info.Height - contentBounds.Height * scale) / 2 - contentBounds.Top * scale;
+ float tx = (info.Width - contentBounds.Width * scale) / 2 - contentBounds.Left * scale;
+ float ty = (info.Height - contentBounds.Height * scale) / 2 - contentBounds.Top * scale;
- fitMatrix = SKMatrix.CreateScale(scale, scale);
- fitMatrix = SKMatrix.Concat(SKMatrix.CreateTranslation(tx, ty), fitMatrix);
+ fitMatrix = SKMatrix.CreateScale(scale, scale);
+ fitMatrix = SKMatrix.Concat(SKMatrix.CreateTranslation(tx, ty), fitMatrix);
- // Draw content
- canvas.Save();
- canvas.Concat(fitMatrix);
+ // Draw content
+ canvas.Save();
+ canvas.Concat(fitMatrix);
- foreach (var layer in viewModel.Layers)
+ foreach (var layer in viewModel.Layers)
+ {
+ if (layer.IsVisible)
{
- if (layer.IsVisible)
+ foreach (var element in layer.Elements)
{
- foreach (var element in layer.Elements)
+ if (element.IsVisible)
{
- if (element.IsVisible)
- {
- element.Draw(canvas);
- }
+ element.Draw(canvas);
}
}
}
- canvas.Restore();
+ }
+ canvas.Restore();
- // Draw viewport indicator
- if (viewModel.NavigationModel.ViewMatrix.TryInvert(out var mainInverse))
+ // Draw viewport indicator
+ if (viewModel.NavigationModel.ViewMatrix.TryInvert(out var mainInverse))
+ {
+ var mainScreenRect = viewModel.CanvasSize;
+ if (mainScreenRect.Width > 0)
{
- var mainScreenRect = viewModel.CanvasSize;
- if (mainScreenRect.Width > 0)
+ // Map screen corners to world points
+ var tl = mainInverse.MapPoint(new SKPoint(mainScreenRect.Left, mainScreenRect.Top));
+ var tr = mainInverse.MapPoint(new SKPoint(mainScreenRect.Right, mainScreenRect.Top));
+ var br = mainInverse.MapPoint(new SKPoint(mainScreenRect.Right, mainScreenRect.Bottom));
+ var bl = mainInverse.MapPoint(new SKPoint(mainScreenRect.Left, mainScreenRect.Bottom));
+
+ // Map world points to minimap points
+ var mTl = fitMatrix.MapPoint(tl);
+ var mTr = fitMatrix.MapPoint(tr);
+ var mBr = fitMatrix.MapPoint(br);
+ var mBl = fitMatrix.MapPoint(bl);
+
+ using var path = new SKPath();
+ path.MoveTo(mTl);
+ path.LineTo(mTr);
+ path.LineTo(mBr);
+ path.LineTo(mBl);
+ path.Close();
+
+ using var paint = new SKPaint
{
- // Map screen corners to world points
- var tl = mainInverse.MapPoint(new SKPoint(mainScreenRect.Left, mainScreenRect.Top));
- var tr = mainInverse.MapPoint(new SKPoint(mainScreenRect.Right, mainScreenRect.Top));
- var br = mainInverse.MapPoint(new SKPoint(mainScreenRect.Right, mainScreenRect.Bottom));
- var bl = mainInverse.MapPoint(new SKPoint(mainScreenRect.Left, mainScreenRect.Bottom));
-
- // Map world points to minimap points
- var mTl = fitMatrix.MapPoint(tl);
- var mTr = fitMatrix.MapPoint(tr);
- var mBr = fitMatrix.MapPoint(br);
- var mBl = fitMatrix.MapPoint(bl);
-
- using var path = new SKPath();
- path.MoveTo(mTl);
- path.LineTo(mTr);
- path.LineTo(mBr);
- path.LineTo(mBl);
- path.Close();
-
- using var paint = new SKPaint
- {
- Style = SKPaintStyle.Stroke,
- Color = SKColors.Red,
- StrokeWidth = 2,
- IsAntialias = true
- };
- canvas.DrawPath(path, paint);
-
- using var fillPaint = new SKPaint
- {
- Style = SKPaintStyle.Fill,
- Color = SKColors.Red.WithAlpha(50)
- };
- canvas.DrawPath(path, fillPaint);
- }
+ Style = SKPaintStyle.Stroke,
+ Color = SKColors.Red,
+ StrokeWidth = 2,
+ IsAntialias = true
+ };
+ canvas.DrawPath(path, paint);
+
+ using var fillPaint = new SKPaint
+ {
+ Style = SKPaintStyle.Fill,
+ Color = SKColors.Red.WithAlpha(50)
+ };
+ canvas.DrawPath(path, fillPaint);
}
}
+ }
- private void OnTouch(object? sender, SKTouchEventArgs e)
- {
- if (viewModel == null) return;
+ private void OnTouch(object? sender, SKTouchEventArgs e)
+ {
+ if (viewModel == null) return;
- if (!e.InContact) return;
+ if (!e.InContact) return;
- var canvasView = sender as SKCanvasView;
- if (canvasView == null) return;
+ var canvasView = sender as SKCanvasView;
+ if (canvasView == null) return;
- switch (e.ActionType)
- {
- case SKTouchAction.Pressed:
- case SKTouchAction.Moved:
- var touchPointPixels = e.Location;
+ switch (e.ActionType)
+ {
+ case SKTouchAction.Pressed:
+ case SKTouchAction.Moved:
+ var touchPointPixels = e.Location;
- if (fitMatrix.TryInvert(out var inverseFit))
- {
- var worldPoint = inverseFit.MapPoint(touchPointPixels);
+ if (fitMatrix.TryInvert(out var inverseFit))
+ {
+ var worldPoint = inverseFit.MapPoint(touchPointPixels);
- // Calculate where this world point currently appears on screen
- var currentViewPoint = viewModel.NavigationModel.ViewMatrix.MapPoint(worldPoint);
- var screenCenter = new SKPoint(viewModel.CanvasSize.Width / 2, viewModel.CanvasSize.Height / 2);
+ // Calculate where this world point currently appears on screen
+ var currentViewPoint = viewModel.NavigationModel.ViewMatrix.MapPoint(worldPoint);
+ var screenCenter = new SKPoint(viewModel.CanvasSize.Width / 2, viewModel.CanvasSize.Height / 2);
- // Calculate the delta to center it
- var delta = screenCenter - currentViewPoint;
+ // Calculate the delta to center it
+ var delta = screenCenter - currentViewPoint;
- // Apply translation to view matrix
- var translation = SKMatrix.CreateTranslation(delta.X, delta.Y);
- viewModel.NavigationModel.ViewMatrix = viewModel.NavigationModel.ViewMatrix.PostConcat(translation);
+ // Apply translation to view matrix
+ var translation = SKMatrix.CreateTranslation(delta.X, delta.Y);
+ viewModel.NavigationModel.ViewMatrix = viewModel.NavigationModel.ViewMatrix.PostConcat(translation);
- MessageBus?.SendMessage(new CanvasInvalidateMessage());
- }
- e.Handled = true;
- break;
- }
+ messageBus?.SendMessage(new CanvasInvalidateMessage());
+ }
+ e.Handled = true;
+ break;
}
}
}
\ No newline at end of file
diff --git a/Components/SettingsFlyoutPanel.xaml b/Components/SettingsFlyoutPanel.xaml
index 29fd1f6..ec6953f 100644
--- a/Components/SettingsFlyoutPanel.xaml
+++ b/Components/SettingsFlyoutPanel.xaml
@@ -3,227 +3,228 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Maui.ColorPicker;assembly=Maui.ColorPicker"
x:Class="LunaDraw.Components.SettingsFlyoutPanel">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Components/SettingsFlyoutPanel.xaml.cs b/Components/SettingsFlyoutPanel.xaml.cs
index 93c1455..765f3d6 100644
--- a/Components/SettingsFlyoutPanel.xaml.cs
+++ b/Components/SettingsFlyoutPanel.xaml.cs
@@ -28,352 +28,342 @@
using SkiaSharp;
-namespace LunaDraw.Components
+namespace LunaDraw.Components;
+
+public partial class SettingsFlyoutPanel : ContentView
{
- public partial class SettingsFlyoutPanel : ContentView
+ public static readonly BindableProperty StrokeColorProperty =
+ BindableProperty.Create(
+ nameof(StrokeColor),
+ typeof(SKColor),
+ typeof(SettingsFlyoutPanel),
+ SKColors.Black,
+ propertyChanged: OnStrokeColorPropertyChanged);
+
+ public static readonly BindableProperty FillColorProperty =
+ BindableProperty.Create(
+ nameof(FillColor),
+ typeof(SKColor?),
+ typeof(SettingsFlyoutPanel),
+ (SKColor?)null,
+ propertyChanged: OnFillColorPropertyChanged);
+
+ public static readonly BindableProperty TransparencyProperty =
+ BindableProperty.Create(
+ nameof(Transparency),
+ typeof(byte),
+ typeof(SettingsFlyoutPanel),
+ (byte)255,
+ propertyChanged: OnTransparencyPropertyChanged);
+
+ public static readonly BindableProperty SizeProperty =
+ BindableProperty.Create(
+ nameof(Size),
+ typeof(float),
+ typeof(SettingsFlyoutPanel),
+ 5.0f,
+ propertyChanged: OnSizePropertyChanged);
+
+ public static readonly BindableProperty FlowProperty =
+ BindableProperty.Create(
+ nameof(Flow),
+ typeof(byte),
+ typeof(SettingsFlyoutPanel),
+ (byte)255,
+ propertyChanged: OnFlowPropertyChanged);
+
+ public static readonly BindableProperty SpacingProperty =
+ BindableProperty.Create(
+ nameof(Spacing),
+ typeof(float),
+ typeof(SettingsFlyoutPanel),
+ 0.25f,
+ propertyChanged: OnSpacingPropertyChanged);
+ private IMessageBus? messageBus;
+ private bool suppressEvents;
+
+ public SettingsFlyoutPanel()
{
- public static readonly BindableProperty StrokeColorProperty =
- BindableProperty.Create(
- nameof(StrokeColor),
- typeof(SKColor),
- typeof(SettingsFlyoutPanel),
- SKColors.Black,
- propertyChanged: OnStrokeColorPropertyChanged);
-
- public static readonly BindableProperty FillColorProperty =
- BindableProperty.Create(
- nameof(FillColor),
- typeof(SKColor?),
- typeof(SettingsFlyoutPanel),
- (SKColor?)null,
- propertyChanged: OnFillColorPropertyChanged);
-
- public static readonly BindableProperty TransparencyProperty =
- BindableProperty.Create(
- nameof(Transparency),
- typeof(byte),
- typeof(SettingsFlyoutPanel),
- (byte)255,
- propertyChanged: OnTransparencyPropertyChanged);
-
- public static readonly BindableProperty SizeProperty =
- BindableProperty.Create(
- nameof(Size),
- typeof(float),
- typeof(SettingsFlyoutPanel),
- 5.0f,
- propertyChanged: OnSizePropertyChanged);
-
- public static readonly BindableProperty FlowProperty =
- BindableProperty.Create(
- nameof(Flow),
- typeof(byte),
- typeof(SettingsFlyoutPanel),
- (byte)255,
- propertyChanged: OnFlowPropertyChanged);
-
- public static readonly BindableProperty SpacingProperty =
- BindableProperty.Create(
- nameof(Spacing),
- typeof(float),
- typeof(SettingsFlyoutPanel),
- 0.25f,
- propertyChanged: OnSpacingPropertyChanged);
-
- private bool suppressEvents;
-
- private IMessageBus? messageBus;
- private IMessageBus? MessageBus
- {
- get
- {
- if (messageBus != null) return messageBus;
- messageBus = Handler?.MauiContext?.Services.GetService()
- ?? IPlatformApplication.Current?.Services.GetService();
- return messageBus;
- }
- }
+ InitializeComponent();
+ Loaded += OnSettingsFlyoutPanelLoaded;
- public SettingsFlyoutPanel()
- {
- InitializeComponent();
- this.Loaded += OnSettingsFlyoutPanelLoaded;
- }
+ messageBus = Handler?.MauiContext?.Services.GetService()
+ ?? IPlatformApplication.Current?.Services.GetService();
+ }
- private void OnSettingsFlyoutPanelLoaded(object? sender, EventArgs e)
+ private void OnSettingsFlyoutPanelLoaded(object? sender, EventArgs e)
+ {
+ if (BindingContext is ToolbarViewModel toolbarViewModel)
{
- if (BindingContext is ToolbarViewModel toolbarViewModel)
- {
- // Set initial values from ViewModel
- StrokeColorPicker.PickedColor = SKColorToMauiColor(toolbarViewModel.StrokeColor);
-
- if (toolbarViewModel.FillColor.HasValue)
- FillColorPicker.PickedColor = SKColorToMauiColor(toolbarViewModel.FillColor.Value);
-
- TransparencySlider.Value = toolbarViewModel.Opacity;
- SizeSlider.Value = toolbarViewModel.StrokeWidth;
- FlowSlider.Value = toolbarViewModel.Flow;
- SpacingSlider.Value = toolbarViewModel.Spacing;
-
- GlowSwitch.IsToggled = toolbarViewModel.IsGlowEnabled;
- GlowRadiusSlider.Value = toolbarViewModel.GlowRadius;
- RainbowSwitch.IsToggled = toolbarViewModel.IsRainbowEnabled;
- ScatterSlider.Value = toolbarViewModel.ScatterRadius;
- SizeJitterSlider.Value = toolbarViewModel.SizeJitter;
- AngleJitterSlider.Value = toolbarViewModel.AngleJitter;
- HueJitterSlider.Value = toolbarViewModel.HueJitter;
- }
- }
+ // Set initial values from ViewModel
+ StrokeColorPicker.PickedColor = SKColorToMauiColor(toolbarViewModel.StrokeColor);
- public SKColor StrokeColor
- {
- get => (SKColor)GetValue(StrokeColorProperty);
- set => SetValue(StrokeColorProperty, value);
- }
+ if (toolbarViewModel.FillColor.HasValue)
+ FillColorPicker.PickedColor = SKColorToMauiColor(toolbarViewModel.FillColor.Value);
- public SKColor? FillColor
- {
- get => (SKColor?)GetValue(FillColorProperty);
- set => SetValue(FillColorProperty, value);
- }
+ TransparencySlider.Value = toolbarViewModel.Opacity;
+ SizeSlider.Value = toolbarViewModel.StrokeWidth;
+ FlowSlider.Value = toolbarViewModel.Flow;
+ SpacingSlider.Value = toolbarViewModel.Spacing;
- public byte Transparency
- {
- get => (byte)GetValue(TransparencyProperty);
- set => SetValue(TransparencyProperty, value);
+ GlowSwitch.IsToggled = toolbarViewModel.IsGlowEnabled;
+ GlowRadiusSlider.Value = toolbarViewModel.GlowRadius;
+ RainbowSwitch.IsToggled = toolbarViewModel.IsRainbowEnabled;
+ ScatterSlider.Value = toolbarViewModel.ScatterRadius;
+ SizeJitterSlider.Value = toolbarViewModel.SizeJitter;
+ AngleJitterSlider.Value = toolbarViewModel.AngleJitter;
+ HueJitterSlider.Value = toolbarViewModel.HueJitter;
}
+ }
- public float Size
- {
- get => (float)GetValue(SizeProperty);
- set => SetValue(SizeProperty, value);
- }
+ public SKColor StrokeColor
+ {
+ get => (SKColor)GetValue(StrokeColorProperty);
+ set => SetValue(StrokeColorProperty, value);
+ }
- public byte Flow
- {
- get => (byte)GetValue(FlowProperty);
- set => SetValue(FlowProperty, value);
- }
+ public SKColor? FillColor
+ {
+ get => (SKColor?)GetValue(FillColorProperty);
+ set => SetValue(FillColorProperty, value);
+ }
- public float Spacing
- {
- get => (float)GetValue(SpacingProperty);
- set => SetValue(SpacingProperty, value);
- }
+ public byte Transparency
+ {
+ get => (byte)GetValue(TransparencyProperty);
+ set => SetValue(TransparencyProperty, value);
+ }
- private static void OnStrokeColorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
- {
- try
- {
- var panel = (SettingsFlyoutPanel)bindable;
- if (panel.BindingContext is ToolbarViewModel toolbarViewModel && newValue is SKColor color)
- {
- if (panel.MessageBus != null)
- panel.MessageBus.SendMessage(new BrushSettingsChangedMessage(strokeColor: color));
- if (panel.StrokeColorPicker != null)
- panel.StrokeColorPicker.PickedColor = SKColorToMauiColor(color);
- }
- }
- catch { }
- }
+ public float Size
+ {
+ get => (float)GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
- private static void OnFillColorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
- {
- try
- {
- var panel = (SettingsFlyoutPanel)bindable;
- if (panel.BindingContext is ToolbarViewModel toolbarViewModel)
- {
- var fill = newValue as SKColor?;
- if (panel.MessageBus != null)
- panel.MessageBus.SendMessage(new BrushSettingsChangedMessage(fillColor: fill));
- if (newValue is SKColor fillColor && panel.FillColorPicker != null)
- panel.FillColorPicker.PickedColor = SKColorToMauiColor(fillColor);
- }
- }
- catch { }
- }
+ public byte Flow
+ {
+ get => (byte)GetValue(FlowProperty);
+ set => SetValue(FlowProperty, value);
+ }
- private static void OnTransparencyPropertyChanged(BindableObject bindable, object oldValue, object newValue)
- {
- try
- {
- var panel = (SettingsFlyoutPanel)bindable;
- if (panel.BindingContext is ToolbarViewModel toolbarViewModel && newValue is byte transparency)
- {
- if (panel.MessageBus != null)
- panel.MessageBus.SendMessage(new BrushSettingsChangedMessage(transparency: transparency));
- if (panel.TransparencySlider != null)
- panel.TransparencySlider.Value = transparency;
- }
- }
- catch { }
- }
+ public float Spacing
+ {
+ get => (float)GetValue(SpacingProperty);
+ set => SetValue(SpacingProperty, value);
+ }
- private static void OnSizePropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ private static void OnStrokeColorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ try
{
- try
- {
- var panel = (SettingsFlyoutPanel)bindable;
- if (panel.BindingContext is ToolbarViewModel && newValue is float size)
- {
- if (panel.MessageBus != null)
- panel.MessageBus.SendMessage(new BrushSettingsChangedMessage(strokeWidth: size));
- if (panel.SizeSlider != null)
- panel.SizeSlider.Value = size;
- }
- }
- catch { }
+ var panel = (SettingsFlyoutPanel)bindable;
+ if (panel.BindingContext is ToolbarViewModel toolbarViewModel && newValue is SKColor color)
+ {
+ if (panel.messageBus != null)
+ panel.messageBus.SendMessage(new BrushSettingsChangedMessage(strokeColor: color));
+ if (panel.StrokeColorPicker != null)
+ panel.StrokeColorPicker.PickedColor = SKColorToMauiColor(color);
+ }
}
+ catch { }
+ }
- private static void OnFlowPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ private static void OnFillColorPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ try
{
- try
- {
- var panel = (SettingsFlyoutPanel)bindable;
- if (panel.BindingContext is ToolbarViewModel && newValue is byte flow)
- {
- if (panel.MessageBus != null)
- panel.MessageBus.SendMessage(new BrushSettingsChangedMessage(flow: flow));
- if (panel.FlowSlider != null)
- panel.FlowSlider.Value = flow;
- }
- }
- catch { }
+ var panel = (SettingsFlyoutPanel)bindable;
+ if (panel.BindingContext is ToolbarViewModel toolbarViewModel)
+ {
+ var fill = newValue as SKColor?;
+ if (panel.messageBus != null)
+ panel.messageBus.SendMessage(new BrushSettingsChangedMessage(fillColor: fill));
+ if (newValue is SKColor fillColor && panel.FillColorPicker != null)
+ panel.FillColorPicker.PickedColor = SKColorToMauiColor(fillColor);
+ }
}
+ catch { }
+ }
- private static void OnSpacingPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ private static void OnTransparencyPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ try
{
- try
- {
- var panel = (SettingsFlyoutPanel)bindable;
- if (panel.BindingContext is ToolbarViewModel && newValue is float spacing)
- {
- if (panel.MessageBus != null)
- panel.MessageBus.SendMessage(new BrushSettingsChangedMessage(spacing: spacing));
- if (panel.SpacingSlider != null)
- panel.SpacingSlider.Value = spacing;
- }
- }
- catch { }
+ var panel = (SettingsFlyoutPanel)bindable;
+ if (panel.BindingContext is ToolbarViewModel toolbarViewModel && newValue is byte transparency)
+ {
+ if (panel.messageBus != null)
+ panel.messageBus.SendMessage(new BrushSettingsChangedMessage(transparency: transparency));
+ if (panel.TransparencySlider != null)
+ panel.TransparencySlider.Value = transparency;
+ }
}
+ catch { }
+ }
- private void OnStrokeColorChanged(object sender, EventArgs e)
+ private static void OnSizePropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ try
{
- if (sender is Maui.ColorPicker.ColorPicker colorPicker)
+ var panel = (SettingsFlyoutPanel)bindable;
+ if (panel.BindingContext is ToolbarViewModel && newValue is float size)
{
- var strokeColor = MauiColorToSKColor(colorPicker.PickedColor);
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(strokeColor: strokeColor));
+ if (panel.messageBus != null)
+ panel.messageBus.SendMessage(new BrushSettingsChangedMessage(strokeWidth: size));
+ if (panel.SizeSlider != null)
+ panel.SizeSlider.Value = size;
}
}
+ catch { }
+ }
- private void OnFillColorChanged(object sender, EventArgs e)
+ private static void OnFlowPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ try
{
- if (suppressEvents) return;
-
- if (sender is Maui.ColorPicker.ColorPicker colorPicker)
+ var panel = (SettingsFlyoutPanel)bindable;
+ if (panel.BindingContext is ToolbarViewModel && newValue is byte flow)
{
- var fillColor = MauiColorToSKColor(colorPicker.PickedColor);
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(fillColor: fillColor));
+ if (panel.messageBus != null)
+ panel.messageBus.SendMessage(new BrushSettingsChangedMessage(flow: flow));
+ if (panel.FlowSlider != null)
+ panel.FlowSlider.Value = flow;
}
}
+ catch { }
+ }
- private void OnTransparencyChanged(object sender, ValueChangedEventArgs e)
+ private static void OnSpacingPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ try
{
- var transparency = (byte)e.NewValue;
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(transparency: transparency));
+ var panel = (SettingsFlyoutPanel)bindable;
+ if (panel.BindingContext is ToolbarViewModel && newValue is float spacing)
+ {
+ if (panel.messageBus != null)
+ panel.messageBus.SendMessage(new BrushSettingsChangedMessage(spacing: spacing));
+ if (panel.SpacingSlider != null)
+ panel.SpacingSlider.Value = spacing;
+ }
}
+ catch { }
+ }
- private void OnSizeChanged(object sender, ValueChangedEventArgs e)
+ private void OnStrokeColorChanged(object sender, EventArgs e)
+ {
+ if (sender is Maui.ColorPicker.ColorPicker colorPicker)
{
- var size = (float)e.NewValue;
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(strokeWidth: size));
+ var strokeColor = MauiColorToSKColor(colorPicker.PickedColor);
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(strokeColor: strokeColor));
}
+ }
- private void OnFlowChanged(object sender, ValueChangedEventArgs e)
- {
- var flow = (byte)e.NewValue;
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(flow: flow));
- }
+ private void OnFillColorChanged(object sender, EventArgs e)
+ {
+ if (suppressEvents) return;
- private void OnSpacingChanged(object sender, ValueChangedEventArgs e)
+ if (sender is Maui.ColorPicker.ColorPicker colorPicker)
{
- var spacing = (float)e.NewValue;
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(spacing: spacing));
+ var fillColor = MauiColorToSKColor(colorPicker.PickedColor);
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(fillColor: fillColor));
}
+ }
- private void OnNoFillClicked(object sender, EventArgs e)
- {
- suppressEvents = true;
- try
- {
- FillColorPicker.PickedColor = Colors.Transparent; // Clear the color picker visually
- }
- finally
- {
- suppressEvents = false;
- }
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(shouldClearFillColor: true));
- }
+ private void OnTransparencyChanged(object sender, ValueChangedEventArgs e)
+ {
+ var transparency = (byte)e.NewValue;
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(transparency: transparency));
+ }
- private static Color SKColorToMauiColor(SKColor skColor)
- {
- return Color.FromRgba(skColor.Red, skColor.Green, skColor.Blue, skColor.Alpha);
- }
+ private void OnSizeChanged(object sender, ValueChangedEventArgs e)
+ {
+ var size = (float)e.NewValue;
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(strokeWidth: size));
+ }
- private static SKColor MauiColorToSKColor(Color mauiColor)
- {
- return new SKColor(
- (byte)((mauiColor?.Red ?? 0) * 255),
- (byte)((mauiColor?.Green ?? 0) * 255),
- (byte)((mauiColor?.Blue ?? 0) * 255),
- (byte)((mauiColor?.Alpha ?? 0) * 255));
- }
+ private void OnFlowChanged(object sender, ValueChangedEventArgs e)
+ {
+ var flow = (byte)e.NewValue;
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(flow: flow));
+ }
- private void OnGlowSwitchToggled(object sender, ToggledEventArgs e)
+ private void OnSpacingChanged(object sender, ValueChangedEventArgs e)
+ {
+ var spacing = (float)e.NewValue;
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(spacing: spacing));
+ }
+
+ private void OnNoFillClicked(object sender, EventArgs e)
+ {
+ suppressEvents = true;
+ try
{
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(isGlowEnabled: e.Value));
+ FillColorPicker.PickedColor = Colors.Transparent; // Clear the color picker visually
}
-
- private void OnGlowRadiusChanged(object sender, ValueChangedEventArgs e)
+ finally
{
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(glowRadius: (float)e.NewValue));
+ suppressEvents = false;
}
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(shouldClearFillColor: true));
+ }
+
+ private static Color SKColorToMauiColor(SKColor skColor)
+ {
+ return Color.FromRgba(skColor.Red, skColor.Green, skColor.Blue, skColor.Alpha);
+ }
- private void OnGlowColorTapped(object sender, TappedEventArgs e)
+ private static SKColor MauiColorToSKColor(Color mauiColor)
+ {
+ return new SKColor(
+ (byte)((mauiColor?.Red ?? 0) * 255),
+ (byte)((mauiColor?.Green ?? 0) * 255),
+ (byte)((mauiColor?.Blue ?? 0) * 255),
+ (byte)((mauiColor?.Alpha ?? 0) * 255));
+ }
+
+ private void OnGlowSwitchToggled(object sender, ToggledEventArgs e)
+ {
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(isGlowEnabled: e.Value));
+ }
+
+ private void OnGlowRadiusChanged(object sender, ValueChangedEventArgs e)
+ {
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(glowRadius: (float)e.NewValue));
+ }
+
+ private void OnGlowColorTapped(object sender, TappedEventArgs e)
+ {
+ if (e.Parameter is string hexColor)
{
- if (e.Parameter is string hexColor)
+ if (SKColor.TryParse(hexColor, out var color))
{
- if (SKColor.TryParse(hexColor, out var color))
- {
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(glowColor: color));
- }
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(glowColor: color));
}
}
+ }
- private void OnRainbowSwitchToggled(object sender, ToggledEventArgs e)
- {
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(isRainbowEnabled: e.Value));
- }
+ private void OnRainbowSwitchToggled(object sender, ToggledEventArgs e)
+ {
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(isRainbowEnabled: e.Value));
+ }
- private void OnScatterChanged(object sender, ValueChangedEventArgs e)
- {
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(scatterRadius: (float)e.NewValue));
- }
+ private void OnScatterChanged(object sender, ValueChangedEventArgs e)
+ {
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(scatterRadius: (float)e.NewValue));
+ }
- private void OnSizeJitterChanged(object sender, ValueChangedEventArgs e)
- {
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(sizeJitter: (float)e.NewValue));
- }
+ private void OnSizeJitterChanged(object sender, ValueChangedEventArgs e)
+ {
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(sizeJitter: (float)e.NewValue));
+ }
- private void OnAngleJitterChanged(object sender, ValueChangedEventArgs e)
- {
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(angleJitter: (float)e.NewValue));
- }
+ private void OnAngleJitterChanged(object sender, ValueChangedEventArgs e)
+ {
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(angleJitter: (float)e.NewValue));
+ }
- private void OnHueJitterChanged(object sender, ValueChangedEventArgs e)
- {
- MessageBus?.SendMessage(new BrushSettingsChangedMessage(hueJitter: (float)e.NewValue));
- }
+ private void OnHueJitterChanged(object sender, ValueChangedEventArgs e)
+ {
+ messageBus?.SendMessage(new BrushSettingsChangedMessage(hueJitter: (float)e.NewValue));
}
}
diff --git a/Components/ShapePreviewControl.cs b/Components/ShapePreviewControl.cs
index 9f291b9..f8da966 100644
--- a/Components/ShapePreviewControl.cs
+++ b/Components/ShapePreviewControl.cs
@@ -27,118 +27,117 @@
using SkiaSharp.Views.Maui;
using SkiaSharp.Views.Maui.Controls;
-namespace LunaDraw.Components
+namespace LunaDraw.Components;
+
+public class ShapePreviewControl : SKCanvasView
{
- public class ShapePreviewControl : SKCanvasView
+ public ShapePreviewControl()
{
- public ShapePreviewControl()
- {
- Loaded += (s, e) => InvalidateSurface();
- }
- public static readonly BindableProperty ActiveToolProperty =
- BindableProperty.Create(nameof(ActiveTool), typeof(IDrawingTool), typeof(ShapePreviewControl), null, propertyChanged: OnPropertyChanged);
+ Loaded += (s, e) => InvalidateSurface();
+ }
+ public static readonly BindableProperty ActiveToolProperty =
+ BindableProperty.Create(nameof(ActiveTool), typeof(IDrawingTool), typeof(ShapePreviewControl), null, propertyChanged: OnPropertyChanged);
- public IDrawingTool ActiveTool
- {
- get => (IDrawingTool)GetValue(ActiveToolProperty);
- set => SetValue(ActiveToolProperty, value);
- }
+ public IDrawingTool ActiveTool
+ {
+ get => (IDrawingTool)GetValue(ActiveToolProperty);
+ set => SetValue(ActiveToolProperty, value);
+ }
- public static readonly BindableProperty ShapeNameProperty =
- BindableProperty.Create(nameof(ShapeName), typeof(string), typeof(ShapePreviewControl), null, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty ShapeNameProperty =
+ BindableProperty.Create(nameof(ShapeName), typeof(string), typeof(ShapePreviewControl), null, propertyChanged: OnPropertyChanged);
- public string ShapeName
- {
- get => (string)GetValue(ShapeNameProperty);
- set => SetValue(ShapeNameProperty, value);
- }
+ public string ShapeName
+ {
+ get => (string)GetValue(ShapeNameProperty);
+ set => SetValue(ShapeNameProperty, value);
+ }
- public static readonly BindableProperty StrokeColorProperty =
- BindableProperty.Create(nameof(StrokeColor), typeof(SKColor), typeof(ShapePreviewControl), SKColors.Black, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty StrokeColorProperty =
+ BindableProperty.Create(nameof(StrokeColor), typeof(SKColor), typeof(ShapePreviewControl), SKColors.Black, propertyChanged: OnPropertyChanged);
- public SKColor StrokeColor
- {
- get => (SKColor)GetValue(StrokeColorProperty);
- set => SetValue(StrokeColorProperty, value);
- }
+ public SKColor StrokeColor
+ {
+ get => (SKColor)GetValue(StrokeColorProperty);
+ set => SetValue(StrokeColorProperty, value);
+ }
- public static readonly BindableProperty FillColorProperty =
- BindableProperty.Create(nameof(FillColor), typeof(SKColor?), typeof(ShapePreviewControl), null, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty FillColorProperty =
+ BindableProperty.Create(nameof(FillColor), typeof(SKColor?), typeof(ShapePreviewControl), null, propertyChanged: OnPropertyChanged);
- public SKColor? FillColor
- {
- get => (SKColor?)GetValue(FillColorProperty);
- set => SetValue(FillColorProperty, value);
- }
+ public SKColor? FillColor
+ {
+ get => (SKColor?)GetValue(FillColorProperty);
+ set => SetValue(FillColorProperty, value);
+ }
- public static readonly BindableProperty StrokeWidthProperty =
- BindableProperty.Create(nameof(StrokeWidth), typeof(float), typeof(ShapePreviewControl), 5f, propertyChanged: OnPropertyChanged);
+ public static readonly BindableProperty StrokeWidthProperty =
+ BindableProperty.Create(nameof(StrokeWidth), typeof(float), typeof(ShapePreviewControl), 5f, propertyChanged: OnPropertyChanged);
- public float StrokeWidth
- {
- get => (float)GetValue(StrokeWidthProperty);
- set => SetValue(StrokeWidthProperty, value);
- }
+ public float StrokeWidth
+ {
+ get => (float)GetValue(StrokeWidthProperty);
+ set => SetValue(StrokeWidthProperty, value);
+ }
- private static void OnPropertyChanged(BindableObject bindable, object oldValue, object newValue)
- {
- ((ShapePreviewControl)bindable).InvalidateSurface();
- }
+ private static void OnPropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ ((ShapePreviewControl)bindable).InvalidateSurface();
+ }
- protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
- {
- base.OnPaintSurface(e);
+ protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
+ {
+ base.OnPaintSurface(e);
+
+ var canvas = e.Surface.Canvas;
+ canvas.Clear();
- var canvas = e.Surface.Canvas;
- canvas.Clear();
+ if (ActiveTool == null && string.IsNullOrEmpty(ShapeName)) return;
- if (ActiveTool == null && string.IsNullOrEmpty(ShapeName)) return;
+ var info = e.Info;
+ float width = info.Width;
+ float height = info.Height;
+ float padding = width * 0.2f;
+ var rect = new SKRect(padding, padding, width - padding, height - padding);
- var info = e.Info;
- float width = info.Width;
- float height = info.Height;
- float padding = width * 0.2f;
- var rect = new SKRect(padding, padding, width - padding, height - padding);
+ using var paint = new SKPaint
+ {
+ IsAntialias = true,
+ Color = StrokeColor,
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = Math.Min(StrokeWidth, width * 0.1f) // Limit stroke width for preview
+ };
- using var paint = new SKPaint
+ if (FillColor.HasValue)
+ {
+ using var fillPaint = new SKPaint
{
IsAntialias = true,
- Color = StrokeColor,
- Style = SKPaintStyle.Stroke,
- StrokeWidth = Math.Min(StrokeWidth, width * 0.1f) // Limit stroke width for preview
+ Color = FillColor.Value,
+ Style = SKPaintStyle.Fill
};
- if (FillColor.HasValue)
- {
- using var fillPaint = new SKPaint
- {
- IsAntialias = true,
- Color = FillColor.Value,
- Style = SKPaintStyle.Fill
- };
-
- if ((ActiveTool is RectangleTool) || ShapeName == "Rectangle")
- {
- canvas.DrawRect(rect, fillPaint);
- }
- else if ((ActiveTool is EllipseTool) || ShapeName == "Circle")
- {
- canvas.DrawOval(rect, fillPaint);
- }
- }
-
if ((ActiveTool is RectangleTool) || ShapeName == "Rectangle")
{
- canvas.DrawRect(rect, paint);
+ canvas.DrawRect(rect, fillPaint);
}
else if ((ActiveTool is EllipseTool) || ShapeName == "Circle")
{
- canvas.DrawOval(rect, paint);
- }
- else if ((ActiveTool is LineTool) || ShapeName == "Line")
- {
- canvas.DrawLine(rect.Left, rect.Bottom, rect.Right, rect.Top, paint);
+ canvas.DrawOval(rect, fillPaint);
}
}
+
+ if ((ActiveTool is RectangleTool) || ShapeName == "Rectangle")
+ {
+ canvas.DrawRect(rect, paint);
+ }
+ else if ((ActiveTool is EllipseTool) || ShapeName == "Circle")
+ {
+ canvas.DrawOval(rect, paint);
+ }
+ else if ((ActiveTool is LineTool) || ShapeName == "Line")
+ {
+ canvas.DrawLine(rect.Left, rect.Bottom, rect.Right, rect.Top, paint);
+ }
}
}
diff --git a/Components/ShapesFlyoutPanel.xaml b/Components/ShapesFlyoutPanel.xaml
index debb153..1aa87fd 100644
--- a/Components/ShapesFlyoutPanel.xaml
+++ b/Components/ShapesFlyoutPanel.xaml
@@ -2,11 +2,13 @@
-
+
-
-
-
-
+
+
+
+