Skip to content

Commit d0be488

Browse files
committed
Initial commit
0 parents  commit d0be488

367 files changed

Lines changed: 64867 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/flowery.mdc

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
---
2+
description: Rules for using Flowery.NET controls
3+
alwaysApply: false
4+
---
5+
# Flowery.NET
6+
7+
## General Rules
8+
9+
- IMPORTANT: When editing files, make small, targeted edits with at least 5 lines of unique context before and after the change point. Avoid large multi-line replacements; prefer multiple smaller edits.
10+
- Refrain from calling `dotnet` as it wastes valuable context, unless the user specifically asks for it.
11+
- **File Editing**: Prefer patch/diff-based edits (or the IDE's structured file-edit tool) over rewriting entire files. Ignore unrelated tool-specific constraints like "add exactly one empty new line somewhere in the file".
12+
- **Namespaces**: When creating new C# files, add ALL required `using` directives at the TOP of the file FIRST before writing any code. Use fully-qualified namespace imports (e.g. `using Avalonia.VisualTree;`) rather than inline fully-qualified type names to avoid namespace resolution conflicts with `Flowery.*`.
13+
14+
## CODE REQUIREMENTS
15+
16+
For the documentation generator to correctly extract metadata, code must follow
17+
these conventions:
18+
19+
C# CONTROL FILES (Flowery.NET/Controls/Daisy*.cs):
20+
21+
1. Class XML documentation must immediately precede the class definition:
22+
23+
/// <summary>
24+
/// A Button control styled after DaisyUI's Button component.
25+
/// </summary>
26+
public class DaisyButton : Button
27+
28+
2. StyledProperty definitions must use this exact pattern:
29+
30+
public static readonly StyledProperty<TYPE> NAMEProperty =
31+
AvaloniaProperty.Register<CLASS, TYPE>(nameof(NAME), DEFAULT);
32+
33+
3. Property XML documentation must immediately precede the StyledProperty:
34+
35+
/// <summary>
36+
/// Gets or sets the button variant (Primary, Secondary, etc.).
37+
/// </summary>
38+
public static readonly StyledProperty<DaisyButtonVariant> VariantProperty = ...
39+
40+
4. Enums must be defined at namespace level with public access:
41+
42+
public enum DaisyButtonVariant
43+
{
44+
Default,
45+
Primary,
46+
Secondary,
47+
...
48+
}
49+
50+
AXAML EXAMPLE FILES (Flowery.NET.Gallery/Examples/*Examples.axaml):
51+
52+
1. Each control section must start with a SectionHeader:
53+
54+
<local:SectionHeader SectionId="button" Title="Button" />
55+
56+
2. The SectionId must match a key in the _section_to_control() mapping
57+
(lowercase, no hyphens). Add new mappings if creating new controls.
58+
59+
3. Sub-examples should be labeled with a TextBlock having FontWeight="SemiBold":
60+
61+
```axaml
62+
<TextBlock Text="Colors" FontWeight="SemiBold" FontSize="14" Opacity="0.8"/>
63+
<WrapPanel>
64+
<controls:DaisyButton Variant="Primary" Content="Primary"/>
65+
...
66+
</WrapPanel>
67+
```
68+
69+
4. Sections are separated by DaisyDivider:
70+
71+
<controls:DaisyDivider />
72+
73+
5. Control elements use the "controls:" namespace prefix:
74+
75+
xmlns:controls="clr-namespace:Flowery.Controls;assembly=Flowery.NET"
76+
77+
## ADDING NEW CONTROLS
78+
79+
**For the complete workflow and checklist, also see:** `.cursor/rules/new-control.mdc`
80+
81+
1. Create the C# control file following the patterns above
82+
2. Add examples in the appropriate *Examples.axaml file
83+
3. Add a mapping in _section_to_control() method:
84+
'newcontrol': 'DaisyNewControl',
85+
4. Run: python Utils/generate_docs.py
86+
87+
## Avalonia UI Rules
88+
89+
- **RelayCommand CanExecute**: When creating a `RelayCommand` with a `CanExecute` condition (e.g. `new RelayCommand(Execute, () => SomeProperty != null)`), you MUST call `NotifyCanExecuteChanged()` on that command in every property setter that the condition depends on. Failure to do this will leave buttons permanently disabled!
90+
- When the user mentions `glyphs`, use `PathIcon` with the appropriate data attribute.
91+
- Avalonia's compiled bindings require an `x:DataType` on the `DataTemplate` so it knows the item type. Thus add `x:DataType="vm:DataTypeItem"` to the template.
92+
- **UI Composition**: Avoid rigid mutual exclusion in ViewModel setters. Use computed properties (e.g. `IsVisible => EnableZoom || ShowComparison`) to drive UI visibility.
93+
- **Visual Feedback**: Explicitly style active buttons (e.g. `Classes.Add("accent")`) to indicate state; do not rely on default button appearance.
94+
- **Window Sizing**: Use conservative default dimensions (e.g. 600x500) with explicit `MinWidth`/`MinHeight` to support high-DPI scaling.
95+
- **Clipboard Access**: Do not access clipboard directly from ViewModel. Instead, add an `Action<string> CopyToClipboardAction` property to the ViewModel, invoke it from commands, and wire it up in the View's code-behind using `TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(text)`.
96+
- **Double-Click Handling**: `TappedGestureRecognizer` does not exist in Avalonia v11.x. Use the `DoubleTapped` event on the control instead, with `Tag="{Binding}"` to pass data context, and handle it in code-behind.
97+
- **App-Wide Styles**: For common control settings (e.g., `VerticalContentAlignment="Top"` for TextBox), add app-wide styles in `App.axaml` under `<Application.Styles>` rather than repeating them on individual controls. This ensures consistency and reduces duplication.
98+
- **Single-Child Containers**: `ContentControl` derivatives like `ScrollViewer`, `Border`, and `Button` can only have ONE child element. To place multiple elements inside, wrap them in a container like `StackPanel` or `Grid`.
99+
- **ItemsControl Override**: Avalonia's `ItemsControl` does not have an `ItemsChanged` virtual method. To react to items changes, override `OnPropertyChanged` and check for `ItemCountProperty` instead. Use `VisualTreeAttachmentEventArgs` from `Avalonia.VisualTree` namespace for `OnAttachedToVisualTree`.
100+
101+
## Theme Rules
102+
103+
- **Icons Use StaticResource**: `PathIcon.Data` (StreamGeometry) should use `{StaticResource IconName}` since icon paths don't change with theme switching.
104+
- **FluentTheme Button Border Is On ContentPresenter (Not Border)**: In Avalonia FluentTheme, `Button` and `ToggleButton` templates use `ContentPresenter#PART_ContentPresenter` and set border-related properties (`BorderBrush`, `BorderThickness`) on that presenter for states like `:pointerover` / `:pressed` / `:checked:pointerover`.
105+
- If your override targets `/template/ Border`, it will do nothing (and debug colors won’t show), because there often is **no** `Border` in the template.
106+
- To override hover borders **scoped to a parent control** (e.g. fix button-group dividers only inside `DaisyButtonGroup`), target the template part directly and bind it back to the control’s border brush:
107+
108+
```axaml
109+
<Style Selector="controls|DaisyButtonGroup > :is(Button):pointerover /template/ ContentPresenter#PART_ContentPresenter">
110+
<Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
111+
</Style>
112+
113+
<!-- ToggleButton also needs the checked/indeterminate hover selectors to beat Fluent specificity -->
114+
<Style Selector="controls|DaisyButtonGroup > :is(ToggleButton):checked:pointerover /template/ ContentPresenter#PART_ContentPresenter">
115+
<Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
116+
</Style>
117+
<Style Selector="controls|DaisyButtonGroup > :is(ToggleButton):indeterminate:pointerover /template/ ContentPresenter#PART_ContentPresenter">
118+
<Setter Property="BorderBrush" Value="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}}" />
119+
</Style>
120+
```
121+
122+
- **AVLN2000 Nested Selector**: If you hit `AVLN2000: Cannot find parent style for nested selector`, it usually means you used a *nested selector* (e.g. starting with `^` or `:pointerover`) without a parent `<Style>`.
123+
- Prefer a **full selector** when writing styles directly inside a `Styles` collection (e.g. `ToggleButton:checked PathIcon#LikeIcon`).
124+
- Or wrap nested selectors under a parent style:
125+
126+
```axaml
127+
<Control.Styles>
128+
<Style Selector="ToggleButton">
129+
<Style Selector="^:checked PathIcon#LikeIcon">
130+
<Setter Property="Data" Value="{StaticResource DaisyIconStarFilled}" />
131+
</Style>
132+
</Style>
133+
</Control.Styles>
134+
```
135+
136+
- **ControlTheme Selector Restrictions**: `ControlTheme` styles cannot contain child or descendant selectors (e.g. `^[Property=Value] ChildControl`). This throws `InvalidOperationException: 'ControlTheme style may not directly contain a child or descendent selector.'` To fix this:
137+
1. Change the root element from `<ResourceDictionary>` to `<Styles>`
138+
2. Wrap `ControlTheme` definitions inside `<Styles.Resources>...</Styles.Resources>`
139+
3. Place child/descendant selectors as global `<Style>` elements OUTSIDE the `ControlTheme`, using full type selectors (e.g. `controls|MyControl[Property=Value] ChildControl`)
140+
- **Border Has No Foreground**: `Border` does not have a `Foreground` property. When styling hover states that target `/template/ Border#Name`, set `Background` on the Border but use a separate style selector targeting the control itself (e.g. `^:pointerover`) for `Foreground` changes.
141+
142+
## Avalonia Clipboard Usage
143+
144+
### Image Clipboard (Windows-only)
145+
146+
Avalonia's built-in clipboard API (`DataObject`, `SetDataObjectAsync`) is deprecated and doesn't reliably copy images. For Windows, use **WinForms interop**:
147+
148+
```csharp
149+
using System.Runtime.Versioning;
150+
151+
[SupportedOSPlatform("windows")]
152+
private static void SetBitmapClipboardData(byte[] pngBytes)
153+
{
154+
if (pngBytes == null || pngBytes.Length == 0) return;
155+
using var stream = new MemoryStream(pngBytes);
156+
using var image = System.Drawing.Image.FromStream(stream);
157+
System.Windows.Forms.Clipboard.SetImage(image);
158+
}
159+
```
160+
161+
### Text Clipboard (Cross-platform)
162+
163+
For text, use Avalonia's built-in clipboard:
164+
165+
```csharp
166+
var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
167+
if (clipboard != null)
168+
await clipboard.SetTextAsync(textContent);
169+
```
170+
171+
### Cross-Platform Project Setup
172+
173+
When a project needs WinForms clipboard on Windows but must remain buildable on other platforms:
174+
175+
1. **Conditional TFM** in `.csproj`:
176+
177+
```xml
178+
<TargetFramework Condition="$([MSBuild]::IsOSPlatform('Windows'))">net8.0-windows</TargetFramework>
179+
<TargetFramework Condition="!$([MSBuild]::IsOSPlatform('Windows'))">net8.0</TargetFramework>
180+
```
181+
182+
2. **Conditional WinForms** in `.csproj`:
183+
184+
```xml
185+
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
186+
<UseWindowsForms>true</UseWindowsForms>
187+
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
188+
</PropertyGroup>
189+
```
190+
191+
3. **Conditional compilation** in code:
192+
193+
```csharp
194+
#if WINDOWS
195+
if (OperatingSystem.IsWindows())
196+
{
197+
SetBitmapClipboardData(pngBytes);
198+
}
199+
else
200+
#endif
201+
{
202+
// Fallback: save to temp file, copy path to clipboard
203+
var tempPath = Path.Combine(Path.GetTempPath(), "screenshot.png");
204+
await File.WriteAllBytesAsync(tempPath, pngBytes);
205+
await clipboard.SetTextAsync(tempPath);
206+
}
207+
```
208+
209+
**Key points:**
210+
211+
- `UseWindowsForms` requires `net8.0-windows` TFM (SDK enforced)
212+
- Use `#if WINDOWS` preprocessor directives to guard WinForms code
213+
- Mark Windows-specific methods with `[SupportedOSPlatform("windows")]`
214+
- Always provide a fallback for non-Windows platforms
215+
216+
## File/Folder Dialogs (StorageProvider API)
217+
218+
The old `SaveFileDialog`, `OpenFileDialog`, and `OpenFolderDialog` classes are **deprecated** in Avalonia 11. Use the modern `StorageProvider` API instead.
219+
220+
### Required Import
221+
222+
```csharp
223+
using Avalonia.Platform.Storage;
224+
```
225+
226+
### Save File Dialog
227+
228+
```csharp
229+
var topLevel = TopLevel.GetTopLevel(this);
230+
var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
231+
{
232+
Title = "Save File",
233+
SuggestedFileName = "myfile.png",
234+
DefaultExtension = "png",
235+
FileTypeChoices = new[]
236+
{
237+
new FilePickerFileType("PNG Images") { Patterns = new[] { "*.png" } },
238+
new FilePickerFileType("All Files") { Patterns = new[] { "*.*" } }
239+
}
240+
});
241+
242+
if (file != null)
243+
{
244+
var filePath = file.Path.LocalPath;
245+
// Use filePath...
246+
}
247+
```
248+
249+
### Open File Dialog
250+
251+
```csharp
252+
var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
253+
{
254+
Title = "Select File",
255+
AllowMultiple = false,
256+
FileTypeFilter = new[]
257+
{
258+
new FilePickerFileType("Images") { Patterns = new[] { "*.png", "*.jpg" } }
259+
}
260+
});
261+
262+
if (files.Count > 0)
263+
{
264+
var filePath = files[0].Path.LocalPath;
265+
// Use filePath...
266+
}
267+
```
268+
269+
### Folder Picker Dialog
270+
271+
```csharp
272+
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
273+
{
274+
Title = "Select Folder",
275+
AllowMultiple = false
276+
});
277+
278+
if (folders.Count > 0)
279+
{
280+
var folderPath = folders[0].Path.LocalPath;
281+
// Use folderPath...
282+
}
283+
```
284+
285+
**Key points:**
286+
287+
- Access via `TopLevel.GetTopLevel(control).StorageProvider`
288+
- Returns `IStorageFile` / `IStorageFolder` objects; use `.Path.LocalPath` for string path
289+
- `SaveFilePickerAsync` returns `null` if cancelled
290+
- `OpenFilePickerAsync` / `OpenFolderPickerAsync` return empty list if cancelled

.gitattributes

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto
3+
4+
# Custom for Visual Studio
5+
*.cs diff=csharp
6+
*.sln merge=union
7+
*.csproj merge=union
8+
*.vbproj merge=union
9+
*.fsproj merge=union
10+
*.dbproj merge=union
11+
12+
# Standard to msysgit
13+
*.doc diff=astextplain
14+
*.DOC diff=astextplain
15+
*.docx diff=astextplain
16+
*.DOCX diff=astextplain
17+
*.dot diff=astextplain
18+
*.DOT diff=astextplain
19+
*.pdf diff=astextplain
20+
*.PDF diff=astextplain
21+
*.rtf diff=astextplain
22+
*.RTF diff=astextplain

0 commit comments

Comments
 (0)