committed by
GitHub
240 changed files with 5648 additions and 1765 deletions
@ -0,0 +1,181 @@ |
|||
name: Output API Diff |
|||
|
|||
on: |
|||
issue_comment: |
|||
types: [created] |
|||
|
|||
permissions: {} |
|||
|
|||
concurrency: |
|||
group: api-diff-${{ github.event.issue.number }} |
|||
cancel-in-progress: true |
|||
|
|||
jobs: |
|||
api-diff: |
|||
name: Output API Diff |
|||
if: >- |
|||
github.event.issue.pull_request |
|||
&& contains(github.event.comment.body, '/api-diff') |
|||
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) |
|||
runs-on: ubuntu-latest |
|||
|
|||
permissions: |
|||
contents: read |
|||
pull-requests: write |
|||
|
|||
steps: |
|||
- name: Check maintainer permission |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const { data: permLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
username: context.payload.comment.user.login, |
|||
}); |
|||
const allowed = ['admin', 'maintain', 'write']; |
|||
if (!allowed.includes(permLevel.permission)) { |
|||
core.setFailed(`User @${context.payload.comment.user.login} does not have write access.`); |
|||
} |
|||
|
|||
- name: Add reaction to acknowledge command |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: 'eyes', |
|||
}); |
|||
|
|||
- name: Get PR branch info |
|||
id: pr |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const { data: pr } = await github.rest.pulls.get({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
pull_number: context.issue.number, |
|||
}); |
|||
if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) { |
|||
core.setFailed('Cannot run /api-diff on fork PRs — would execute untrusted code.'); |
|||
return; |
|||
} |
|||
core.setOutput('ref', pr.head.ref); |
|||
core.setOutput('sha', pr.head.sha); |
|||
|
|||
- name: Checkout PR branch |
|||
uses: actions/checkout@v4 |
|||
with: |
|||
ref: ${{ steps.pr.outputs.sha }} |
|||
submodules: recursive |
|||
|
|||
- name: Setup .NET |
|||
uses: actions/setup-dotnet@v4 |
|||
with: |
|||
global-json-file: global.json |
|||
|
|||
- name: Run OutputApiDiff |
|||
run: dotnet run --project ./nukebuild/_build.csproj -- OutputApiDiff |
|||
|
|||
- name: Post API diff as PR comment |
|||
if: always() && steps.pr.outcome == 'success' |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const fs = require('fs'); |
|||
const path = require('path'); |
|||
|
|||
const diffDir = path.join(process.env.GITHUB_WORKSPACE, 'artifacts', 'api-diff', 'markdown'); |
|||
const mergedPath = path.join(diffDir, '_diff.md'); |
|||
|
|||
let body; |
|||
if (fs.existsSync(mergedPath)) { |
|||
let diff = fs.readFileSync(mergedPath, 'utf8').trim(); |
|||
if (!diff || diff.toLowerCase().includes('no changes')) { |
|||
body = '### API Diff\n\n✅ No public API changes detected in this PR.'; |
|||
} else { |
|||
const MAX_COMMENT_LENGTH = 60000; // GitHub comment limit is 65536 |
|||
const header = '### API Diff\n\n'; |
|||
const footer = '\n\n---\n_Generated by `/api-diff` command._'; |
|||
const budget = MAX_COMMENT_LENGTH - header.length - footer.length; |
|||
|
|||
if (diff.length > budget) { |
|||
diff = diff.substring(0, budget) + '\n\n> ⚠️ Output truncated. See the [full workflow run](' + |
|||
`${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + |
|||
') for complete diff.'; |
|||
} |
|||
|
|||
body = header + diff + footer; |
|||
} |
|||
} else { |
|||
body = '### API Diff\n\n⚠️ No diff output was produced. Check the [workflow run](' + |
|||
`${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}` + |
|||
') for details.'; |
|||
} |
|||
|
|||
// Collapse into <details> if large |
|||
if (body.length > 2000) { |
|||
const inner = body; |
|||
body = '<details>\n<summary>📋 API Diff (click to expand)</summary>\n\n' + inner + '\n\n</details>'; |
|||
} |
|||
|
|||
// Update existing bot comment or create a new one |
|||
const marker = '<!-- api-diff-bot -->'; |
|||
body = marker + '\n' + body; |
|||
|
|||
const { data: comments } = await github.rest.issues.listComments({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
issue_number: context.issue.number, |
|||
per_page: 100, |
|||
}); |
|||
const existing = comments.find(c => c.body?.includes(marker)); |
|||
|
|||
if (existing) { |
|||
await github.rest.issues.updateComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: existing.id, |
|||
body, |
|||
}); |
|||
} else { |
|||
await github.rest.issues.createComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
issue_number: context.issue.number, |
|||
body, |
|||
}); |
|||
} |
|||
|
|||
- name: Add success reaction |
|||
if: success() |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: 'rocket', |
|||
}); |
|||
|
|||
- name: Report failure |
|||
if: failure() |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: '-1', |
|||
}); |
|||
await github.rest.issues.createComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
issue_number: context.issue.number, |
|||
body: `❌ \`/api-diff\` failed. [See logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`, |
|||
}); |
|||
@ -0,0 +1,124 @@ |
|||
name: Update API Suppressions |
|||
|
|||
on: |
|||
issue_comment: |
|||
types: [created] |
|||
|
|||
permissions: {} |
|||
|
|||
concurrency: |
|||
group: update-api-${{ github.event.issue.number }} |
|||
cancel-in-progress: true |
|||
|
|||
jobs: |
|||
update-api: |
|||
name: Update API Suppressions |
|||
if: >- |
|||
github.event.issue.pull_request |
|||
&& contains(github.event.comment.body, '/update-api') |
|||
&& contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association) |
|||
runs-on: ubuntu-latest |
|||
|
|||
permissions: |
|||
contents: write |
|||
pull-requests: write |
|||
|
|||
steps: |
|||
- name: Check maintainer permission |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const { data: permLevel } = await github.rest.repos.getCollaboratorPermissionLevel({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
username: context.payload.comment.user.login, |
|||
}); |
|||
const allowed = ['admin', 'maintain', 'write']; |
|||
if (!allowed.includes(permLevel.permission)) { |
|||
core.setFailed(`User @${context.payload.comment.user.login} does not have write access.`); |
|||
} |
|||
|
|||
- name: Add reaction to acknowledge command |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: 'eyes', |
|||
}); |
|||
|
|||
- name: Get PR branch info |
|||
id: pr |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
const { data: pr } = await github.rest.pulls.get({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
pull_number: context.issue.number, |
|||
}); |
|||
if (pr.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`) { |
|||
core.setFailed('Cannot run /update-api on fork PRs — would execute untrusted code with write permissions.'); |
|||
return; |
|||
} |
|||
core.setOutput('ref', pr.head.ref); |
|||
core.setOutput('sha', pr.head.sha); |
|||
|
|||
- name: Checkout PR branch |
|||
uses: actions/checkout@v4 |
|||
with: |
|||
ref: ${{ steps.pr.outputs.sha }} |
|||
token: ${{ secrets.GITHUB_TOKEN }} |
|||
submodules: recursive |
|||
|
|||
- name: Setup .NET |
|||
uses: actions/setup-dotnet@v4 |
|||
with: |
|||
global-json-file: global.json |
|||
|
|||
- name: Run ValidateApiDiff |
|||
run: dotnet run --project ./nukebuild/_build.csproj -- ValidateApiDiff --update-api-suppression true |
|||
|
|||
- name: Commit and push changes |
|||
run: | |
|||
git config user.name "github-actions[bot]" |
|||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" |
|||
git add api/ |
|||
if git diff --cached --quiet; then |
|||
echo "No API suppression changes to commit." |
|||
else |
|||
git commit -m "Update API suppressions" |
|||
git push origin HEAD:${{ steps.pr.outputs.ref }} |
|||
fi |
|||
|
|||
- name: Add success reaction |
|||
if: success() |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: 'rocket', |
|||
}); |
|||
|
|||
- name: Report failure |
|||
if: failure() |
|||
uses: actions/github-script@v7 |
|||
with: |
|||
script: | |
|||
await github.rest.reactions.createForIssueComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
comment_id: context.payload.comment.id, |
|||
content: '-1', |
|||
}); |
|||
await github.rest.issues.createComment({ |
|||
owner: context.repo.owner, |
|||
repo: context.repo.repo, |
|||
issue_number: context.issue.number, |
|||
body: `❌ \`/update-api\` failed. [See logs](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`, |
|||
}); |
|||
@ -0,0 +1,50 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.PipsPagerCarouselPage"> |
|||
<DockPanel> |
|||
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
|||
<StackPanel Margin="12" Spacing="8"> |
|||
<TextBlock Text="Carousel Integration" FontSize="16" FontWeight="SemiBold" |
|||
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
|||
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
|||
Text="Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation." /> |
|||
<Separator /> |
|||
<TextBlock Text="Binding" FontSize="13" FontWeight="SemiBold" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" |
|||
Text="SelectedIndex='{Binding #Pager.SelectedPageIndex, Mode=TwoWay}'" /> |
|||
</StackPanel> |
|||
</ScrollViewer> |
|||
|
|||
<Border DockPanel.Dock="Right" Width="1" |
|||
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
|||
|
|||
<Grid RowDefinitions="*,Auto" Margin="24"> |
|||
<Carousel Name="GalleryCarousel" |
|||
SelectedIndex="{Binding #GalleryPager.SelectedPageIndex, Mode=TwoWay}"> |
|||
<Carousel.Items> |
|||
<Border Background="#E3F2FD" CornerRadius="8"> |
|||
<TextBlock Text="Page 1" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
|||
</Border> |
|||
<Border Background="#C8E6C9" CornerRadius="8"> |
|||
<TextBlock Text="Page 2" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
|||
</Border> |
|||
<Border Background="#FFE0B2" CornerRadius="8"> |
|||
<TextBlock Text="Page 3" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
|||
</Border> |
|||
<Border Background="#E1BEE7" CornerRadius="8"> |
|||
<TextBlock Text="Page 4" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
|||
</Border> |
|||
<Border Background="#FFCDD2" CornerRadius="8"> |
|||
<TextBlock Text="Page 5" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
|||
</Border> |
|||
</Carousel.Items> |
|||
</Carousel> |
|||
|
|||
<PipsPager Name="GalleryPager" |
|||
Grid.Row="1" |
|||
NumberOfPages="5" |
|||
HorizontalAlignment="Center" |
|||
Margin="0,12,0,0" /> |
|||
</Grid> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class PipsPagerCarouselPage : UserControl |
|||
{ |
|||
public PipsPagerCarouselPage() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.PipsPagerCustomButtonsPage"> |
|||
<DockPanel> |
|||
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
|||
<StackPanel Margin="12" Spacing="8"> |
|||
<TextBlock Text="Custom Buttons" FontSize="16" FontWeight="SemiBold" |
|||
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
|||
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
|||
Text="Replace the default chevron navigation buttons with custom styled buttons using PreviousButtonStyle and NextButtonStyle." /> |
|||
<Separator /> |
|||
<TextBlock Text="Properties" FontSize="13" FontWeight="SemiBold" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="PreviousButtonStyle" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="NextButtonStyle" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="IsPreviousButtonVisible" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="IsNextButtonVisible" /> |
|||
</StackPanel> |
|||
</ScrollViewer> |
|||
|
|||
<Border DockPanel.Dock="Right" Width="1" |
|||
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
|||
|
|||
<StackPanel Spacing="24" Margin="24"> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Text Buttons" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="5" MaxVisiblePips="5" |
|||
IsPreviousButtonVisible="True" IsNextButtonVisible="True"> |
|||
<PipsPager.Resources> |
|||
<ControlTheme x:Key="CustomPreviousButtonStyle" TargetType="Button"> |
|||
<Setter Property="Content" Value="Prev" /> |
|||
<Setter Property="Background" Value="LightGray" /> |
|||
<Setter Property="Foreground" Value="Black" /> |
|||
<Setter Property="Padding" Value="8,2" /> |
|||
<Setter Property="Margin" Value="0,0,8,0" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border Background="{TemplateBinding Background}" CornerRadius="4"> |
|||
<ContentPresenter Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" /> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</ControlTheme> |
|||
<ControlTheme x:Key="CustomNextButtonStyle" TargetType="Button"> |
|||
<Setter Property="Content" Value="Next" /> |
|||
<Setter Property="Background" Value="LightGray" /> |
|||
<Setter Property="Foreground" Value="Black" /> |
|||
<Setter Property="Padding" Value="8,2" /> |
|||
<Setter Property="Margin" Value="8,0,0,0" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border Background="{TemplateBinding Background}" CornerRadius="4"> |
|||
<ContentPresenter Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" /> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</ControlTheme> |
|||
</PipsPager.Resources> |
|||
<PipsPager.PreviousButtonStyle> |
|||
<StaticResource ResourceKey="CustomPreviousButtonStyle" /> |
|||
</PipsPager.PreviousButtonStyle> |
|||
<PipsPager.NextButtonStyle> |
|||
<StaticResource ResourceKey="CustomNextButtonStyle" /> |
|||
</PipsPager.NextButtonStyle> |
|||
</PipsPager> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Hidden Buttons" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="7" |
|||
MaxVisiblePips="7" |
|||
IsPreviousButtonVisible="False" |
|||
IsNextButtonVisible="False" /> |
|||
</StackPanel> |
|||
|
|||
</StackPanel> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class PipsPagerCustomButtonsPage : UserControl |
|||
{ |
|||
public PipsPagerCustomButtonsPage() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,59 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.PipsPagerCustomColorsPage"> |
|||
<DockPanel> |
|||
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
|||
<StackPanel Margin="12" Spacing="8"> |
|||
<TextBlock Text="Custom Colors" FontSize="16" FontWeight="SemiBold" |
|||
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
|||
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
|||
Text="Override pip indicator colors using resource keys." /> |
|||
<Separator /> |
|||
<TextBlock Text="Resource Keys" FontSize="13" FontWeight="SemiBold" /> |
|||
<TextBlock FontSize="11" TextWrapping="Wrap" Text="PipsPagerSelectionIndicatorForeground" /> |
|||
<TextBlock FontSize="11" TextWrapping="Wrap" Text="PipsPagerSelectionIndicatorForegroundSelected" /> |
|||
<TextBlock FontSize="11" TextWrapping="Wrap" Text="PipsPagerSelectionIndicatorForegroundPointerOver" /> |
|||
</StackPanel> |
|||
</ScrollViewer> |
|||
|
|||
<Border DockPanel.Dock="Right" Width="1" |
|||
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
|||
|
|||
<StackPanel Spacing="24" Margin="24"> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Orange / Blue" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="5" MaxVisiblePips="5"> |
|||
<PipsPager.Resources> |
|||
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="Orange" /> |
|||
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="Blue" /> |
|||
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="Gold" /> |
|||
</PipsPager.Resources> |
|||
</PipsPager> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Green / Red" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="5" MaxVisiblePips="5"> |
|||
<PipsPager.Resources> |
|||
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#81C784" /> |
|||
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#E53935" /> |
|||
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#A5D6A7" /> |
|||
</PipsPager.Resources> |
|||
</PipsPager> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Purple / Teal" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="5" MaxVisiblePips="5"> |
|||
<PipsPager.Resources> |
|||
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#CE93D8" /> |
|||
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#00897B" /> |
|||
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#BA68C8" /> |
|||
</PipsPager.Resources> |
|||
</PipsPager> |
|||
</StackPanel> |
|||
|
|||
</StackPanel> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class PipsPagerCustomColorsPage : UserControl |
|||
{ |
|||
public PipsPagerCustomColorsPage() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,197 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.PipsPagerCustomTemplatesPage"> |
|||
<DockPanel> |
|||
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
|||
<StackPanel Margin="12" Spacing="8"> |
|||
<TextBlock Text="Custom Templates" FontSize="16" FontWeight="SemiBold" |
|||
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
|||
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
|||
Text="Override pip item templates using Style selectors targeting the inner ListBoxItem to create squares, pills, numbers, or any custom shape." /> |
|||
<Separator /> |
|||
<TextBlock Text="Technique" FontSize="13" FontWeight="SemiBold" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" |
|||
Text="Target: PipsPager /template/ ListBox ListBoxItem" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" |
|||
Text="States: :selected, :pointerover, :pressed" /> |
|||
</StackPanel> |
|||
</ScrollViewer> |
|||
|
|||
<Border DockPanel.Dock="Right" Width="1" |
|||
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
|||
|
|||
<StackPanel Spacing="32" Margin="24"> |
|||
|
|||
<!-- Squares --> |
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Squares" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="5"> |
|||
<PipsPager.Styles> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid Background="Transparent"> |
|||
<Rectangle Name="Pip" |
|||
Width="4" Height="4" |
|||
HorizontalAlignment="Center" VerticalAlignment="Center" |
|||
Fill="{DynamicResource PipsPagerSelectionIndicatorForeground}"> |
|||
<Rectangle.Transitions> |
|||
<Transitions> |
|||
<DoubleTransition Property="Width" Duration="0:0:0.167" /> |
|||
<DoubleTransition Property="Height" Duration="0:0:0.167" /> |
|||
<BrushTransition Property="Fill" Duration="0:0:0.167" /> |
|||
</Transitions> |
|||
</Rectangle.Transitions> |
|||
</Rectangle> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Rectangle#Pip"> |
|||
<Setter Property="Width" Value="6" /> |
|||
<Setter Property="Height" Value="6" /> |
|||
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundPointerOver}" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Rectangle#Pip"> |
|||
<Setter Property="Width" Value="6" /> |
|||
<Setter Property="Height" Value="6" /> |
|||
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundSelected}" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Rectangle#Pip"> |
|||
<Setter Property="Width" Value="6" /> |
|||
<Setter Property="Height" Value="6" /> |
|||
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundSelected}" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Rectangle#Pip"> |
|||
<Setter Property="Width" Value="4" /> |
|||
<Setter Property="Height" Value="4" /> |
|||
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundPressed}" /> |
|||
</Style> |
|||
</PipsPager.Styles> |
|||
</PipsPager> |
|||
</StackPanel> |
|||
|
|||
<!-- Pill-shaped --> |
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Pill-shaped Selected" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="5" |
|||
IsPreviousButtonVisible="False" |
|||
IsNextButtonVisible="False"> |
|||
<PipsPager.Styles> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem"> |
|||
<Setter Property="Width" Value="24" /> |
|||
<Setter Property="Height" Value="24" /> |
|||
<Setter Property="Padding" Value="0" /> |
|||
<Setter Property="Margin" Value="2,0" /> |
|||
<Setter Property="MinWidth" Value="0" /> |
|||
<Setter Property="MinHeight" Value="0" /> |
|||
<Setter Property="ClipToBounds" Value="False" /> |
|||
<Setter Property="VerticalAlignment" Value="Center" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid Background="Transparent"> |
|||
<Border Name="Pip" |
|||
Width="8" Height="8" CornerRadius="4" |
|||
HorizontalAlignment="Center" VerticalAlignment="Center" |
|||
Background="#C0C0C0"> |
|||
<Border.Transitions> |
|||
<Transitions> |
|||
<DoubleTransition Property="Width" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
|||
<DoubleTransition Property="Height" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
|||
<CornerRadiusTransition Property="CornerRadius" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
|||
<BrushTransition Property="Background" Duration="0:0:0.2" /> |
|||
</Transitions> |
|||
</Border.Transitions> |
|||
</Border> |
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Border#Pip"> |
|||
<Setter Property="Width" Value="10" /> |
|||
<Setter Property="Height" Value="10" /> |
|||
<Setter Property="CornerRadius" Value="5" /> |
|||
<Setter Property="Background" Value="#909090" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Border#Pip"> |
|||
<Setter Property="Width" Value="24" /> |
|||
<Setter Property="Height" Value="8" /> |
|||
<Setter Property="CornerRadius" Value="4" /> |
|||
<Setter Property="Background" Value="#FF6B35" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Border#Pip"> |
|||
<Setter Property="Width" Value="24" /> |
|||
<Setter Property="Height" Value="8" /> |
|||
<Setter Property="CornerRadius" Value="4" /> |
|||
<Setter Property="Background" Value="#E85A2A" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Border#Pip"> |
|||
<Setter Property="Width" Value="8" /> |
|||
<Setter Property="Height" Value="8" /> |
|||
<Setter Property="Background" Value="#707070" /> |
|||
</Style> |
|||
</PipsPager.Styles> |
|||
</PipsPager> |
|||
</StackPanel> |
|||
|
|||
<!-- Numbers --> |
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Numbers" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="10" |
|||
MaxVisiblePips="4" |
|||
IsNextButtonVisible="True" |
|||
IsPreviousButtonVisible="True" |
|||
ClipToBounds="False"> |
|||
<PipsPager.Styles> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem"> |
|||
<Setter Property="Width" Value="44" /> |
|||
<Setter Property="Height" Value="44" /> |
|||
<Setter Property="Padding" Value="0" /> |
|||
<Setter Property="Margin" Value="2" /> |
|||
<Setter Property="ClipToBounds" Value="False" /> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Border Name="PipBorder" |
|||
Width="30" Height="30" |
|||
VerticalAlignment="Center" HorizontalAlignment="Center" |
|||
CornerRadius="10" ClipToBounds="False" |
|||
Background="LightGray"> |
|||
<Border.Transitions> |
|||
<Transitions> |
|||
<DoubleTransition Property="Width" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
|||
<DoubleTransition Property="Height" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
|||
<BrushTransition Property="Background" Duration="0:0:0.2" /> |
|||
</Transitions> |
|||
</Border.Transitions> |
|||
<TextBlock Text="{TemplateBinding Content}" |
|||
VerticalAlignment="Center" HorizontalAlignment="Center" |
|||
Foreground="Black" FontWeight="SemiBold" /> |
|||
</Border> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Border#PipBorder"> |
|||
<Setter Property="Background" Value="DarkGray" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Border#PipBorder"> |
|||
<Setter Property="Width" Value="28" /> |
|||
<Setter Property="Height" Value="28" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Border#PipBorder"> |
|||
<Setter Property="Background" Value="#0078D7" /> |
|||
<Setter Property="Width" Value="36" /> |
|||
<Setter Property="Height" Value="36" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ TextBlock"> |
|||
<Setter Property="Foreground" Value="White" /> |
|||
</Style> |
|||
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Border#PipBorder"> |
|||
<Setter Property="Background" Value="#106EBE" /> |
|||
</Style> |
|||
</PipsPager.Styles> |
|||
</PipsPager> |
|||
</StackPanel> |
|||
|
|||
</StackPanel> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class PipsPagerCustomTemplatesPage : UserControl |
|||
{ |
|||
public PipsPagerCustomTemplatesPage() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.PipsPagerEventsPage"> |
|||
<DockPanel> |
|||
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
|||
<StackPanel Margin="12" Spacing="8"> |
|||
<TextBlock Text="Events" FontSize="16" FontWeight="SemiBold" |
|||
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
|||
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
|||
Text="Monitor SelectedPageIndex changes to react to user navigation." /> |
|||
<Separator /> |
|||
<TextBlock Text="Event Log" FontSize="13" FontWeight="SemiBold" /> |
|||
<ItemsControl Name="EventLog"> |
|||
<ItemsControl.ItemTemplate> |
|||
<DataTemplate> |
|||
<TextBlock Text="{Binding}" FontSize="11" TextWrapping="Wrap" /> |
|||
</DataTemplate> |
|||
</ItemsControl.ItemTemplate> |
|||
</ItemsControl> |
|||
</StackPanel> |
|||
</ScrollViewer> |
|||
|
|||
<Border DockPanel.Dock="Right" Width="1" |
|||
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
|||
|
|||
<StackPanel Spacing="16" Margin="24"> |
|||
<TextBlock Text="Tap a pip and watch the event log" FontSize="14" Opacity="0.6" /> |
|||
<PipsPager Name="EventPager" |
|||
NumberOfPages="8" |
|||
MaxVisiblePips="8" /> |
|||
<TextBlock Name="StatusText" FontSize="13" Opacity="0.7" |
|||
Text="Selected: 0" /> |
|||
</StackPanel> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,29 @@ |
|||
using System.Collections.ObjectModel; |
|||
using Avalonia.Controls; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class PipsPagerEventsPage : UserControl |
|||
{ |
|||
private readonly ObservableCollection<string> _events = new(); |
|||
|
|||
public PipsPagerEventsPage() |
|||
{ |
|||
InitializeComponent(); |
|||
|
|||
EventLog.ItemsSource = _events; |
|||
|
|||
EventPager.PropertyChanged += (_, e) => |
|||
{ |
|||
if (e.Property != PipsPager.SelectedPageIndexProperty) |
|||
return; |
|||
|
|||
var newIndex = (int)e.NewValue!; |
|||
StatusText.Text = $"Selected: {newIndex}"; |
|||
_events.Insert(0, $"SelectedPageIndex changed to {newIndex}"); |
|||
|
|||
if (_events.Count > 20) |
|||
_events.RemoveAt(_events.Count - 1); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.PipsPagerGettingStartedPage"> |
|||
<DockPanel> |
|||
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
|||
<StackPanel Margin="12" Spacing="8"> |
|||
<TextBlock Text="Getting Started" FontSize="16" FontWeight="SemiBold" |
|||
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
|||
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
|||
Text="PipsPager lets users navigate a paginated collection using configurable dot indicators with optional navigation buttons." /> |
|||
<Separator /> |
|||
<TextBlock Text="Features" FontSize="13" FontWeight="SemiBold" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Configurable maximum visible pips" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Automatic scrolling for large collections" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Horizontal and Vertical orientation" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Previous/Next navigation buttons" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Customizable pips and buttons" /> |
|||
</StackPanel> |
|||
</ScrollViewer> |
|||
|
|||
<Border DockPanel.Dock="Right" Width="1" |
|||
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
|||
|
|||
<StackPanel Spacing="24" Margin="24"> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Default" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="5" /> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Without Navigation Buttons" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="10" |
|||
IsPreviousButtonVisible="False" |
|||
IsNextButtonVisible="False" |
|||
MaxVisiblePips="5" /> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="Vertical" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="5" Orientation="Vertical" /> |
|||
</StackPanel> |
|||
|
|||
</StackPanel> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class PipsPagerGettingStartedPage : UserControl |
|||
{ |
|||
public PipsPagerGettingStartedPage() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.PipsPagerLargeCollectionPage"> |
|||
<DockPanel> |
|||
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
|||
<StackPanel Margin="12" Spacing="8"> |
|||
<TextBlock Text="Large Collections" FontSize="16" FontWeight="SemiBold" |
|||
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
|||
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
|||
Text="Use MaxVisiblePips to limit visible indicators when the page count is large. The pips scroll automatically to keep the selected pip visible." /> |
|||
<Separator /> |
|||
<TextBlock Text="Properties" FontSize="13" FontWeight="SemiBold" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="NumberOfPages: Total page count" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="MaxVisiblePips: Visible indicator limit" /> |
|||
<TextBlock FontSize="12" TextWrapping="Wrap" Text="SelectedPageIndex: Current selection" /> |
|||
</StackPanel> |
|||
</ScrollViewer> |
|||
|
|||
<Border DockPanel.Dock="Right" Width="1" |
|||
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
|||
|
|||
<StackPanel Spacing="24" Margin="24"> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="50 Pages, MaxVisiblePips=7" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager Name="LargePager" |
|||
NumberOfPages="50" |
|||
MaxVisiblePips="7" |
|||
SelectedPageIndex="25" /> |
|||
<TextBlock HorizontalAlignment="Left" FontSize="12"> |
|||
<Run Text="Selected: " FontWeight="SemiBold" /> |
|||
<Run Text="{Binding #LargePager.SelectedPageIndex}" /> |
|||
<Run Text=" / " /> |
|||
<Run Text="{Binding #LargePager.NumberOfPages}" /> |
|||
</TextBlock> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="20 Pages, MaxVisiblePips=5" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="20" |
|||
MaxVisiblePips="5" /> |
|||
</StackPanel> |
|||
|
|||
<StackPanel Spacing="8"> |
|||
<TextBlock Text="100 Pages, MaxVisiblePips=9" FontWeight="SemiBold" FontSize="14" /> |
|||
<PipsPager NumberOfPages="100" |
|||
MaxVisiblePips="9" /> |
|||
</StackPanel> |
|||
|
|||
</StackPanel> |
|||
</DockPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,11 @@ |
|||
using Avalonia.Controls; |
|||
|
|||
namespace ControlCatalog.Pages; |
|||
|
|||
public partial class PipsPagerLargeCollectionPage : UserControl |
|||
{ |
|||
public PipsPagerLargeCollectionPage() |
|||
{ |
|||
InitializeComponent(); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
x:Class="ControlCatalog.Pages.PipsPagerPage"> |
|||
<NavigationPage x:Name="SampleNav"> |
|||
<NavigationPage.Styles> |
|||
<Style Selector="NavigationPage#SampleNav /template/ Border#PART_NavigationBar"> |
|||
<Setter Property="Background" Value="Transparent" /> |
|||
</Style> |
|||
</NavigationPage.Styles> |
|||
</NavigationPage> |
|||
</UserControl> |
|||
@ -0,0 +1,47 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public partial class PipsPagerPage : UserControl |
|||
{ |
|||
private static readonly (string Group, string Title, string Description, Func<UserControl> Factory)[] Demos = |
|||
{ |
|||
("Getting Started", "First Look", |
|||
"Default PipsPager with horizontal and vertical orientation, with and without navigation buttons.", |
|||
() => new PipsPagerGettingStartedPage()), |
|||
|
|||
("Features", "Carousel Integration", |
|||
"Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation.", |
|||
() => new PipsPagerCarouselPage()), |
|||
("Features", "Large Collections", |
|||
"Use MaxVisiblePips to limit visible indicators when the page count is large. Pips scroll automatically.", |
|||
() => new PipsPagerLargeCollectionPage()), |
|||
("Features", "Events", |
|||
"Monitor SelectedPageIndex changes to react to user navigation.", |
|||
() => new PipsPagerEventsPage()), |
|||
|
|||
("Appearance", "Custom Colors", |
|||
"Override pip indicator colors using resource keys for normal, selected, and hover states.", |
|||
() => new PipsPagerCustomColorsPage()), |
|||
("Appearance", "Custom Buttons", |
|||
"Replace the default chevron navigation buttons with custom styled buttons.", |
|||
() => new PipsPagerCustomButtonsPage()), |
|||
("Appearance", "Custom Templates", |
|||
"Override pip item templates to create squares, pills, numbers, or any custom shape.", |
|||
() => new PipsPagerCustomTemplatesPage()), |
|||
}; |
|||
|
|||
public PipsPagerPage() |
|||
{ |
|||
InitializeComponent(); |
|||
Loaded += OnLoaded; |
|||
} |
|||
|
|||
private async void OnLoaded(object? sender, RoutedEventArgs e) |
|||
{ |
|||
await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); |
|||
} |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace System; |
|||
|
|||
#if !NET6_0_OR_GREATER
|
|||
internal static class CollectionCompatibilityExtensions |
|||
{ |
|||
public static bool Remove<TKey, TValue>( |
|||
this Dictionary<TKey, TValue> o, |
|||
TKey key, |
|||
[MaybeNullWhen(false)] out TValue value) |
|||
where TKey : notnull |
|||
{ |
|||
if (o.TryGetValue(key, out value)) |
|||
return o.Remove(key); |
|||
return false; |
|||
} |
|||
|
|||
public static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> o, TKey key, TValue value) |
|||
where TKey : notnull |
|||
{ |
|||
if (!o.ContainsKey(key)) |
|||
{ |
|||
o.Add(key, value); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
#endif
|
|||
@ -1,122 +0,0 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Reflection; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Compatibility; |
|||
using Avalonia.Platform.Interop; |
|||
|
|||
namespace Avalonia.Compatibility |
|||
{ |
|||
internal class NativeLibraryEx |
|||
{ |
|||
#if NET6_0_OR_GREATER
|
|||
public static IntPtr Load(string dll, Assembly assembly) => NativeLibrary.Load(dll, assembly, null); |
|||
public static IntPtr Load(string dll) => NativeLibrary.Load(dll); |
|||
public static bool TryGetExport(IntPtr handle, string name, out IntPtr address) => |
|||
NativeLibrary.TryGetExport(handle, name, out address); |
|||
#else
|
|||
public static IntPtr Load(string dll, Assembly assembly) => Load(dll); |
|||
public static IntPtr Load(string dll) |
|||
{ |
|||
var handle = DlOpen!(dll); |
|||
if (handle != IntPtr.Zero) |
|||
return handle; |
|||
throw new InvalidOperationException("Unable to load " + dll, DlError!()); |
|||
} |
|||
|
|||
public static bool TryGetExport(IntPtr handle, string name, out IntPtr address) |
|||
{ |
|||
try |
|||
{ |
|||
address = DlSym!(handle, name); |
|||
return address != default; |
|||
} |
|||
catch (Exception) |
|||
{ |
|||
address = default; |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
static NativeLibraryEx() |
|||
{ |
|||
if (OperatingSystemEx.IsWindows()) |
|||
{ |
|||
Win32Imports.Init(); |
|||
} |
|||
else if (OperatingSystemEx.IsLinux() || OperatingSystemEx.IsMacOS()) |
|||
{ |
|||
var buffer = Marshal.AllocHGlobal(0x1000); |
|||
uname(buffer); |
|||
var unixName = Marshal.PtrToStringAnsi(buffer); |
|||
Marshal.FreeHGlobal(buffer); |
|||
if (unixName == "Darwin") |
|||
OsXImports.Init(); |
|||
else |
|||
LinuxImports.Init(); |
|||
} |
|||
} |
|||
|
|||
private static Func<string, IntPtr>? DlOpen; |
|||
private static Func<IntPtr, string, IntPtr>? DlSym; |
|||
private static Func<Exception?>? DlError; |
|||
|
|||
[DllImport("libc")] |
|||
static extern int uname(IntPtr buf); |
|||
|
|||
static class Win32Imports |
|||
{ |
|||
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)] |
|||
private static extern IntPtr GetProcAddress(IntPtr hModule, string procName); |
|||
|
|||
[DllImport("kernel32", EntryPoint = "LoadLibraryW", SetLastError = true, CharSet = CharSet.Unicode)] |
|||
private static extern IntPtr LoadLibrary(string lpszLib); |
|||
|
|||
public static void Init() |
|||
{ |
|||
DlOpen = LoadLibrary; |
|||
DlSym = GetProcAddress; |
|||
DlError = () => new Win32Exception(Marshal.GetLastWin32Error()); |
|||
} |
|||
} |
|||
|
|||
static class LinuxImports |
|||
{ |
|||
[DllImport("libdl.so.2")] |
|||
private static extern IntPtr dlopen(string path, int flags); |
|||
|
|||
[DllImport("libdl.so.2")] |
|||
private static extern IntPtr dlsym(IntPtr handle, string symbol); |
|||
|
|||
[DllImport("libdl.so.2")] |
|||
private static extern IntPtr dlerror(); |
|||
|
|||
public static void Init() |
|||
{ |
|||
DlOpen = s => dlopen(s, 1); |
|||
DlSym = dlsym; |
|||
DlError = () => new InvalidOperationException(Marshal.PtrToStringAnsi(dlerror())); |
|||
} |
|||
} |
|||
|
|||
static class OsXImports |
|||
{ |
|||
[DllImport("/usr/lib/libSystem.dylib")] |
|||
private static extern IntPtr dlopen(string path, int flags); |
|||
|
|||
[DllImport("/usr/lib/libSystem.dylib")] |
|||
private static extern IntPtr dlsym(IntPtr handle, string symbol); |
|||
|
|||
[DllImport("/usr/lib/libSystem.dylib")] |
|||
private static extern IntPtr dlerror(); |
|||
|
|||
public static void Init() |
|||
{ |
|||
DlOpen = s => dlopen(s, 1); |
|||
DlSym = dlsym; |
|||
DlError = () => new InvalidOperationException(Marshal.PtrToStringAnsi(dlerror())); |
|||
} |
|||
} |
|||
#endif
|
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace Avalonia.Compatibility |
|||
{ |
|||
internal sealed class OperatingSystemEx |
|||
{ |
|||
#if NET6_0_OR_GREATER
|
|||
public static bool IsWindows() => OperatingSystem.IsWindows(); |
|||
public static bool IsMacOS() => OperatingSystem.IsMacOS(); |
|||
public static bool IsMacCatalyst() => OperatingSystem.IsMacCatalyst(); |
|||
public static bool IsLinux() => OperatingSystem.IsLinux(); |
|||
public static bool IsFreeBSD() => OperatingSystem.IsFreeBSD(); |
|||
public static bool IsAndroid() => OperatingSystem.IsAndroid(); |
|||
public static bool IsIOS() => OperatingSystem.IsIOS(); |
|||
public static bool IsTvOS() => OperatingSystem.IsTvOS(); |
|||
public static bool IsBrowser() => OperatingSystem.IsBrowser(); |
|||
public static bool IsOSPlatform(string platform) => OperatingSystem.IsOSPlatform(platform); |
|||
#else
|
|||
public static bool IsWindows() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); |
|||
public static bool IsMacOS() => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); |
|||
public static bool IsLinux() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); |
|||
public static bool IsFreeBSD() => false; |
|||
public static bool IsAndroid() => false; |
|||
public static bool IsIOS() => false; |
|||
public static bool IsMacCatalyst() => false; |
|||
public static bool IsTvOS() => false; |
|||
public static bool IsBrowser() => false; |
|||
public static bool IsOSPlatform(string platform) => RuntimeInformation.IsOSPlatform(OSPlatform.Create(platform)); |
|||
#endif
|
|||
} |
|||
} |
|||
@ -1,43 +0,0 @@ |
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
// https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs
|
|||
|
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
// See the LICENSE file in the project root for more information.
|
|||
|
|||
// ReSharper disable once CheckNamespace
|
|||
namespace System.Diagnostics.CodeAnalysis |
|||
{ |
|||
#if !NET7_0_OR_GREATER
|
|||
/// <summary>Specifies the syntax used in a string.</summary>
|
|||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] |
|||
internal sealed class StringSyntaxAttribute : Attribute |
|||
{ |
|||
/// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
|
|||
/// <param name="syntax">The syntax identifier.</param>
|
|||
public StringSyntaxAttribute(string syntax) |
|||
{ |
|||
Syntax = syntax; |
|||
Arguments = Array.Empty<object?>(); |
|||
} |
|||
|
|||
/// <summary>Initializes the <see cref="StringSyntaxAttribute"/> with the identifier of the syntax used.</summary>
|
|||
/// <param name="syntax">The syntax identifier.</param>
|
|||
/// <param name="arguments">Optional arguments associated with the specific syntax employed.</param>
|
|||
public StringSyntaxAttribute(string syntax, params object?[] arguments) |
|||
{ |
|||
Syntax = syntax; |
|||
Arguments = arguments; |
|||
} |
|||
|
|||
/// <summary>Gets the identifier of the syntax used.</summary>
|
|||
public string Syntax { get; } |
|||
|
|||
/// <summary>Optional arguments associated with the specific syntax employed.</summary>
|
|||
public object?[] Arguments { get; } |
|||
|
|||
/// <summary>The syntax identifier for strings containing XML.</summary>
|
|||
public const string Xml = nameof(Xml); |
|||
} |
|||
#endif
|
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
/// <summary>
|
|||
/// Represents the arguments of <see cref="InputElement.GotFocus"/> and <see cref="InputElement.LostFocus"/>.
|
|||
/// </summary>
|
|||
public class FocusChangedEventArgs : RoutedEventArgs, IKeyModifiersEventArgs |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of <see cref="FocusChangedEventArgs"/>.
|
|||
/// </summary>
|
|||
/// <param name="routedEvent">The routed event associated with these event args.</param>
|
|||
public FocusChangedEventArgs(RoutedEvent routedEvent) |
|||
: base(routedEvent) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the element that focus has moved to.
|
|||
/// </summary>
|
|||
public IInputElement? NewFocusedElement { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the element that previously had focus.
|
|||
/// </summary>
|
|||
public IInputElement? OldFocusedElement { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating how the change in focus occurred.
|
|||
/// </summary>
|
|||
public NavigationMethod NavigationMethod { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets any key modifiers active at the time of focus.
|
|||
/// </summary>
|
|||
public KeyModifiers KeyModifiers { get; init; } |
|||
} |
|||
} |
|||
@ -1,24 +0,0 @@ |
|||
using Avalonia.Interactivity; |
|||
|
|||
namespace Avalonia.Input |
|||
{ |
|||
/// <summary>
|
|||
/// Holds arguments for a <see cref="InputElement.GotFocusEvent"/>.
|
|||
/// </summary>
|
|||
public class GotFocusEventArgs : RoutedEventArgs, IKeyModifiersEventArgs |
|||
{ |
|||
public GotFocusEventArgs() : base(InputElement.GotFocusEvent) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets a value indicating how the change in focus occurred.
|
|||
/// </summary>
|
|||
public NavigationMethod NavigationMethod { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets any key modifiers active at the time of focus.
|
|||
/// </summary>
|
|||
public KeyModifiers KeyModifiers { get; init; } |
|||
} |
|||
} |
|||
@ -1,17 +1,38 @@ |
|||
namespace Avalonia.Input.Navigation; |
|||
|
|||
internal class XYFocusOptions |
|||
internal sealed class XYFocusOptions |
|||
{ |
|||
public InputElement? SearchRoot { get; set; } |
|||
public Rect ExclusionRect { get; set; } |
|||
public Rect? FocusHintRectangle { get; set; } |
|||
public Rect? FocusedElementBounds { get; set; } |
|||
public XYFocusNavigationStrategy? NavigationStrategyOverride { get; set; } |
|||
public bool IgnoreClipping { get; set; } = true; |
|||
public bool IgnoreClipping { get; set; } |
|||
public bool IgnoreCone { get; set; } |
|||
public KeyDeviceType? KeyDeviceType { get; set; } |
|||
public bool ConsiderEngagement { get; set; } = true; |
|||
public bool UpdateManifold { get; set; } = true; |
|||
public bool ConsiderEngagement { get; set; } |
|||
public bool UpdateManifold { get; set; } |
|||
public bool UpdateManifoldsFromFocusHintRect { get; set; } |
|||
public bool IgnoreOcclusivity { get; set; } |
|||
|
|||
public XYFocusOptions() |
|||
{ |
|||
Reset(); |
|||
} |
|||
|
|||
internal void Reset() |
|||
{ |
|||
SearchRoot = null; |
|||
ExclusionRect = default; |
|||
FocusHintRectangle = null; |
|||
FocusedElementBounds = null; |
|||
NavigationStrategyOverride = null; |
|||
IgnoreClipping = true; |
|||
IgnoreCone = false; |
|||
KeyDeviceType = null; |
|||
ConsiderEngagement = true; |
|||
UpdateManifold = true; |
|||
UpdateManifoldsFromFocusHintRect = false; |
|||
IgnoreOcclusivity = false; |
|||
} |
|||
} |
|||
|
|||
@ -1,70 +0,0 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media.TextFormatting |
|||
{ |
|||
/// <summary>
|
|||
/// References a portion of a text buffer.
|
|||
/// </summary>
|
|||
public readonly record struct TextRange |
|||
{ |
|||
public TextRange(int start, int length) |
|||
{ |
|||
Start = start; |
|||
Length = length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the start.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The start.
|
|||
/// </value>
|
|||
public int Start { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the length.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The length.
|
|||
/// </value>
|
|||
public int Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the end.
|
|||
/// </summary>
|
|||
/// <value>
|
|||
/// The end.
|
|||
/// </value>
|
|||
public int End => Start + Length - 1; |
|||
|
|||
/// <summary>
|
|||
/// Returns a specified number of contiguous elements from the start of the slice.
|
|||
/// </summary>
|
|||
/// <param name="length">The number of elements to return.</param>
|
|||
/// <returns>A <see cref="TextRange"/> that contains the specified number of elements from the start of this slice.</returns>
|
|||
public TextRange Take(int length) |
|||
{ |
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
return new TextRange(Start, length); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Bypasses a specified number of elements in the slice and then returns the remaining elements.
|
|||
/// </summary>
|
|||
/// <param name="length">The number of elements to skip before returning the remaining elements.</param>
|
|||
/// <returns>A <see cref="TextRange"/> that contains the elements that occur after the specified index in this slice.</returns>
|
|||
public TextRange Skip(int length) |
|||
{ |
|||
if (length > Length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(length)); |
|||
} |
|||
|
|||
return new TextRange(Start + length, Length - length); |
|||
} |
|||
} |
|||
} |
|||
@ -1,10 +1,7 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Avalonia.Rendering |
|||
{ |
|||
internal interface IRenderLoopTask |
|||
{ |
|||
void Render(); |
|||
bool Render(); |
|||
} |
|||
} |
|||
|
|||
@ -1,156 +0,0 @@ |
|||
#if NET6_0_OR_GREATER
|
|||
// In .NET Core, the security context and call context are not supported, however,
|
|||
// the impersonation context and culture would typically flow with the execution context.
|
|||
// See: https://learn.microsoft.com/en-us/dotnet/api/system.threading.executioncontext
|
|||
//
|
|||
// So we can safely use ExecutionContext without worrying about culture flowing issues.
|
|||
#else
|
|||
using System; |
|||
using System.Diagnostics; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Globalization; |
|||
using System.Threading; |
|||
|
|||
namespace Avalonia.Threading; |
|||
|
|||
/// <summary>
|
|||
/// An ExecutionContext that preserves culture information across async operations.
|
|||
/// This is a modernized version that removes legacy compatibility switches and
|
|||
/// includes nullable reference type annotations.
|
|||
/// </summary>
|
|||
internal sealed class CulturePreservingExecutionContext |
|||
{ |
|||
private readonly ExecutionContext _context; |
|||
private CultureAndContext? _cultureAndContext; |
|||
|
|||
private CulturePreservingExecutionContext(ExecutionContext context) |
|||
{ |
|||
_context = context; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Captures the current ExecutionContext and culture information.
|
|||
/// </summary>
|
|||
/// <returns>A new CulturePreservingExecutionContext instance, or null if no context needs to be captured.</returns>
|
|||
public static CulturePreservingExecutionContext? Capture() |
|||
{ |
|||
// ExecutionContext.SuppressFlow had been called.
|
|||
// We expect ExecutionContext.Capture() to return null, so match that behavior and return null.
|
|||
if (ExecutionContext.IsFlowSuppressed()) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var context = ExecutionContext.Capture(); |
|||
if (context == null) |
|||
return null; |
|||
|
|||
return new CulturePreservingExecutionContext(context); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Runs the specified callback in the captured execution context while preserving culture information.
|
|||
/// This method is used for .NET Framework and earlier .NET versions.
|
|||
/// </summary>
|
|||
/// <param name="executionContext">The execution context to run in.</param>
|
|||
/// <param name="callback">The callback to execute.</param>
|
|||
/// <param name="state">The state to pass to the callback.</param>
|
|||
public static void Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, object? state) |
|||
{ |
|||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|||
if (callback == null) |
|||
return; |
|||
|
|||
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|||
if (executionContext == null) |
|||
ThrowNullContext(); |
|||
|
|||
// Save culture information - we will need this to restore just before
|
|||
// the callback is actually invoked from CallbackWrapper.
|
|||
executionContext._cultureAndContext = CultureAndContext.Initialize(callback, state); |
|||
|
|||
try |
|||
{ |
|||
ExecutionContext.Run( |
|||
executionContext._context, |
|||
s_callbackWrapperDelegate, |
|||
executionContext._cultureAndContext); |
|||
} |
|||
finally |
|||
{ |
|||
// Restore culture information - it might have been modified during callback execution.
|
|||
executionContext._cultureAndContext.RestoreCultureInfos(); |
|||
} |
|||
} |
|||
|
|||
[DoesNotReturn] |
|||
private static void ThrowNullContext() |
|||
{ |
|||
throw new InvalidOperationException("ExecutionContext cannot be null."); |
|||
} |
|||
|
|||
private static readonly ContextCallback s_callbackWrapperDelegate = CallbackWrapper; |
|||
|
|||
/// <summary>
|
|||
/// Executes the callback and saves culture values immediately afterwards.
|
|||
/// </summary>
|
|||
/// <param name="obj">Contains the actual callback and state.</param>
|
|||
private static void CallbackWrapper(object? obj) |
|||
{ |
|||
var cultureAndContext = (CultureAndContext)obj!; |
|||
|
|||
// Restore culture information saved during Run()
|
|||
cultureAndContext.RestoreCultureInfos(); |
|||
|
|||
try |
|||
{ |
|||
// Execute the actual callback
|
|||
cultureAndContext.Callback(cultureAndContext.State); |
|||
} |
|||
finally |
|||
{ |
|||
// Save any culture changes that might have occurred during callback execution
|
|||
cultureAndContext.CaptureCultureInfos(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Helper class to manage culture information across execution contexts.
|
|||
/// </summary>
|
|||
private sealed class CultureAndContext |
|||
{ |
|||
public ContextCallback Callback { get; } |
|||
public object? State { get; } |
|||
|
|||
private CultureInfo? _culture; |
|||
private CultureInfo? _uiCulture; |
|||
|
|||
private CultureAndContext(ContextCallback callback, object? state) |
|||
{ |
|||
Callback = callback; |
|||
State = state; |
|||
CaptureCultureInfos(); |
|||
} |
|||
|
|||
public static CultureAndContext Initialize(ContextCallback callback, object? state) |
|||
{ |
|||
return new CultureAndContext(callback, state); |
|||
} |
|||
|
|||
public void CaptureCultureInfos() |
|||
{ |
|||
_culture = Thread.CurrentThread.CurrentCulture; |
|||
_uiCulture = Thread.CurrentThread.CurrentUICulture; |
|||
} |
|||
|
|||
public void RestoreCultureInfos() |
|||
{ |
|||
if (_culture != null) |
|||
Thread.CurrentThread.CurrentCulture = _culture; |
|||
|
|||
if (_uiCulture != null) |
|||
Thread.CurrentThread.CurrentUICulture = _uiCulture; |
|||
} |
|||
} |
|||
} |
|||
#endif
|
|||
@ -1,34 +0,0 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
internal class EnumHelper |
|||
{ |
|||
#if NET6_0_OR_GREATER
|
|||
public static T Parse<T>(ReadOnlySpan<char> key, bool ignoreCase) where T : struct |
|||
{ |
|||
return Enum.Parse<T>(key, ignoreCase); |
|||
} |
|||
|
|||
public static bool TryParse<T>(ReadOnlySpan<char> key, bool ignoreCase, out T result) where T : struct |
|||
{ |
|||
return Enum.TryParse(key, ignoreCase, out result); |
|||
} |
|||
#else
|
|||
public static T Parse<T>(string key, bool ignoreCase) where T : struct |
|||
{ |
|||
return (T)Enum.Parse(typeof(T), key, ignoreCase); |
|||
} |
|||
|
|||
public static bool TryParse<T>(string key, bool ignoreCase, out T result) where T : struct |
|||
{ |
|||
return Enum.TryParse(key, ignoreCase, out result); |
|||
} |
|||
|
|||
public static bool TryParse<T>(ReadOnlySpan<char> key, bool ignoreCase, out T result) where T : struct |
|||
{ |
|||
return Enum.TryParse(key.ToString(), ignoreCase, out result); |
|||
} |
|||
#endif
|
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue