committed by
GitHub
889 changed files with 52861 additions and 6104 deletions
@ -1,3 +1,16 @@ |
|||||
|
|
||||
When running code review, please refer to the following documents if they are relevant to the changed parts of the codebase: |
When running code review, please refer to the following documents if they are relevant to the changed parts of the codebase: |
||||
- `native/Avalonia.Native/**` -> `native/Avalonia.Native/README.md` |
- `native/Avalonia.Native/**` -> `native/Avalonia.Native/README.md` |
||||
|
- `src/Avalonia.Wayland/**` -> `src/Avalonia.Wayland/README.md` |
||||
|
|
||||
|
## SBOM (EU Cyber Resilience Act) |
||||
|
|
||||
|
We generate a CycloneDX SBOM per published NuGet package (`nukebuild/SbomGenerator.cs`, `CreateSbom` target) so that every shipped package carries an accurate bill of materials, as required for EU CRA compliance. When reviewing, flag the following: |
||||
|
|
||||
|
- **New shipped package:** if a change adds a new NuGet package that is published to end users (a new packable project, or a new final package in `nukebuild/numerge.json`), it must be covered by SBOM generation. Confirm `CreateSbom` produces an SBOM for it, and call it out if the package would ship without one. |
||||
|
- **New kinds of shipped dependencies:** if a change alters what dependencies are delivered with a component in a way the existing scan doesn't already understand, `nukebuild/SbomGenerator.cs` must be updated so those dependencies appear in the SBOM. In particular: |
||||
|
- Adding npm/JS dependencies to a component that was previously .NET-only (e.g. a new `webapp` bundled into a package), or changing how existing bundled JS is built. |
||||
|
- Bundling third-party binaries directly into a package (copy-local/ILRepack/embedded assemblies) rather than referencing them as normal NuGet dependencies. |
||||
|
- New Numerge merge groups, or any other mechanism that folds a project's shipped output into a package other than via ordinary `PackageReference`/`ProjectReference`. |
||||
|
|
||||
|
The scope is what is *delivered* with the package (BSI TR-03183-2 "scope of delivery"), not build/test-only tooling. Purely development-time dependencies that are never shipped do not need to be in the SBOM. |
||||
|
|||||
@ -1 +1 @@ |
|||||
Subproject commit 86f5a26fadb745e95169c3c805ebd9fd7c9b44d2 |
Subproject commit d7e37ca63dc9b13cdc95ca165938d4904fa0eddf |
||||
@ -0,0 +1,740 @@ |
|||||
|
#nullable enable |
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.IO; |
||||
|
using System.IO.Compression; |
||||
|
using System.Linq; |
||||
|
using System.Reflection.Metadata; |
||||
|
using System.Reflection.PortableExecutable; |
||||
|
using System.Security.Cryptography; |
||||
|
using System.Text.Json; |
||||
|
using System.Text.Json.Nodes; |
||||
|
using System.Text.RegularExpressions; |
||||
|
using System.Xml.Linq; |
||||
|
using Nuke.Common.IO; |
||||
|
using Nuke.Common.Tooling; |
||||
|
using static Serilog.Log; |
||||
|
|
||||
|
// Generates one CycloneDX SBOM per published NuGet package, using the already-restored
|
||||
|
// solution (obj/project.assets.json) so component versions are the ones actually resolved
|
||||
|
// into the build, rather than the floating PackageReference ranges GitHub's own dependency
|
||||
|
// graph reports.
|
||||
|
//
|
||||
|
// Numerge (see numerge.json) merges several source projects' packed output into a handful of
|
||||
|
// the final NuGet packages (e.g. "Avalonia" absorbs Avalonia.Base, Avalonia.Controls, the
|
||||
|
// build tasks/analyzers, etc). That merge happens on the built .nupkg files, not via MSBuild
|
||||
|
// ProjectReferences, so it can't be discovered by pointing CycloneDX at a single project with
|
||||
|
// -rs/--recursive. Instead this compares the intermediate and final package sets to work out
|
||||
|
// which source projects were folded into which final package, generates a BOM per constituent
|
||||
|
// project, and unions their components (de-duplicated by purl) into one BOM per final package.
|
||||
|
public static class SbomGenerator |
||||
|
{ |
||||
|
public static void Generate( |
||||
|
Tool cycloneDx, |
||||
|
AbsolutePath rootDirectory, |
||||
|
AbsolutePath nugetRoot, |
||||
|
AbsolutePath nugetIntermediateRoot, |
||||
|
AbsolutePath numergeConfigPath, |
||||
|
AbsolutePath outputDirectory, |
||||
|
string version) |
||||
|
{ |
||||
|
outputDirectory.CreateOrCleanDirectory(); |
||||
|
|
||||
|
// Read each final package's id exactly once here (opening/unzipping a nupkg to parse its
|
||||
|
// nuspec isn't free) and reuse the id->path map when locating each package's own nupkg
|
||||
|
// below, rather than re-scanning every nupkg per final package.
|
||||
|
var finalPackagePathsById = nugetRoot.GlobFiles("*.nupkg") |
||||
|
.GroupBy(p => ReadPackageId((string)p)) |
||||
|
.ToDictionary(g => g.Key, g => g.First()); |
||||
|
var finalPackageIds = finalPackagePathsById.Keys.ToHashSet(); |
||||
|
var intermediatePackageIds = nugetIntermediateRoot.GlobFiles("*.nupkg") |
||||
|
.Select(p => ReadPackageId((string)p)).Distinct(); |
||||
|
|
||||
|
var numerge = Numerge.MergeConfiguration.LoadFile(numergeConfigPath); |
||||
|
var explicitParentByChild = numerge.Packages |
||||
|
.SelectMany(p => p.Merge.Select(c => (Parent: p.Id, Child: c.Id))) |
||||
|
.ToDictionary(x => x.Child, x => x.Parent); |
||||
|
var mergeAllParent = numerge.Packages.FirstOrDefault(p => p.MergeAll)?.Id; |
||||
|
|
||||
|
// Every final package starts out as its own sole constituent; leftover intermediate
|
||||
|
// packages (ones that never shipped standalone) get assigned to whichever final
|
||||
|
// package absorbed them, per numerge.json.
|
||||
|
var constituentProjectIdsByFinalId = finalPackageIds.ToDictionary(id => id, id => new List<string> { id }); |
||||
|
foreach (var id in intermediatePackageIds.Where(id => !finalPackageIds.Contains(id))) |
||||
|
{ |
||||
|
var owner = explicitParentByChild.TryGetValue(id, out var explicitOwner) ? explicitOwner : mergeAllParent; |
||||
|
if (owner is not null && constituentProjectIdsByFinalId.TryGetValue(owner, out var siblings)) |
||||
|
siblings.Add(id); |
||||
|
else |
||||
|
Warning($"SBOM: couldn't determine which published package absorbs intermediate package '{id}' - it will be missing from all generated SBOMs."); |
||||
|
} |
||||
|
|
||||
|
foreach (var (finalId, projectIds) in constituentProjectIdsByFinalId) |
||||
|
GenerateForPackage(cycloneDx, rootDirectory, finalPackagePathsById, outputDirectory, version, finalId, projectIds); |
||||
|
} |
||||
|
|
||||
|
static void GenerateForPackage(Tool cycloneDx, AbsolutePath rootDirectory, |
||||
|
IReadOnlyDictionary<string, AbsolutePath> finalPackagePathsById, |
||||
|
AbsolutePath outputDirectory, string version, string finalId, List<string> projectIds) |
||||
|
{ |
||||
|
JsonObject? merged = null; |
||||
|
var seenComponentKeys = new HashSet<string>(); |
||||
|
var scannedProjectDirs = new List<AbsolutePath>(); |
||||
|
|
||||
|
foreach (var projectId in projectIds) |
||||
|
{ |
||||
|
var project = rootDirectory.GlobFiles($"src/**/{projectId}.csproj") |
||||
|
.Concat(rootDirectory.GlobFiles($"packages/**/{projectId}.csproj")) |
||||
|
.FirstOrDefault(); |
||||
|
if (project is null) |
||||
|
{ |
||||
|
Warning($"SBOM: couldn't locate source project for '{projectId}', skipping it in the SBOM for '{finalId}'."); |
||||
|
continue; |
||||
|
} |
||||
|
scannedProjectDirs.Add(project.Parent); |
||||
|
|
||||
|
var tempBom = outputDirectory / $"_{projectId}.tmp.json"; |
||||
|
cycloneDx( |
||||
|
$"\"{project}\" -o \"{outputDirectory}\" -fn \"{tempBom.Name}\" -F Json -dpr -ed -sn \"{finalId}\" -sv \"{version}\"", |
||||
|
workingDirectory: rootDirectory); |
||||
|
|
||||
|
var doc = JsonNode.Parse(File.ReadAllText(tempBom))!.AsObject(); |
||||
|
File.Delete(tempBom); |
||||
|
|
||||
|
var components = doc["components"]?.AsArray() ?? new JsonArray(); |
||||
|
if (merged is null) |
||||
|
{ |
||||
|
merged = doc; |
||||
|
foreach (var component in components) |
||||
|
seenComponentKeys.Add(ComponentKey(component)); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var target = merged["components"]?.AsArray() ?? (JsonArray)(merged["components"] = new JsonArray()); |
||||
|
foreach (var component in components) |
||||
|
{ |
||||
|
if (seenComponentKeys.Add(ComponentKey(component))) |
||||
|
target.Add(component!.DeepClone()); |
||||
|
} |
||||
|
|
||||
|
// Every constituent project's own -sn/-sv override makes its root component (and
|
||||
|
// therefore its dependency-graph "ref") identical to the final package's, so merging
|
||||
|
// by ref correctly unions all constituents' dependsOn edges onto that shared root
|
||||
|
// instead of silently keeping only the first project's edges.
|
||||
|
MergeDependencyGraph(merged, doc["dependencies"]?.AsArray() ?? new JsonArray()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (merged is null) |
||||
|
{ |
||||
|
Warning($"SBOM: no source projects could be scanned for '{finalId}', no SBOM was generated for it."); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// cyclonedx-dotnet only sees the MSBuild/NuGet graph. Some projects also bundle a
|
||||
|
// Bun/npm-built webapp directly into their published package (e.g. Avalonia.Browser's
|
||||
|
// staticwebassets, Avalonia.DesignerSupport's embedded previewer) - scan those separately
|
||||
|
// so their shipped JS dependencies aren't silently absent from the SBOM.
|
||||
|
var rootRef = merged["metadata"]?["component"]?["bom-ref"]?.GetValue<string>(); |
||||
|
foreach (var projectDir in scannedProjectDirs) |
||||
|
AddNpmComponents(merged, seenComponentKeys, projectDir, rootRef); |
||||
|
|
||||
|
// The final .nupkg carries the authoritative publisher/license/repository metadata and the
|
||||
|
// actual shipped binaries; use it to flesh out the thin root component cyclonedx-dotnet
|
||||
|
// emits and to verify nothing ships that the dependency scan didn't already account for.
|
||||
|
var finalNupkg = finalPackagePathsById.GetValueOrDefault(finalId); |
||||
|
if (finalNupkg is not null) |
||||
|
{ |
||||
|
var nuspec = ReadNuspecMetadata((string)finalNupkg); |
||||
|
EnrichRootComponent(merged, nuspec); |
||||
|
AddPackageContentComponents(merged, seenComponentKeys, (string)finalNupkg, finalId, projectIds, nuspec); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Warning($"SBOM: couldn't find the built .nupkg for '{finalId}' - root metadata and package-content verification were skipped."); |
||||
|
} |
||||
|
|
||||
|
// cyclonedx-dotnet leaves dependsOn edges pointing at packages it excluded as dev
|
||||
|
// dependencies (e.g. analyzers stripped by -ed), which dangle once the component is gone
|
||||
|
// (upstream bug CycloneDX/cyclonedx-dotnet#761, still reproducing in 6.2.0). Drop those so
|
||||
|
// the graph only references components actually present in the SBOM.
|
||||
|
PruneDanglingDependencyEdges(merged); |
||||
|
|
||||
|
var sbomJson = merged.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); |
||||
|
File.WriteAllText(outputDirectory / $"{finalId}.{version}.cdx.json", sbomJson); |
||||
|
|
||||
|
// Embed the SBOM inside the shipped .nupkg so it travels with the package, in addition to
|
||||
|
// the standalone copy written above (which CI publishes as the SBOM artifact) - belt and
|
||||
|
// suspenders: consumers who only ever see the package still get its bill of materials.
|
||||
|
if (finalNupkg is not null) |
||||
|
EmbedSbomInPackage(finalNupkg, sbomJson); |
||||
|
} |
||||
|
|
||||
|
// The path inside the .nupkg where the CycloneDX SBOM is embedded. Mirrors the _manifest/
|
||||
|
// layout Microsoft.Sbom.Targets uses for its SPDX manifest, but keeps CycloneDX's recognised
|
||||
|
// *.cdx.json filename so tools that scan for that pattern still find it once unpacked.
|
||||
|
const string EmbeddedSbomEntryPath = "_manifest/cyclonedx/bom.cdx.json"; |
||||
|
|
||||
|
// Adds the generated SBOM as a new part inside the shipped package. Must run before the package
|
||||
|
// is signed - a NuGet signature covers the whole archive, so adding a part afterwards would
|
||||
|
// invalidate it. That holds here: these packages are signed server-side by nuget.org on push,
|
||||
|
// which happens after this build step.
|
||||
|
static void EmbedSbomInPackage(AbsolutePath nupkgPath, string sbomJson) |
||||
|
{ |
||||
|
using var file = File.Open(nupkgPath, FileMode.Open, FileAccess.ReadWrite); |
||||
|
using var zip = new ZipArchive(file, ZipArchiveMode.Update); |
||||
|
|
||||
|
if (zip.Entries.Any(e => e.FullName.EndsWith(".signature.p7s", StringComparison.OrdinalIgnoreCase))) |
||||
|
{ |
||||
|
Warning($"SBOM: '{nupkgPath.Name}' is already signed - skipping embed so its signature stays valid."); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Re-embedding (e.g. a re-run over the same output) should replace, not stack duplicates.
|
||||
|
zip.GetEntry(EmbeddedSbomEntryPath)?.Delete(); |
||||
|
using (var entryStream = zip.CreateEntry(EmbeddedSbomEntryPath).Open()) |
||||
|
using (var writer = new StreamWriter(entryStream)) |
||||
|
writer.Write(sbomJson); |
||||
|
|
||||
|
EnsureJsonContentTypeRegistered(zip); |
||||
|
} |
||||
|
|
||||
|
// A .nupkg is an OPC package: every part's extension must be declared in [Content_Types].xml or
|
||||
|
// strict OPC readers - including NuGet's own signature verification - reject the package. The
|
||||
|
// SBOM is a .json part, so register that extension before (or as) we add it.
|
||||
|
static void EnsureJsonContentTypeRegistered(ZipArchive zip) |
||||
|
{ |
||||
|
const string contentTypesEntryName = "[Content_Types].xml"; |
||||
|
XNamespace ns = "http://schemas.openxmlformats.org/package/2006/content-types"; |
||||
|
|
||||
|
var entry = zip.GetEntry(contentTypesEntryName); |
||||
|
if (entry is null) |
||||
|
return; // not a well-formed OPC package; don't fabricate one
|
||||
|
|
||||
|
XDocument doc; |
||||
|
using (var read = entry.Open()) |
||||
|
doc = XDocument.Load(read); |
||||
|
|
||||
|
var alreadyRegistered = doc.Root!.Elements(ns + "Default") |
||||
|
.Any(d => string.Equals((string?)d.Attribute("Extension"), "json", StringComparison.OrdinalIgnoreCase)); |
||||
|
if (alreadyRegistered) |
||||
|
return; |
||||
|
|
||||
|
doc.Root.Add(new XElement(ns + "Default", |
||||
|
new XAttribute("Extension", "json"), |
||||
|
new XAttribute("ContentType", "application/json"))); |
||||
|
|
||||
|
entry.Delete(); |
||||
|
using var write = zip.CreateEntry(contentTypesEntryName).Open(); |
||||
|
doc.Save(write); |
||||
|
} |
||||
|
|
||||
|
static void PruneDanglingDependencyEdges(JsonObject merged) |
||||
|
{ |
||||
|
var deps = merged["dependencies"]?.AsArray(); |
||||
|
if (deps is null) |
||||
|
return; |
||||
|
|
||||
|
var known = new HashSet<string>(); |
||||
|
var rootRef = merged["metadata"]?["component"]?["bom-ref"]?.GetValue<string>(); |
||||
|
if (rootRef is not null) |
||||
|
known.Add(rootRef); |
||||
|
foreach (var component in merged["components"]?.AsArray() ?? new JsonArray()) |
||||
|
{ |
||||
|
if (component?["bom-ref"]?.GetValue<string>() is { } bomRef) |
||||
|
known.Add(bomRef); |
||||
|
if (component?["purl"]?.GetValue<string>() is { } purl) |
||||
|
known.Add(purl); |
||||
|
} |
||||
|
|
||||
|
foreach (var node in deps.OfType<JsonObject>()) |
||||
|
{ |
||||
|
var dependsOn = node["dependsOn"]?.AsArray(); |
||||
|
if (dependsOn is null) |
||||
|
continue; |
||||
|
var kept = new JsonArray(); |
||||
|
foreach (var edge in dependsOn) |
||||
|
if (known.Contains(edge!.GetValue<string>())) |
||||
|
kept.Add(edge.GetValue<string>()); |
||||
|
node["dependsOn"] = kept; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static void MergeDependencyGraph(JsonObject target, JsonArray incoming) |
||||
|
{ |
||||
|
var targetDeps = target["dependencies"]?.AsArray() ?? (JsonArray)(target["dependencies"] = new JsonArray()); |
||||
|
var byRef = targetDeps.OfType<JsonObject>().ToDictionary(d => d["ref"]!.GetValue<string>()); |
||||
|
|
||||
|
foreach (var node in incoming.OfType<JsonObject>()) |
||||
|
{ |
||||
|
var nodeRef = node["ref"]!.GetValue<string>(); |
||||
|
var dependsOn = node["dependsOn"]?.AsArray().Select(x => x!.GetValue<string>()) ?? Enumerable.Empty<string>(); |
||||
|
|
||||
|
if (!byRef.TryGetValue(nodeRef, out var existing)) |
||||
|
{ |
||||
|
existing = node.DeepClone().AsObject(); |
||||
|
targetDeps.Add(existing); |
||||
|
byRef[nodeRef] = existing; |
||||
|
} |
||||
|
|
||||
|
var existingDependsOn = existing["dependsOn"]?.AsArray() ?? (JsonArray)(existing["dependsOn"] = new JsonArray()); |
||||
|
var seen = existingDependsOn.Select(x => x!.GetValue<string>()).ToHashSet(); |
||||
|
foreach (var dep in dependsOn) |
||||
|
if (seen.Add(dep)) |
||||
|
existingDependsOn.Add(dep); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Scans <projectDir>/**/webapp/package.json for production "dependencies" (deliberately
|
||||
|
// ignoring devDependencies, which never ship) and adds them - plus their transitive
|
||||
|
// dependencies, walked through the installed node_modules - as fully-formed components:
|
||||
|
// bom-ref, resolved version, license, and dependency-graph edges, matching the shape of the
|
||||
|
// NuGet components cyclonedx-dotnet emits so npm packages aren't second-class SBOM entries.
|
||||
|
// Versions come from the actually-installed node_modules (same rationale as reading
|
||||
|
// project.assets.json rather than trusting floating ranges).
|
||||
|
static void AddNpmComponents(JsonObject merged, HashSet<string> seenComponentKeys, AbsolutePath projectDir, |
||||
|
string? rootRef) |
||||
|
{ |
||||
|
foreach (string packageJsonPath in projectDir.GlobFiles("**/webapp/package.json")) |
||||
|
{ |
||||
|
var packageJson = JsonNode.Parse(File.ReadAllText(packageJsonPath))!.AsObject(); |
||||
|
var nodeModules = ((AbsolutePath)packageJsonPath).Parent / "node_modules"; |
||||
|
var dependencies = packageJson["dependencies"]?.AsObject() ?? new JsonObject(); |
||||
|
|
||||
|
// The webapp is bundled into the shipped package, so its direct production
|
||||
|
// dependencies are direct dependencies of the final NuGet package.
|
||||
|
foreach (var (name, rangeNode) in dependencies) |
||||
|
{ |
||||
|
if (IsTypeOnlyPackage(name)) |
||||
|
continue; |
||||
|
var purl = AddNpmComponentTree(merged, seenComponentKeys, nodeModules, name, |
||||
|
rangeNode!.GetValue<string>(), nodeModules); |
||||
|
if (rootRef is not null) |
||||
|
AddDependsOn(merged, rootRef, purl); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Adds the component for (name, range) and, recursively, everything it depends on, returning
|
||||
|
// its purl. The component/graph node/subtree are materialised only the first time a purl is
|
||||
|
// seen (which also breaks any dependency cycles); repeat encounters just return the purl so
|
||||
|
// the caller can still record its own edge to it.
|
||||
|
static string AddNpmComponentTree(JsonObject merged, HashSet<string> seenComponentKeys, |
||||
|
AbsolutePath topLevelNodeModules, string name, string declaredRange, AbsolutePath parentNodeModules) |
||||
|
{ |
||||
|
var (purl, componentVersion, installedDir) = |
||||
|
ResolveNpmComponent(name, declaredRange, parentNodeModules, topLevelNodeModules); |
||||
|
|
||||
|
if (!seenComponentKeys.Add(purl)) |
||||
|
return purl; |
||||
|
|
||||
|
var installed = installedDir is not null && File.Exists(installedDir / "package.json") |
||||
|
? JsonNode.Parse(File.ReadAllText(installedDir / "package.json"))!.AsObject() |
||||
|
: null; |
||||
|
|
||||
|
var component = new JsonObject |
||||
|
{ |
||||
|
["type"] = "library", |
||||
|
["bom-ref"] = purl, |
||||
|
["name"] = name, |
||||
|
["version"] = componentVersion, |
||||
|
["purl"] = purl |
||||
|
}; |
||||
|
// No hashes: npm's verifiable hashes live in the (binary bun) lockfile, not the installed
|
||||
|
// tree, and a hash of the unpacked directory wouldn't be checkable against a registry.
|
||||
|
var licenses = installed is null ? null : BuildLicenses(installed); |
||||
|
if (licenses is not null) |
||||
|
component["licenses"] = licenses; |
||||
|
|
||||
|
var target = merged["components"]?.AsArray() ?? (JsonArray)(merged["components"] = new JsonArray()); |
||||
|
target.Add(component); |
||||
|
|
||||
|
var node = new JsonObject { ["ref"] = purl, ["dependsOn"] = new JsonArray() }; |
||||
|
(merged["dependencies"]?.AsArray() ?? (JsonArray)(merged["dependencies"] = new JsonArray())).Add(node); |
||||
|
|
||||
|
var childDeps = installed?["dependencies"]?.AsObject() ?? new JsonObject(); |
||||
|
var childNodeModules = installedDir is not null ? installedDir / "node_modules" : parentNodeModules; |
||||
|
var dependsOn = node["dependsOn"]!.AsArray(); |
||||
|
foreach (var (childName, childRange) in childDeps) |
||||
|
{ |
||||
|
if (IsTypeOnlyPackage(childName)) |
||||
|
continue; |
||||
|
var childPurl = AddNpmComponentTree(merged, seenComponentKeys, topLevelNodeModules, childName, |
||||
|
childRange!.GetValue<string>(), childNodeModules); |
||||
|
dependsOn.Add(childPurl); |
||||
|
} |
||||
|
|
||||
|
return purl; |
||||
|
} |
||||
|
|
||||
|
// @types/* packages are TypeScript declaration stubs: esbuild strips them at build time, so
|
||||
|
// they're never part of the shipped bytes and don't belong in a scope-of-delivery SBOM.
|
||||
|
static bool IsTypeOnlyPackage(string name) => name.StartsWith("@types/", StringComparison.Ordinal); |
||||
|
|
||||
|
static void AddDependsOn(JsonObject merged, string fromRef, string toPurl) |
||||
|
{ |
||||
|
var deps = merged["dependencies"]?.AsArray() ?? (JsonArray)(merged["dependencies"] = new JsonArray()); |
||||
|
var node = deps.OfType<JsonObject>().FirstOrDefault(d => d["ref"]?.GetValue<string>() == fromRef); |
||||
|
if (node is null) |
||||
|
{ |
||||
|
node = new JsonObject { ["ref"] = fromRef, ["dependsOn"] = new JsonArray() }; |
||||
|
deps.Add(node); |
||||
|
} |
||||
|
|
||||
|
var dependsOn = node["dependsOn"]?.AsArray() ?? (JsonArray)(node["dependsOn"] = new JsonArray()); |
||||
|
if (!dependsOn.Any(x => x!.GetValue<string>() == toPurl)) |
||||
|
dependsOn.Add(toPurl); |
||||
|
} |
||||
|
|
||||
|
static JsonArray? BuildLicenses(JsonObject installedPackageJson) |
||||
|
{ |
||||
|
// Modern npm: "license" is an SPDX id or expression. Legacy: "license"/"licenses" objects.
|
||||
|
if (installedPackageJson["license"] is JsonValue licenseValue && licenseValue.TryGetValue(out string? spdx)) |
||||
|
{ |
||||
|
var licenses = SpdxToLicenses(spdx); |
||||
|
if (licenses is not null) |
||||
|
return licenses; |
||||
|
} |
||||
|
|
||||
|
var legacy = (installedPackageJson["license"] as JsonObject)?["type"]?.GetValue<string>() |
||||
|
?? (installedPackageJson["licenses"] as JsonArray)?.OfType<JsonObject>() |
||||
|
.FirstOrDefault()?["type"]?.GetValue<string>(); |
||||
|
return legacy is null |
||||
|
? null |
||||
|
: new JsonArray(new JsonObject { ["license"] = new JsonObject { ["name"] = legacy } }); |
||||
|
} |
||||
|
|
||||
|
// Turns an SPDX string into a CycloneDX licenses array: a single license id becomes a
|
||||
|
// {license:{id}} entry, a compound SPDX expression becomes an {expression} entry.
|
||||
|
static JsonArray? SpdxToLicenses(string? spdx) |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(spdx)) |
||||
|
return null; |
||||
|
var isExpression = spdx.IndexOf(" OR ", StringComparison.Ordinal) >= 0 |
||||
|
|| spdx.IndexOf(" AND ", StringComparison.Ordinal) >= 0 |
||||
|
|| spdx.IndexOf(" WITH ", StringComparison.Ordinal) >= 0; |
||||
|
return new JsonArray(isExpression |
||||
|
? new JsonObject { ["expression"] = spdx } |
||||
|
: new JsonObject { ["license"] = new JsonObject { ["id"] = spdx } }); |
||||
|
} |
||||
|
|
||||
|
static (string Purl, string Version, AbsolutePath? InstalledDir) ResolveNpmComponent( |
||||
|
string name, string declaredRange, AbsolutePath parentNodeModules, AbsolutePath topLevelNodeModules) |
||||
|
{ |
||||
|
// npm/bun hoist most packages to the top level but may nest a conflicting version under
|
||||
|
// the depending package, so prefer the nested copy and fall back to the hoisted one.
|
||||
|
var installedDir = new[] { parentNodeModules / name, topLevelNodeModules / name } |
||||
|
.FirstOrDefault(d => File.Exists(d / "package.json")); |
||||
|
|
||||
|
if (declaredRange.StartsWith("github:") || declaredRange.StartsWith("git") || declaredRange.Contains("://")) |
||||
|
{ |
||||
|
if (TryParseGitHubDependency(declaredRange, out var owner, out var repo, out var reference)) |
||||
|
return ($"pkg:github/{owner}/{repo}@{reference}", reference, installedDir); |
||||
|
|
||||
|
// A non-GitHub git/URL specifier (GitLab/Bitbucket, a raw tarball URL, ...). We have no
|
||||
|
// provider-specific purl for it, so degrade to a generic component - preferring the
|
||||
|
// version installed on disk - rather than throwing and failing the whole release over a
|
||||
|
// single dependency we can't classify precisely.
|
||||
|
var resolvedVersion = installedDir is not null |
||||
|
? JsonNode.Parse(File.ReadAllText(installedDir / "package.json"))!["version"]?.GetValue<string>() |
||||
|
: null; |
||||
|
resolvedVersion ??= declaredRange; |
||||
|
Warning($"SBOM: npm dependency '{name}' uses an unrecognised git/URL specifier '{declaredRange}' - recording it as a generic component with version '{resolvedVersion}'."); |
||||
|
return ($"pkg:generic/{EncodeNpmName(name)}@{resolvedVersion}", resolvedVersion, installedDir); |
||||
|
} |
||||
|
|
||||
|
if (installedDir is null) |
||||
|
{ |
||||
|
Warning($"SBOM: npm dependency '{name}' isn't installed near {parentNodeModules} - recording its declared range '{declaredRange}' instead of a resolved version."); |
||||
|
return ($"pkg:npm/{EncodeNpmName(name)}@{declaredRange}", declaredRange, null); |
||||
|
} |
||||
|
|
||||
|
var installedVersion = JsonNode.Parse(File.ReadAllText(installedDir / "package.json"))!["version"]?.GetValue<string>(); |
||||
|
if (installedVersion is null) |
||||
|
{ |
||||
|
// package.json without a "version" is valid for private packages; don't let it NRE.
|
||||
|
Warning($"SBOM: npm dependency '{name}' installed at {installedDir} has no version in its package.json - recording its declared range '{declaredRange}' instead."); |
||||
|
installedVersion = declaredRange; |
||||
|
} |
||||
|
return ($"pkg:npm/{EncodeNpmName(name)}@{installedVersion}", installedVersion, installedDir); |
||||
|
} |
||||
|
|
||||
|
static string EncodeNpmName(string name) => name.StartsWith("@") ? $"%40{name[1..]}" : name; |
||||
|
|
||||
|
static bool TryParseGitHubDependency(string spec, out string owner, out string repo, out string reference) |
||||
|
{ |
||||
|
owner = repo = ""; |
||||
|
var hashIndex = spec.IndexOf('#'); |
||||
|
reference = hashIndex >= 0 ? spec[(hashIndex + 1)..] : "HEAD"; |
||||
|
var withoutRef = hashIndex >= 0 ? spec[..hashIndex] : spec; |
||||
|
|
||||
|
var match = Regex.Match(withoutRef, @"github(?:\.com)?[:/]+([^/]+)/([^/#]+?)(?:\.git)?$"); |
||||
|
if (!match.Success) |
||||
|
return false; |
||||
|
owner = match.Groups[1].Value; |
||||
|
repo = match.Groups[2].Value; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// Prefer purl (or bom-ref) as the identity; both encode version. Fall back to name+version so
|
||||
|
// distinct versions of the same package aren't collapsed into one, and to a fresh GUID when
|
||||
|
// there's nothing identifiable to key on (treating it as unique rather than deduping blindly).
|
||||
|
static string ComponentKey(JsonNode? component) => |
||||
|
component?["purl"]?.GetValue<string>() |
||||
|
?? component?["bom-ref"]?.GetValue<string>() |
||||
|
?? (component?["name"]?.GetValue<string>() is { } name |
||||
|
? $"{name}@{component?["version"]?.GetValue<string>()}" |
||||
|
: Guid.NewGuid().ToString()); |
||||
|
|
||||
|
// Fills in the root component with the publisher, licence, description and repository details
|
||||
|
// from the shipped .nuspec - cyclonedx-dotnet only emits type/name/version, which is far short
|
||||
|
// of the manufacturer/provenance information a CRA-facing SBOM is expected to carry.
|
||||
|
static void EnrichRootComponent(JsonObject merged, NuspecMetadata meta) |
||||
|
{ |
||||
|
var component = merged["metadata"]?["component"]?.AsObject(); |
||||
|
if (component is null) |
||||
|
return; |
||||
|
|
||||
|
// These are shipped libraries, not applications (cyclonedx-dotnet's default type).
|
||||
|
component["type"] = "library"; |
||||
|
component["purl"] = $"pkg:nuget/{meta.Id}@{meta.Version}"; |
||||
|
if (meta.Description is not null) |
||||
|
component["description"] = meta.Description; |
||||
|
if (meta.Copyright is not null) |
||||
|
component["copyright"] = meta.Copyright; |
||||
|
|
||||
|
JsonObject? supplier = null; |
||||
|
if (meta.Authors is not null) |
||||
|
{ |
||||
|
component["publisher"] = meta.Authors; |
||||
|
component["author"] = meta.Authors; |
||||
|
supplier = new JsonObject { ["name"] = meta.Authors }; |
||||
|
if (meta.ProjectUrl is not null) |
||||
|
supplier["url"] = new JsonArray(meta.ProjectUrl); |
||||
|
component["supplier"] = supplier; |
||||
|
} |
||||
|
|
||||
|
var licenses = SpdxToLicenses(meta.LicenseExpression ?? meta.LicenseId); |
||||
|
// A file license has no SPDX id; record it by name rather than emitting an invalid id.
|
||||
|
if (licenses is null && meta.LicenseFile is not null) |
||||
|
licenses = new JsonArray(new JsonObject |
||||
|
{ ["license"] = new JsonObject { ["name"] = Path.GetFileName(meta.LicenseFile) } }); |
||||
|
if (licenses is not null) |
||||
|
component["licenses"] = licenses; |
||||
|
|
||||
|
var externalReferences = new JsonArray(); |
||||
|
if (meta.ProjectUrl is not null) |
||||
|
externalReferences.Add(new JsonObject { ["url"] = meta.ProjectUrl, ["type"] = "website" }); |
||||
|
if (meta.RepositoryUrl is not null) |
||||
|
externalReferences.Add(new JsonObject { ["url"] = meta.RepositoryUrl, ["type"] = "vcs" }); |
||||
|
if (externalReferences.Count > 0) |
||||
|
component["externalReferences"] = externalReferences; |
||||
|
|
||||
|
// Also record the manufacturer at the document level (the entity that supplied the BOM).
|
||||
|
if (supplier is not null && merged["metadata"] is JsonObject metadata) |
||||
|
metadata["supplier"] = supplier.DeepClone(); |
||||
|
} |
||||
|
|
||||
|
// Cross-checks what the package actually ships against the components derived from the
|
||||
|
// dependency graph. Avalonia's own merged modules (Numerge folds several projects' assemblies
|
||||
|
// into one package) are recorded as manufacturer-supplied components with a verifiable SHA-512
|
||||
|
// of the shipped bytes; any third-party binary that no restored dependency accounts for is
|
||||
|
// added and flagged, so a future bundling regression can't silently escape the SBOM.
|
||||
|
//
|
||||
|
// These shipped binaries are *constituents* of the package, not dependencies of it: the package
|
||||
|
// is made up of them (Component C bundles D and E, in CycloneDX's terms), so they're modelled as
|
||||
|
// a CycloneDX "assembly" via a top-level `compositions` entry rather than with a dependsOn edge
|
||||
|
// from the root - "contains" and "depends on" are different relationships, and an assembly does
|
||||
|
// not imply a dependency. They stay flat top-level components (not nested subcomponents) so their
|
||||
|
// per-assembly hashes remain visible to scanners that ignore nested components.
|
||||
|
static void AddPackageContentComponents(JsonObject merged, HashSet<string> seenComponentKeys, |
||||
|
string nupkgPath, string finalId, List<string> constituentProjectIds, NuspecMetadata meta) |
||||
|
{ |
||||
|
var productNames = new HashSet<string>(constituentProjectIds, StringComparer.OrdinalIgnoreCase) { finalId }; |
||||
|
var representedNames = (merged["components"]?.AsArray() ?? new JsonArray()) |
||||
|
.Select(c => c?["name"]?.GetValue<string>()) |
||||
|
.Where(n => n is not null) |
||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase)!; |
||||
|
|
||||
|
var supplier = meta.Authors is null ? null : new JsonObject { ["name"] = meta.Authors }; |
||||
|
var target = merged["components"]?.AsArray() ?? (JsonArray)(merged["components"] = new JsonArray()); |
||||
|
var rootRef = merged["metadata"]?["component"]?["bom-ref"]?.GetValue<string>(); |
||||
|
|
||||
|
// The bundled first-party assemblies share the package's licence; cyclonedx-dotnet only puts
|
||||
|
// it on the root component, so carry it onto them too (same derivation as EnrichRootComponent).
|
||||
|
var productLicenses = SpdxToLicenses(meta.LicenseExpression ?? meta.LicenseId); |
||||
|
if (productLicenses is null && meta.LicenseFile is not null) |
||||
|
productLicenses = new JsonArray(new JsonObject |
||||
|
{ ["license"] = new JsonObject { ["name"] = Path.GetFileName(meta.LicenseFile) } }); |
||||
|
|
||||
|
// bom-refs of every shipped binary added below; declared as a complete assembly at the end.
|
||||
|
var assemblyRefs = new List<string>(); |
||||
|
|
||||
|
using var file = File.Open(nupkgPath, FileMode.Open, FileAccess.Read); |
||||
|
using var zip = new ZipArchive(file, ZipArchiveMode.Read); |
||||
|
foreach (var entry in zip.Entries) |
||||
|
{ |
||||
|
var path = entry.FullName; |
||||
|
// Reference assemblies under ref/ are compile-time surface, not shipped runtime code;
|
||||
|
// the real implementation lives under lib/ and is scanned there.
|
||||
|
if (!IsShippedBinary(path) || path.StartsWith("ref/", StringComparison.OrdinalIgnoreCase)) |
||||
|
continue; |
||||
|
|
||||
|
var bytes = ReadEntry(entry); |
||||
|
var assemblyName = TryReadAssemblyName(bytes, out var assemblyVersion); |
||||
|
var simpleName = assemblyName ?? Path.GetFileNameWithoutExtension(path); |
||||
|
|
||||
|
// Third-party binaries already represented by a NuGet/npm component need no duplicate.
|
||||
|
if (assemblyName is not null && representedNames.Contains(simpleName)) |
||||
|
continue; |
||||
|
|
||||
|
var isProduct = productNames.Contains(simpleName) |
||||
|
|| simpleName.StartsWith("Avalonia.", StringComparison.OrdinalIgnoreCase) |
||||
|
|| simpleName.Equals("Avalonia", StringComparison.OrdinalIgnoreCase); |
||||
|
|
||||
|
// The package's primary assembly is the root component itself - don't list it as its own subcomponent.
|
||||
|
if (isProduct && simpleName.Equals(finalId, StringComparison.OrdinalIgnoreCase)) |
||||
|
continue; |
||||
|
|
||||
|
var version = assemblyVersion ?? meta.Version; |
||||
|
var bomRef = $"binary:{simpleName}@{version}"; |
||||
|
if (!seenComponentKeys.Add(bomRef)) |
||||
|
continue; |
||||
|
|
||||
|
if (!isProduct) |
||||
|
Warning($"SBOM: package '{finalId}' ships '{path}' ({simpleName}) which no restored dependency accounts for - added from package contents, please verify its provenance."); |
||||
|
|
||||
|
var component = new JsonObject |
||||
|
{ |
||||
|
["type"] = "library", |
||||
|
["bom-ref"] = bomRef, |
||||
|
["name"] = simpleName, |
||||
|
["version"] = version, |
||||
|
["scope"] = "required", |
||||
|
["hashes"] = new JsonArray(new JsonObject |
||||
|
{ |
||||
|
["alg"] = "SHA-512", |
||||
|
["content"] = Convert.ToHexString(SHA512.HashData(bytes)) |
||||
|
}), |
||||
|
["properties"] = new JsonArray(new JsonObject |
||||
|
{ |
||||
|
["name"] = "avalonia:packagePath", |
||||
|
["value"] = path |
||||
|
}) |
||||
|
}; |
||||
|
if (isProduct) |
||||
|
{ |
||||
|
if (supplier is not null) |
||||
|
component["supplier"] = supplier.DeepClone(); |
||||
|
if (productLicenses is not null) |
||||
|
component["licenses"] = productLicenses.DeepClone(); |
||||
|
} |
||||
|
target.Add(component); |
||||
|
|
||||
|
assemblyRefs.Add(bomRef); |
||||
|
} |
||||
|
|
||||
|
// Declare the package's assembly: the root component consists of exactly these bundled
|
||||
|
// binaries. We've read every shipped binary out of the .nupkg, so the enumeration is
|
||||
|
// complete (third-party dependencies remain in the dependency graph, as they should).
|
||||
|
if (assemblyRefs.Count > 0) |
||||
|
{ |
||||
|
var assemblies = new JsonArray(); |
||||
|
if (rootRef is not null) |
||||
|
assemblies.Add(rootRef); |
||||
|
foreach (var bomRef in assemblyRefs) |
||||
|
assemblies.Add(bomRef); |
||||
|
|
||||
|
var compositions = merged["compositions"]?.AsArray() |
||||
|
?? (JsonArray)(merged["compositions"] = new JsonArray()); |
||||
|
compositions.Add(new JsonObject |
||||
|
{ |
||||
|
["aggregate"] = "complete", |
||||
|
["assemblies"] = assemblies |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static bool IsShippedBinary(string path) |
||||
|
{ |
||||
|
var ext = Path.GetExtension(path).ToLowerInvariant(); |
||||
|
return ext is ".dll" or ".so" or ".dylib" or ".wasm" or ".node" or ".a"; |
||||
|
} |
||||
|
|
||||
|
static byte[] ReadEntry(ZipArchiveEntry entry) |
||||
|
{ |
||||
|
using var stream = entry.Open(); |
||||
|
using var ms = new MemoryStream(); |
||||
|
stream.CopyTo(ms); |
||||
|
return ms.ToArray(); |
||||
|
} |
||||
|
|
||||
|
// Returns the managed assembly's simple name (and version), or null for native / non-managed binaries.
|
||||
|
static string? TryReadAssemblyName(byte[] bytes, out string? version) |
||||
|
{ |
||||
|
version = null; |
||||
|
try |
||||
|
{ |
||||
|
using var pe = new PEReader(new MemoryStream(bytes)); |
||||
|
if (!pe.HasMetadata) |
||||
|
return null; |
||||
|
var reader = pe.GetMetadataReader(); |
||||
|
if (!reader.IsAssembly) |
||||
|
return null; |
||||
|
var assembly = reader.GetAssemblyDefinition(); |
||||
|
version = assembly.Version.ToString(); |
||||
|
return reader.GetString(assembly.Name); |
||||
|
} |
||||
|
catch (BadImageFormatException) |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
class NuspecMetadata |
||||
|
{ |
||||
|
public string Id = ""; |
||||
|
public string Version = ""; |
||||
|
public string? Authors; |
||||
|
public string? LicenseId; |
||||
|
public string? LicenseExpression; |
||||
|
public string? LicenseFile; |
||||
|
public string? ProjectUrl; |
||||
|
public string? RepositoryUrl; |
||||
|
public string? Description; |
||||
|
public string? Copyright; |
||||
|
} |
||||
|
|
||||
|
static NuspecMetadata ReadNuspecMetadata(string nupkgPath) |
||||
|
{ |
||||
|
using var file = File.Open(nupkgPath, FileMode.Open, FileAccess.Read); |
||||
|
using var zip = new ZipArchive(file, ZipArchiveMode.Read); |
||||
|
var nuspecEntry = zip.Entries.First(e => e.FullName.EndsWith(".nuspec", StringComparison.Ordinal) && e.FullName == e.Name); |
||||
|
using var nuspecStream = nuspecEntry.Open(); |
||||
|
var metadata = XDocument.Load(nuspecStream).Root! |
||||
|
.Elements().First(x => x.Name.LocalName == "metadata"); |
||||
|
|
||||
|
string? Value(string name) => metadata.Elements().FirstOrDefault(x => x.Name.LocalName == name)?.Value; |
||||
|
var license = metadata.Elements().FirstOrDefault(x => x.Name.LocalName == "license"); |
||||
|
var repository = metadata.Elements().FirstOrDefault(x => x.Name.LocalName == "repository"); |
||||
|
|
||||
|
return new NuspecMetadata |
||||
|
{ |
||||
|
Id = Value("id") ?? "", |
||||
|
Version = Value("version") ?? "", |
||||
|
Authors = Value("authors"), |
||||
|
// A nuspec <license> is either type="expression" (an SPDX expression) or type="file"
|
||||
|
// (a path to a bundled licence file); only the former is a valid SPDX id/expression.
|
||||
|
LicenseExpression = license?.Attribute("type")?.Value == "expression" ? license.Value : null, |
||||
|
LicenseFile = license?.Attribute("type")?.Value == "file" ? license.Value : null, |
||||
|
LicenseId = license?.Attribute("type")?.Value is "expression" or "file" ? null : license?.Value, |
||||
|
ProjectUrl = Value("projectUrl"), |
||||
|
RepositoryUrl = repository?.Attribute("url")?.Value, |
||||
|
Description = Value("description"), |
||||
|
Copyright = Value("copyright") |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
public static string ReadPackageId(string nupkgPath) => ReadNuspecMetadata(nupkgPath).Id; |
||||
|
} |
||||
@ -1 +0,0 @@ |
|||||
DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend |
|
||||
@ -1 +0,0 @@ |
|||||
DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend |
|
||||
@ -1,29 +1,57 @@ |
|||||
using System; |
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
|
||||
|
using Avalonia.Controls.Platform; |
||||
using Avalonia.Platform; |
using Avalonia.Platform; |
||||
using Avalonia.Threading; |
|
||||
|
|
||||
using ControlCatalog.Pages; |
using ControlCatalog.Pages; |
||||
|
|
||||
using MonoMac.Foundation; |
|
||||
using MonoMac.WebKit; |
|
||||
|
|
||||
namespace ControlCatalog.Desktop; |
namespace ControlCatalog.Desktop; |
||||
|
|
||||
public class EmbedSampleMac : INativeDemoControl |
public class EmbedSampleMac : INativeDemoControl |
||||
{ |
{ |
||||
|
private static bool? s_webKitLoaded; |
||||
|
|
||||
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
||||
{ |
{ |
||||
// Note: We are using MonoMac for example purposes
|
s_webKitLoaded ??= NativeLibrary.TryLoad("/System/Library/Frameworks/WebKit.framework/WebKit", out _); |
||||
// It shouldn't be used in production apps
|
|
||||
MacHelper.EnsureInitialized(); |
|
||||
|
|
||||
var webView = new WebView(); |
if (s_webKitLoaded is false) |
||||
Dispatcher.UIThread.Post(() => |
|
||||
{ |
{ |
||||
webView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl( |
return createDefault(); |
||||
isSecond ? "https://bing.com" : "https://google.com/"))); |
} |
||||
}); |
|
||||
|
// alloc
|
||||
|
var webView = ObjC.MsgSend(ObjC.GetClass("WKWebView"), ObjC.GetUid("alloc")); |
||||
|
// init
|
||||
|
webView = ObjC.MsgSend(webView, ObjC.GetUid("init")); |
||||
|
|
||||
|
// ns url
|
||||
|
var urlNsString = ObjC.CreateString(isSecond ? "https://bing.com" : "https://google.com/"); |
||||
|
var url = ObjC.MsgSend( |
||||
|
ObjC.GetClass("NSURL"), ObjC.GetUid("URLWithString:"), |
||||
|
urlNsString); |
||||
|
|
||||
|
// ns url request
|
||||
|
var request = ObjC.MsgSend(ObjC.GetClass("NSURLRequest"), ObjC.GetUid("requestWithURL:"), url); |
||||
|
|
||||
|
// load request
|
||||
|
ObjC.MsgSend(webView, ObjC.GetUid("loadRequest:"), request); |
||||
|
|
||||
return new MacOSViewHandle(webView); |
return new MacOSViewHandle(webView); |
||||
} |
} |
||||
|
|
||||
|
private sealed class MacOSViewHandle(IntPtr view) : INativeControlHostDestroyableControlHandle |
||||
|
{ |
||||
|
public IntPtr Handle { get; private set; } = view; |
||||
|
public string HandleDescriptor => "NSView"; |
||||
|
|
||||
|
public void Destroy() |
||||
|
{ |
||||
|
if (Handle != IntPtr.Zero) |
||||
|
{ |
||||
|
ObjC.MsgSend(Handle, ObjC.GetUid("release")); |
||||
|
Handle = IntPtr.Zero; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
|
|||||
@ -1,38 +0,0 @@ |
|||||
using System; |
|
||||
|
|
||||
using Avalonia.Controls.Platform; |
|
||||
using MonoMac.AppKit; |
|
||||
|
|
||||
namespace ControlCatalog.Desktop; |
|
||||
|
|
||||
internal class MacHelper |
|
||||
{ |
|
||||
private static bool _isInitialized; |
|
||||
|
|
||||
public static void EnsureInitialized() |
|
||||
{ |
|
||||
if (_isInitialized) |
|
||||
return; |
|
||||
_isInitialized = true; |
|
||||
NSApplication.Init(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
internal class MacOSViewHandle : INativeControlHostDestroyableControlHandle |
|
||||
{ |
|
||||
private NSView? _view; |
|
||||
|
|
||||
public MacOSViewHandle(NSView view) |
|
||||
{ |
|
||||
_view = view; |
|
||||
} |
|
||||
|
|
||||
public IntPtr Handle => _view?.Handle ?? IntPtr.Zero; |
|
||||
public string HandleDescriptor => "NSView"; |
|
||||
|
|
||||
public void Destroy() |
|
||||
{ |
|
||||
_view?.Dispose(); |
|
||||
_view = null; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,28 @@ |
|||||
|
using System; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using Avalonia.Platform.Interop; |
||||
|
|
||||
|
namespace ControlCatalog.Desktop; |
||||
|
|
||||
|
internal static partial class ObjC |
||||
|
{ |
||||
|
private const string LibObjC = "/usr/lib/libobjc.dylib"; |
||||
|
|
||||
|
public static IntPtr CreateString(string value) |
||||
|
{ |
||||
|
using var utf8 = new Utf8Buffer(value); |
||||
|
return MsgSend(GetClass("NSString"), GetUid("stringWithUTF8String:"), utf8); |
||||
|
} |
||||
|
|
||||
|
[LibraryImport(LibObjC, EntryPoint = "objc_getClass", StringMarshalling = StringMarshalling.Utf8)] |
||||
|
internal static partial IntPtr GetClass(string name); |
||||
|
|
||||
|
[LibraryImport(LibObjC, EntryPoint = "sel_getUid", StringMarshalling = StringMarshalling.Utf8)] |
||||
|
internal static partial IntPtr GetUid(string selector); |
||||
|
|
||||
|
[LibraryImport(LibObjC, EntryPoint = "objc_msgSend")] |
||||
|
internal static partial IntPtr MsgSend(IntPtr receiver, IntPtr selector); |
||||
|
|
||||
|
[LibraryImport(LibObjC, EntryPoint = "objc_msgSend")] |
||||
|
internal static partial IntPtr MsgSend(IntPtr receiver, IntPtr selector, IntPtr arg); |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Data.Converters; |
||||
|
|
||||
|
namespace ControlCatalog.Converter |
||||
|
{ |
||||
|
internal sealed class FlexDemoNumberToThicknessConverter : IValueConverter |
||||
|
{ |
||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||
|
{ |
||||
|
if (value is int x && targetType.IsAssignableFrom(typeof(Thickness))) |
||||
|
{ |
||||
|
var y = 16 + 2 * ((x * 5) % 9); |
||||
|
return new Thickness(2 * y, y); |
||||
|
} |
||||
|
|
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
|
||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => |
||||
|
throw new NotSupportedException(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Data.Converters; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Media.Immutable; |
||||
|
|
||||
|
namespace ControlCatalog.Converter; |
||||
|
|
||||
|
public sealed class GdpToBrushConverter : IValueConverter |
||||
|
{ |
||||
|
private readonly ImmutableSolidColorBrush _orangeBrush = new(Colors.Orange, 0.6); |
||||
|
private readonly ImmutableSolidColorBrush _yellowBrush = new(Colors.Yellow, 0.6); |
||||
|
private readonly ImmutableSolidColorBrush _greenBrush = new(Colors.LightGreen, 0.6); |
||||
|
|
||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||
|
{ |
||||
|
if (value is not int gdp) |
||||
|
return AvaloniaProperty.UnsetValue; |
||||
|
|
||||
|
return gdp switch |
||||
|
{ |
||||
|
<= 5000 => _orangeBrush, |
||||
|
<= 10000 => _yellowBrush, |
||||
|
_ => _greenBrush |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
object IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||
|
=> throw new NotSupportedException(); |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using System; |
||||
|
using System.Globalization; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Data.Converters; |
||||
|
|
||||
|
namespace ControlCatalog.Converter; |
||||
|
|
||||
|
public sealed class TableViewColumnWidthConverter : IValueConverter |
||||
|
{ |
||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||
|
{ |
||||
|
if (value is bool useStarSize && |
||||
|
parameter is string stringParameter && |
||||
|
double.TryParse(stringParameter, NumberStyles.Number, CultureInfo.InvariantCulture, out var baseWidth)) |
||||
|
{ |
||||
|
return useStarSize ? |
||||
|
new GridLength(baseWidth, GridUnitType.Star) : |
||||
|
new GridLength(baseWidth * 100, GridUnitType.Pixel); |
||||
|
} |
||||
|
|
||||
|
return AvaloniaProperty.UnsetValue; |
||||
|
} |
||||
|
|
||||
|
object? IValueConverter.ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||
|
=> throw new NotSupportedException(); |
||||
|
} |
||||
@ -1,61 +1,204 @@ |
|||||
<TabbedPage xmlns="https://github.com/avaloniaui" |
<TabbedPage |
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
x:Class="ControlCatalog.Pages.CompositionPage" |
||||
xmlns:pages="using:ControlCatalog.Pages" |
xmlns="https://github.com/avaloniaui" |
||||
Header="Composition" |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
x:Class="ControlCatalog.Pages.CompositionPage"> |
xmlns:pages="using:ControlCatalog.Pages"> |
||||
<ContentPage Theme="{StaticResource ScrollPage}" Header="Implicit animations"> |
<ContentPage Header="Implicit"> |
||||
<StackPanel> |
<StackPanel> |
||||
<Grid ColumnDefinitions="*,10,40" Margin="0 0 40 0"> |
<Grid Margin="0,0,40,0" ColumnDefinitions="*,10,40"> |
||||
<ItemsControl x:Name="Items"> |
<ItemsControl x:Name="Items"> |
||||
<ItemsControl.ItemsPanel> |
<ItemsControl.ItemsPanel> |
||||
<ItemsPanelTemplate> |
<ItemsPanelTemplate> |
||||
<WrapPanel/> |
<WrapPanel /> |
||||
</ItemsPanelTemplate> |
</ItemsPanelTemplate> |
||||
</ItemsControl.ItemsPanel> |
</ItemsControl.ItemsPanel> |
||||
<ItemsControl.DataTemplates> |
<ItemsControl.DataTemplates> |
||||
<DataTemplate DataType="pages:CompositionPageColorItem"> |
<DataTemplate DataType="pages:CompositionPageColorItem"> |
||||
<Border |
<Border |
||||
pages:CompositionPage.EnableAnimations="True" |
Width="100" |
||||
Padding="10" BorderBrush="Gray" BorderThickness="2" |
Height="100" |
||||
Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10"> |
Margin="10" |
||||
<TextBlock Text="{Binding ColorHexValue}"/> |
Padding="10" |
||||
</Border> |
pages:CompositionPage.EnableAnimations="True" |
||||
</DataTemplate> |
Background="{Binding ColorBrush}" |
||||
</ItemsControl.DataTemplates> |
BorderBrush="Gray" |
||||
</ItemsControl> |
BorderThickness="2"> |
||||
<GridSplitter Margin="2" BorderThickness="1" BorderBrush="Gray" |
<TextBlock Text="{Binding ColorHexValue}" /> |
||||
Background="#e0e0e0" Grid.Column="1" |
</Border> |
||||
ResizeDirection="Columns" ResizeBehavior="PreviousAndNext" |
</DataTemplate> |
||||
/> |
</ItemsControl.DataTemplates> |
||||
<Border Grid.Column="2"> |
</ItemsControl> |
||||
<LayoutTransformControl HorizontalAlignment="Center" MinWidth="30"> |
<GridSplitter |
||||
<LayoutTransformControl.LayoutTransform> |
Grid.Column="1" |
||||
<RotateTransform Angle="90"/> |
Margin="2" |
||||
</LayoutTransformControl.LayoutTransform> |
Background="#e0e0e0" |
||||
<TextBlock>Resize me</TextBlock> |
BorderBrush="Gray" |
||||
</LayoutTransformControl> |
BorderThickness="1" |
||||
</Border> |
ResizeBehavior="PreviousAndNext" |
||||
</Grid> |
ResizeDirection="Columns" /> |
||||
</StackPanel> |
<Border Grid.Column="2"> |
||||
</ContentPage> |
<LayoutTransformControl MinWidth="30" HorizontalAlignment="Center"> |
||||
<ContentPage Theme="{StaticResource ScrollPage}" Header="Animation"> |
<LayoutTransformControl.LayoutTransform> |
||||
<DockPanel> |
<RotateTransform Angle="90" /> |
||||
<Button DockPanel.Dock="Top" Margin="10" Click="ButtonThreadSleep">Thread.Sleep(10000);</Button> |
</LayoutTransformControl.LayoutTransform> |
||||
<Control x:Name="SolidVisualHost" /> |
<TextBlock>Resize me</TextBlock> |
||||
</DockPanel> |
</LayoutTransformControl> |
||||
</ContentPage> |
</Border> |
||||
<ContentPage Theme="{StaticResource ScrollPage}" Header="Custom"> |
</Grid> |
||||
<DockPanel> |
</StackPanel> |
||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"> |
</ContentPage> |
||||
<Button Margin="10" Click="ButtonThreadSleep">Thread.Sleep(10000);</Button> |
<ContentPage Header="Animations"> |
||||
<Button Margin="10" Click="ButtonStartCustomVisual">Start</Button> |
<DockPanel> |
||||
<Button Margin="10" Click="ButtonStopCustomVisual">Stop</Button> |
<Button |
||||
<CheckBox Margin="10" |
Margin="10" |
||||
x:Name="PreciseDirtyRectsCheckboxCustomVisual" |
Click="ButtonThreadSleep" |
||||
IsCheckedChanged="PreciseDirtyRectsCheckboxCustomVisualChanged" |
DockPanel.Dock="Top"> |
||||
>Precise dirty rects</CheckBox> |
Thread.Sleep(10000); |
||||
</StackPanel> |
</Button> |
||||
<Control x:Name="CustomVisualHost" /> |
<Control x:Name="SolidVisualHost" /> |
||||
</DockPanel> |
</DockPanel> |
||||
</ContentPage> |
</ContentPage> |
||||
|
<ContentPage Header="Custom"> |
||||
|
<DockPanel> |
||||
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"> |
||||
|
<Button Margin="10" Click="ButtonThreadSleep">Thread.Sleep(5000);</Button> |
||||
|
<Button Margin="10" Click="ButtonStartCustomVisual">Start</Button> |
||||
|
<Button Margin="10" Click="ButtonStopCustomVisual">Stop</Button> |
||||
|
<CheckBox |
||||
|
x:Name="PreciseDirtyRectsCheckboxCustomVisual" |
||||
|
Margin="10" |
||||
|
IsCheckedChanged="PreciseDirtyRectsCheckboxCustomVisualChanged"> |
||||
|
Precise dirty rects |
||||
|
</CheckBox> |
||||
|
</StackPanel> |
||||
|
<Control x:Name="CustomVisualHost" /> |
||||
|
</DockPanel> |
||||
|
</ContentPage> |
||||
|
<ContentPage Header="Brushes"> |
||||
|
<ScrollViewer> |
||||
|
<StackPanel> |
||||
|
<StackPanel.Styles> |
||||
|
<Style Selector="Button"> |
||||
|
<Setter Property="BorderThickness" Value="2" /> |
||||
|
</Style> |
||||
|
</StackPanel.Styles> |
||||
|
<Button Margin="10" Click="ButtonThreadSleep"> |
||||
|
Thread.Sleep(5000); |
||||
|
</Button> |
||||
|
<UniformGrid |
||||
|
Margin="12" |
||||
|
ColumnSpacing="12" |
||||
|
RowSpacing="12"> |
||||
|
|
||||
|
<!-- Solid Color --> |
||||
|
<StackPanel Spacing="6"> |
||||
|
<TextBlock |
||||
|
FontWeight="Bold" |
||||
|
Foreground="{Binding #SolidBrushHost.Background, Mode=OneWay}" |
||||
|
Text="SolidColorBrush" /> |
||||
|
<Border |
||||
|
x:Name="SolidBrushHost" |
||||
|
Height="120" |
||||
|
Background="Gray" |
||||
|
BorderBrush="DarkGray" |
||||
|
BorderThickness="1" |
||||
|
CornerRadius="8"> |
||||
|
<TextBlock |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
FontSize="18" |
||||
|
Text="Solid" /> |
||||
|
</Border> |
||||
|
<StackPanel Orientation="Horizontal" Spacing="8"> |
||||
|
<Button |
||||
|
BorderBrush="{Binding #SolidBrushHost.Background, Mode=OneWay}" |
||||
|
Click="SolidBrushCreateAnimate_Click" |
||||
|
Content="Create" /> |
||||
|
</StackPanel> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<!-- Linear Gradient --> |
||||
|
<StackPanel Spacing="6"> |
||||
|
<TextBlock |
||||
|
FontWeight="Bold" |
||||
|
Foreground="{Binding #LinearBrushHost.Background, Mode=OneWay}" |
||||
|
Text="LinearGradientBrush" /> |
||||
|
<Border |
||||
|
x:Name="LinearBrushHost" |
||||
|
Height="120" |
||||
|
Background="Gray" |
||||
|
BorderBrush="DarkGray" |
||||
|
BorderThickness="1" |
||||
|
CornerRadius="8"> |
||||
|
<TextBlock |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
FontSize="18" |
||||
|
Text="Linear" /> |
||||
|
</Border> |
||||
|
<StackPanel Orientation="Horizontal" Spacing="8"> |
||||
|
<Button |
||||
|
BorderBrush="{Binding #LinearBrushHost.Background, Mode=OneWay}" |
||||
|
Click="LinearBrushCreateAnimate_Click" |
||||
|
Content="Create" /> |
||||
|
</StackPanel> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<!-- Radial Gradient --> |
||||
|
<StackPanel Spacing="6"> |
||||
|
<TextBlock |
||||
|
FontWeight="Bold" |
||||
|
Foreground="{Binding #RadialBrushHost.Background, Mode=OneWay}" |
||||
|
Text="RadialGradientBrush" /> |
||||
|
<Border |
||||
|
x:Name="RadialBrushHost" |
||||
|
Height="120" |
||||
|
Background="Gray" |
||||
|
BorderBrush="DarkGray" |
||||
|
BorderThickness="1" |
||||
|
CornerRadius="8"> |
||||
|
<TextBlock |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
FontSize="18" |
||||
|
Text="Radial" /> |
||||
|
</Border> |
||||
|
<StackPanel Orientation="Horizontal" Spacing="8"> |
||||
|
<Button |
||||
|
BorderBrush="{Binding #RadialBrushHost.Background, Mode=OneWay}" |
||||
|
Click="RadialBrushCreateAnimate_Click" |
||||
|
Content="Create" /> |
||||
|
</StackPanel> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<!-- Conic Gradient --> |
||||
|
<StackPanel Spacing="6"> |
||||
|
<TextBlock |
||||
|
FontWeight="Bold" |
||||
|
Foreground="{Binding #ConicBrushHost.Background, Mode=OneWay}" |
||||
|
Text="ConicGradientBrush" /> |
||||
|
<Border |
||||
|
x:Name="ConicBrushHost" |
||||
|
Height="120" |
||||
|
Background="Gray" |
||||
|
BorderBrush="DarkGray" |
||||
|
BorderThickness="1" |
||||
|
CornerRadius="8"> |
||||
|
<TextBlock |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
FontSize="18" |
||||
|
Text="Conic" /> |
||||
|
</Border> |
||||
|
<StackPanel Orientation="Horizontal" Spacing="8"> |
||||
|
<Button |
||||
|
BorderBrush="{Binding #ConicBrushHost.Background, Mode=OneWay}" |
||||
|
Click="ConicBrushCreateAnimate_Click" |
||||
|
Content="Create" /> |
||||
|
</StackPanel> |
||||
|
</StackPanel> |
||||
|
</UniformGrid> |
||||
|
</StackPanel> |
||||
|
</ScrollViewer> |
||||
|
</ContentPage> |
||||
</TabbedPage> |
</TabbedPage> |
||||
|
|||||
@ -1,12 +0,0 @@ |
|||||
<ContentPage Theme="{StaticResource ScrollPage}" xmlns="https://github.com/avaloniaui" |
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|
||||
Header="DataGrid" |
|
||||
x:Class="ControlCatalog.Pages.DataGridPage"> |
|
||||
|
|
||||
<StackPanel> |
|
||||
<TextBlock Text="DataGrid has moved to its own repository!" Margin="8" /> |
|
||||
<HyperlinkButton Content="https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid" |
|
||||
Click="OnLinkClicked" /> |
|
||||
</StackPanel> |
|
||||
|
|
||||
</ContentPage> |
|
||||
@ -1,22 +0,0 @@ |
|||||
using System.Diagnostics; |
|
||||
using Avalonia.Controls; |
|
||||
using Avalonia.Interactivity; |
|
||||
|
|
||||
namespace ControlCatalog.Pages; |
|
||||
|
|
||||
public partial class DataGridPage : ContentPage |
|
||||
{ |
|
||||
public DataGridPage() |
|
||||
{ |
|
||||
InitializeComponent(); |
|
||||
} |
|
||||
|
|
||||
private void OnLinkClicked(object? sender, RoutedEventArgs e) |
|
||||
{ |
|
||||
Process.Start(new ProcessStartInfo |
|
||||
{ |
|
||||
FileName = "https://github.com/AvaloniaUI/Avalonia.Controls.DataGrid", |
|
||||
UseShellExecute = true |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,219 @@ |
|||||
|
<ContentPage xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
|
xmlns:panels="using:Avalonia.Labs.Panels" |
||||
|
xmlns:conv="using:ControlCatalog.Converter" |
||||
|
xmlns:vm="using:ControlCatalog.ViewModels" |
||||
|
mc:Ignorable="d" |
||||
|
d:DesignWidth="800" |
||||
|
d:DesignHeight="450" |
||||
|
x:DataType="vm:FlexViewModel" |
||||
|
HorizontalContentAlignment="Stretch" |
||||
|
VerticalContentAlignment="Stretch" |
||||
|
x:Class="ControlCatalog.Pages.FlexPage"> |
||||
|
<ContentPage.Resources> |
||||
|
<conv:FlexDemoNumberToThicknessConverter x:Key="NumberToThicknessConverter" /> |
||||
|
<DataTemplate x:Key="ItemTemplate" |
||||
|
x:DataType="vm:FlexItemViewModel"> |
||||
|
<ListBoxItem Padding="{Binding Value, Converter={StaticResource NumberToThicknessConverter}}" |
||||
|
IsSelected="{Binding IsSelected}" |
||||
|
Background="{Binding Color}" |
||||
|
Tapped="OnItemTapped"> |
||||
|
<ListBoxItem.Styles> |
||||
|
<Style Selector="ListBoxItem:selected"> |
||||
|
<Setter Property="Background" Value="DodgerBlue" /> |
||||
|
</Style> |
||||
|
</ListBoxItem.Styles> |
||||
|
<TextBlock HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
Text="{Binding Value}" /> |
||||
|
</ListBoxItem> |
||||
|
</DataTemplate> |
||||
|
</ContentPage.Resources> |
||||
|
<ContentPage.Styles> |
||||
|
<Style Selector="CheckBox"> |
||||
|
<Setter Property="MinWidth" Value="0" /> |
||||
|
<Setter Property="Padding" Value="8,0,0,0" /> |
||||
|
<Setter Property="VerticalContentAlignment" Value="Center" /> |
||||
|
</Style> |
||||
|
<Style Selector="ComboBox"> |
||||
|
<Setter Property="HorizontalAlignment" Value="Stretch" /> |
||||
|
</Style> |
||||
|
<Style Selector="RadioButton"> |
||||
|
<Setter Property="MinWidth" Value="0" /> |
||||
|
<Setter Property="Padding" Value="8,0,0,0" /> |
||||
|
<Setter Property="VerticalContentAlignment" Value="Center" /> |
||||
|
</Style> |
||||
|
</ContentPage.Styles> |
||||
|
|
||||
|
<DockPanel Margin="16"> |
||||
|
|
||||
|
<ScrollViewer HorizontalScrollBarVisibility="Disabled" |
||||
|
Padding="0,0,16,0" |
||||
|
MinWidth="200"> |
||||
|
|
||||
|
<StackPanel Spacing="16"> |
||||
|
|
||||
|
<TextBlock FontSize="16" |
||||
|
Text="Properties" /> |
||||
|
|
||||
|
<!--<panels:FlexPanel ColumnSpacing="16"> |
||||
|
<RadioButton IsChecked="{Binding IsItemsControl}" |
||||
|
Content="ItemsControl" /> |
||||
|
<RadioButton IsChecked="{Binding IsItemsRepeater}" |
||||
|
Content="ItemsRepeater" /> |
||||
|
</panels:FlexPanel>--> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Direction:" /> |
||||
|
<ComboBox ItemsSource="{Binding DirectionValues}" |
||||
|
SelectedItem="{Binding Direction}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="JustifyContent:" /> |
||||
|
<ComboBox ItemsSource="{Binding JustifyContentValues}" |
||||
|
SelectedItem="{Binding JustifyContent}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="AlignItems:" /> |
||||
|
<ComboBox ItemsSource="{Binding AlignItemsValues}" |
||||
|
SelectedItem="{Binding AlignItems}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="AlignContent:" /> |
||||
|
<ComboBox ItemsSource="{Binding AlignContentValues}" |
||||
|
SelectedItem="{Binding AlignContent}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Wrap:" /> |
||||
|
<ComboBox ItemsSource="{Binding WrapValues}" |
||||
|
SelectedItem="{Binding Wrap}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="ColumnSpacing:" /> |
||||
|
<NumericUpDown Minimum="0" |
||||
|
Value="{Binding ColumnSpacing}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="RowSpacing:" /> |
||||
|
<NumericUpDown Minimum="0" |
||||
|
Value="{Binding RowSpacing}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<CheckBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
|
Content="SelectedItem IsVisible" |
||||
|
IsChecked="{Binding SelectedItem.IsVisible}" /> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="SelectedItem AlignSelf:" /> |
||||
|
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
|
ItemsSource="{Binding AlignSelfValues}" |
||||
|
SelectedItem="{Binding SelectedItem.AlignSelfItem}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="SelectedItem Order:" /> |
||||
|
<NumericUpDown IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
|
Value="{Binding SelectedItem.Order}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="SelectedItem Shrink:" /> |
||||
|
<NumericUpDown Minimum="0.0" |
||||
|
IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
|
Value="{Binding SelectedItem.Shrink}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="SelectedItem Grow:" /> |
||||
|
<NumericUpDown Minimum="0.0" |
||||
|
IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
|
Value="{Binding SelectedItem.Grow}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="SelectedItem Basis:" /> |
||||
|
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
|
ItemsSource="{Binding FlexBasisKindValues}" |
||||
|
SelectedItem="{Binding SelectedItem.BasisKind}" /> |
||||
|
<NumericUpDown Minimum="0.0" |
||||
|
IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
|
Value="{Binding SelectedItem.BasisValue}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="SelectedItem HorizontalAlignment:" /> |
||||
|
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
|
ItemsSource="{Binding HorizontalAlignmentValues}" |
||||
|
SelectedItem="{Binding SelectedItem.HorizontalAlignment}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="SelectedItem VerticalAlignment:" /> |
||||
|
<ComboBox IsEnabled="{Binding SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" |
||||
|
ItemsSource="{Binding VerticalAlignmentValues}" |
||||
|
SelectedItem="{Binding SelectedItem.VerticalAlignment}" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<Grid ColumnDefinitions="*,8,*"> |
||||
|
|
||||
|
<Button Grid.Column="0" |
||||
|
HorizontalAlignment="Stretch" |
||||
|
HorizontalContentAlignment="Center" |
||||
|
Command="{Binding AddItemCommand}" |
||||
|
Content="Add Item" /> |
||||
|
|
||||
|
<Button Grid.Column="2" |
||||
|
HorizontalAlignment="Stretch" |
||||
|
HorizontalContentAlignment="Center" |
||||
|
Command="{Binding RemoveItemCommand}" |
||||
|
Content="Remove Item" /> |
||||
|
|
||||
|
</Grid> |
||||
|
|
||||
|
</StackPanel> |
||||
|
|
||||
|
</ScrollViewer> |
||||
|
|
||||
|
<ItemsControl BorderBrush="#777" |
||||
|
BorderThickness="1" |
||||
|
ItemsSource="{Binding Numbers}" |
||||
|
ItemTemplate="{StaticResource ItemTemplate}"> |
||||
|
<ItemsControl.Styles> |
||||
|
<Style Selector="ContentPresenter" |
||||
|
x:DataType="vm:FlexItemViewModel"> |
||||
|
<Setter Property="Flex.AlignSelf" Value="{Binding AlignSelf}" /> |
||||
|
<Setter Property="Flex.Order" Value="{Binding Order}" /> |
||||
|
<Setter Property="Flex.Shrink" Value="{Binding Shrink}" /> |
||||
|
<Setter Property="Flex.Grow" Value="{Binding Grow}" /> |
||||
|
<Setter Property="Flex.Basis" Value="{Binding Basis}" /> |
||||
|
<Setter Property="HorizontalAlignment" Value="{Binding HorizontalAlignment}" /> |
||||
|
<Setter Property="VerticalAlignment" Value="{Binding VerticalAlignment}" /> |
||||
|
<Setter Property="IsVisible" Value="{Binding IsVisible}" /> |
||||
|
</Style> |
||||
|
</ItemsControl.Styles> |
||||
|
<ItemsControl.ItemsPanel> |
||||
|
<ItemsPanelTemplate> |
||||
|
<FlexPanel Direction="{Binding Direction}" |
||||
|
JustifyContent="{Binding JustifyContent}" |
||||
|
AlignItems="{Binding AlignItems}" |
||||
|
AlignContent="{Binding AlignContent}" |
||||
|
Wrap="{Binding Wrap}" |
||||
|
ColumnSpacing="{Binding ColumnSpacing}" |
||||
|
RowSpacing="{Binding RowSpacing}" /> |
||||
|
</ItemsPanelTemplate> |
||||
|
</ItemsControl.ItemsPanel> |
||||
|
</ItemsControl> |
||||
|
|
||||
|
</DockPanel> |
||||
|
|
||||
|
|
||||
|
|
||||
|
</ContentPage> |
||||
@ -0,0 +1,37 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Interactivity; |
||||
|
using ControlCatalog.ViewModels; |
||||
|
|
||||
|
namespace ControlCatalog.Pages |
||||
|
{ |
||||
|
public partial class FlexPage : ContentPage |
||||
|
{ |
||||
|
public FlexPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
|
||||
|
DataContext = new FlexViewModel(); |
||||
|
} |
||||
|
|
||||
|
private void OnItemTapped(object? sender, RoutedEventArgs e) |
||||
|
{ |
||||
|
if (sender is ListBoxItem control && DataContext is FlexViewModel vm && control.DataContext is FlexItemViewModel item) |
||||
|
{ |
||||
|
if (vm.SelectedItem != null) |
||||
|
{ |
||||
|
vm.SelectedItem.IsSelected = false; |
||||
|
} |
||||
|
|
||||
|
if (vm.SelectedItem == item) |
||||
|
{ |
||||
|
vm.SelectedItem = null; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
vm.SelectedItem = item; |
||||
|
vm.SelectedItem.IsSelected = true; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,136 @@ |
|||||
|
<ContentPage Theme="{StaticResource ScrollPage}" x:Class="ControlCatalog.Pages.PlatformSettingsPage" |
||||
|
xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
|
xmlns:viewModels="using:ControlCatalog.ViewModels" |
||||
|
d:DesignHeight="800" |
||||
|
d:DesignWidth="600" |
||||
|
mc:Ignorable="d" |
||||
|
Header="Platform Settings" |
||||
|
x:DataType="viewModels:PlatformSettingsViewModel"> |
||||
|
|
||||
|
<StackPanel Spacing="20"> |
||||
|
|
||||
|
<TextBlock Text="This page displays IPlatformSettings values which may change at runtime based on system settings." |
||||
|
TextWrapping="Wrap" |
||||
|
Opacity="0.7" /> |
||||
|
|
||||
|
<GroupBox Header="System Color Values"> |
||||
|
<StackPanel Spacing="15"> |
||||
|
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto"> |
||||
|
<TextBlock Grid.Column="0" Grid.Row="0" Text="Theme Variant:" VerticalAlignment="Center" /> |
||||
|
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding ThemeVariant}" FontWeight="Bold" VerticalAlignment="Center" /> |
||||
|
|
||||
|
<TextBlock Grid.Column="0" Grid.Row="1" Text="Contrast Preference:" VerticalAlignment="Center" Margin="0,10,0,0" /> |
||||
|
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding ContrastPreference}" FontWeight="Bold" VerticalAlignment="Center" Margin="0,10,0,0" /> |
||||
|
</Grid> |
||||
|
|
||||
|
<StackPanel Spacing="10"> |
||||
|
<Grid ColumnDefinitions="180,120,*"> |
||||
|
<TextBlock Grid.Column="0" Text="Accent Color 1:" VerticalAlignment="Center" /> |
||||
|
<Border Grid.Column="1" |
||||
|
Width="100" |
||||
|
Height="40" |
||||
|
BorderBrush="Black" |
||||
|
BorderThickness="1" |
||||
|
CornerRadius="4" |
||||
|
HorizontalAlignment="Left"> |
||||
|
<Border.Background> |
||||
|
<SolidColorBrush Color="{Binding AccentColor1}" /> |
||||
|
</Border.Background> |
||||
|
</Border> |
||||
|
<TextBlock Grid.Column="2" |
||||
|
Text="{Binding AccentColor1}" |
||||
|
VerticalAlignment="Center" |
||||
|
Margin="10,0,0,0" /> |
||||
|
</Grid> |
||||
|
|
||||
|
<Grid ColumnDefinitions="180,120,*"> |
||||
|
<TextBlock Grid.Column="0" Text="Accent Color 2:" VerticalAlignment="Center" /> |
||||
|
<Border Grid.Column="1" |
||||
|
Width="100" |
||||
|
Height="40" |
||||
|
BorderBrush="Black" |
||||
|
BorderThickness="1" |
||||
|
CornerRadius="4" |
||||
|
HorizontalAlignment="Left"> |
||||
|
<Border.Background> |
||||
|
<SolidColorBrush Color="{Binding AccentColor2}" /> |
||||
|
</Border.Background> |
||||
|
</Border> |
||||
|
<TextBlock Grid.Column="2" |
||||
|
Text="{Binding AccentColor2}" |
||||
|
VerticalAlignment="Center" |
||||
|
Margin="10,0,0,0" /> |
||||
|
</Grid> |
||||
|
|
||||
|
<Grid ColumnDefinitions="180,120,*"> |
||||
|
<TextBlock Grid.Column="0" Text="Accent Color 3:" VerticalAlignment="Center" /> |
||||
|
<Border Grid.Column="1" |
||||
|
Width="100" |
||||
|
Height="40" |
||||
|
BorderBrush="Black" |
||||
|
BorderThickness="1" |
||||
|
CornerRadius="4" |
||||
|
HorizontalAlignment="Left"> |
||||
|
<Border.Background> |
||||
|
<SolidColorBrush Color="{Binding AccentColor3}" /> |
||||
|
</Border.Background> |
||||
|
</Border> |
||||
|
<TextBlock Grid.Column="2" |
||||
|
Text="{Binding AccentColor3}" |
||||
|
VerticalAlignment="Center" |
||||
|
Margin="10,0,0,0" /> |
||||
|
</Grid> |
||||
|
</StackPanel> |
||||
|
</StackPanel> |
||||
|
</GroupBox> |
||||
|
|
||||
|
<GroupBox Header="Language Settings"> |
||||
|
<StackPanel Spacing="10"> |
||||
|
<TextBlock Text="This reflects the preferred application language as specified in OS settings." |
||||
|
TextWrapping="Wrap" |
||||
|
Opacity="0.7" |
||||
|
FontSize="12" /> |
||||
|
<Grid ColumnDefinitions="180,*"> |
||||
|
<TextBlock Grid.Column="0" Text="Preferred Language:" VerticalAlignment="Center" /> |
||||
|
<TextBlock Grid.Column="1" Text="{Binding PreferredLanguage}" FontWeight="Bold" VerticalAlignment="Center" /> |
||||
|
</Grid> |
||||
|
</StackPanel> |
||||
|
</GroupBox> |
||||
|
|
||||
|
<GroupBox Header="Other Platform Settings"> |
||||
|
<StackPanel Spacing="10"> |
||||
|
<TextBlock Text="These settings are less commonly changed and are platform-specific." |
||||
|
TextWrapping="Wrap" |
||||
|
Opacity="0.7" |
||||
|
FontSize="12" /> |
||||
|
|
||||
|
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto"> |
||||
|
<TextBlock Grid.Column="0" Grid.Row="0" Text="Hold Wait Duration:" VerticalAlignment="Center" /> |
||||
|
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding HoldWaitDuration}" VerticalAlignment="Center" /> |
||||
|
|
||||
|
<TextBlock Grid.Column="0" Grid.Row="1" Text="Tap Size (Touch):" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding TapSizeTouch}" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
|
||||
|
<TextBlock Grid.Column="0" Grid.Row="2" Text="Tap Size (Mouse):" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
<TextBlock Grid.Column="1" Grid.Row="2" Text="{Binding TapSizeMouse}" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
|
||||
|
<TextBlock Grid.Column="0" Grid.Row="3" Text="Double-Tap Size (Touch):" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
<TextBlock Grid.Column="1" Grid.Row="3" Text="{Binding DoubleTapSizeTouch}" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
|
||||
|
<TextBlock Grid.Column="0" Grid.Row="4" Text="Double-Tap Size (Mouse):" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
<TextBlock Grid.Column="1" Grid.Row="4" Text="{Binding DoubleTapSizeMouse}" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
|
||||
|
<TextBlock Grid.Column="0" Grid.Row="5" Text="Double-Tap Time (Touch):" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
<TextBlock Grid.Column="1" Grid.Row="5" Text="{Binding DoubleTapTimeTouch}" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
|
||||
|
<TextBlock Grid.Column="0" Grid.Row="6" Text="Double-Tap Time (Mouse):" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
<TextBlock Grid.Column="1" Grid.Row="6" Text="{Binding DoubleTapTimeMouse}" VerticalAlignment="Center" Margin="0,5,0,0" /> |
||||
|
</Grid> |
||||
|
</StackPanel> |
||||
|
</GroupBox> |
||||
|
|
||||
|
</StackPanel> |
||||
|
</ContentPage> |
||||
@ -0,0 +1,30 @@ |
|||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.VisualTree; |
||||
|
using ControlCatalog.ViewModels; |
||||
|
|
||||
|
namespace ControlCatalog.Pages |
||||
|
{ |
||||
|
public partial class PlatformSettingsPage : ContentPage |
||||
|
{ |
||||
|
private readonly PlatformSettingsViewModel _viewModel = new(); |
||||
|
|
||||
|
public PlatformSettingsPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
DataContext = _viewModel; |
||||
|
} |
||||
|
|
||||
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
||||
|
{ |
||||
|
base.OnAttachedToVisualTree(e); |
||||
|
_viewModel.Subscribe(this.GetPlatformSettings() ?? Application.Current?.PlatformSettings); |
||||
|
} |
||||
|
|
||||
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) |
||||
|
{ |
||||
|
_viewModel.Unsubscribe(); |
||||
|
base.OnDetachedFromVisualTree(e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
<ContentPage xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
|
xmlns:viewModels="clr-namespace:ControlCatalog.ViewModels" |
||||
|
xmlns:converter="clr-namespace:ControlCatalog.Converter" |
||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
|
x:Class="ControlCatalog.Pages.TableViewPage" |
||||
|
x:DataType="viewModels:TableViewPageViewModel" |
||||
|
Header="TableView"> |
||||
|
|
||||
|
<ContentPage.Resources> |
||||
|
|
||||
|
<converter:GdpToBrushConverter x:Key="GdpToBrushConverter" /> |
||||
|
<converter:TableViewColumnWidthConverter x:Key="TableViewColumnWidthConverter" /> |
||||
|
|
||||
|
<ControlTheme x:Key="GdpCellTheme" |
||||
|
TargetType="TableViewCell" |
||||
|
BasedOn="{StaticResource {x:Type TableViewCell}}" |
||||
|
x:DataType="viewModels:TableViewPageViewModel+Country"> |
||||
|
<Setter Property="Background" Value="{Binding GDP, Converter={StaticResource GdpToBrushConverter}}" /> |
||||
|
</ControlTheme> |
||||
|
|
||||
|
</ContentPage.Resources> |
||||
|
|
||||
|
<DockPanel> |
||||
|
|
||||
|
<StackPanel Orientation="Horizontal" |
||||
|
DockPanel.Dock="Top"> |
||||
|
|
||||
|
<CheckBox Name="UseStarSizes" |
||||
|
Content="Use star-sized columns" |
||||
|
IsChecked="True" |
||||
|
Margin="8" /> |
||||
|
|
||||
|
<CheckBox Name="CanUserResizeColumns" |
||||
|
Content="Allow column resizing" |
||||
|
IsChecked="True" |
||||
|
Margin="8" /> |
||||
|
|
||||
|
</StackPanel> |
||||
|
|
||||
|
<TableView ItemsSource="{Binding Countries}" |
||||
|
CanUserResizeColumns="{Binding #CanUserResizeColumns.IsChecked}"> |
||||
|
<TableView.Columns> |
||||
|
<TableViewColumn Header="Name" |
||||
|
Binding="{Binding Name}" |
||||
|
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=3}" /> |
||||
|
<TableViewColumn Header="Region" |
||||
|
Binding="{Binding Region}" |
||||
|
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=2}" /> |
||||
|
<TableViewColumn Header="Population" |
||||
|
Binding="{Binding Population, StringFormat=N0}" |
||||
|
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=1.5}" |
||||
|
HorizontalContentAlignment="Right" /> |
||||
|
<TableViewColumn Header="Area" |
||||
|
Binding="{Binding Area, StringFormat=N0}" |
||||
|
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=1}" |
||||
|
HorizontalContentAlignment="Right" /> |
||||
|
<TableViewColumn Header="GDP" |
||||
|
Binding="{Binding GDP, StringFormat=N0}" |
||||
|
Width="{Binding #UseStarSizes.IsChecked, Converter={StaticResource TableViewColumnWidthConverter}, ConverterParameter=1}" |
||||
|
CellTheme="{StaticResource GdpCellTheme}" |
||||
|
HorizontalContentAlignment="Right"> |
||||
|
</TableViewColumn> |
||||
|
</TableView.Columns> |
||||
|
</TableView> |
||||
|
|
||||
|
</DockPanel> |
||||
|
|
||||
|
</ContentPage> |
||||
@ -0,0 +1,13 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using ControlCatalog.ViewModels; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class TableViewPage : ContentPage |
||||
|
{ |
||||
|
public TableViewPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
DataContext = new TableViewPageViewModel(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,120 @@ |
|||||
|
<ContentPage xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||
|
xmlns:viewModels="using:ControlCatalog.ViewModels" |
||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
||||
|
x:Class="ControlCatalog.Pages.WrapPanelPage" |
||||
|
x:DataType="viewModels:WrapPanelPageViewModel" |
||||
|
Header="WrapPanel"> |
||||
|
|
||||
|
<ContentPage.Styles> |
||||
|
<Style Selector="Border.item"> |
||||
|
<Setter Property="Background" Value="#4C8BF5" /> |
||||
|
<Setter Property="BorderBrush" Value="#1A4FB8" /> |
||||
|
<Setter Property="BorderThickness" Value="1" /> |
||||
|
<Setter Property="CornerRadius" Value="3" /> |
||||
|
</Style> |
||||
|
<Style Selector="Border.item > TextBlock"> |
||||
|
<Setter Property="Foreground" Value="White" /> |
||||
|
<Setter Property="FontWeight" Value="Bold" /> |
||||
|
<Setter Property="HorizontalAlignment" Value="Center" /> |
||||
|
<Setter Property="VerticalAlignment" Value="Center" /> |
||||
|
</Style> |
||||
|
</ContentPage.Styles> |
||||
|
|
||||
|
<DockPanel> |
||||
|
|
||||
|
<TextBlock Text="A panel which lays out its children in lines, wrapping to a new line when the current one is full." |
||||
|
Classes="h2" |
||||
|
DockPanel.Dock="Top" /> |
||||
|
|
||||
|
<Grid ColumnDefinitions="*,Auto"> |
||||
|
<Border BorderBrush="{DynamicResource CatalogBaseLowColor}" |
||||
|
BorderThickness="1" |
||||
|
Margin="0,10,10,10" |
||||
|
Grid.Column="0"> |
||||
|
<ItemsControl ItemsSource="{Binding Items}"> |
||||
|
<ItemsControl.ItemsPanel> |
||||
|
<ItemsPanelTemplate> |
||||
|
<WrapPanel ItemsAlignment="{Binding ItemsAlignment}" |
||||
|
Orientation="{Binding Orientation}" |
||||
|
ItemWidth="{Binding EffectiveItemWidth}" |
||||
|
ItemHeight="{Binding EffectiveItemHeight}" |
||||
|
ItemSpacing="{Binding ItemSpacing}" |
||||
|
LineSpacing="{Binding LineSpacing}" /> |
||||
|
</ItemsPanelTemplate> |
||||
|
</ItemsControl.ItemsPanel> |
||||
|
<ItemsControl.ItemTemplate> |
||||
|
<DataTemplate> |
||||
|
<Border Classes="item" Padding="{Binding Padding}"> |
||||
|
<TextBlock Text="{Binding Number}" /> |
||||
|
</Border> |
||||
|
</DataTemplate> |
||||
|
</ItemsControl.ItemTemplate> |
||||
|
</ItemsControl> |
||||
|
</Border> |
||||
|
|
||||
|
<StackPanel Width="220" |
||||
|
Margin="0,10,0,0" |
||||
|
Spacing="4" |
||||
|
VerticalAlignment="Top" |
||||
|
Grid.Column="1"> |
||||
|
|
||||
|
<TextBlock Text="ItemsAlignment" /> |
||||
|
<ComboBox Name="ItemsAlignmentSelector" |
||||
|
HorizontalAlignment="Stretch" |
||||
|
ItemsSource="{Binding ItemsAlignments}" |
||||
|
SelectedItem="{Binding ItemsAlignment}" /> |
||||
|
|
||||
|
<TextBlock Text="Orientation" Margin="0,8,0,0" /> |
||||
|
<ComboBox Name="OrientationSelector" |
||||
|
HorizontalAlignment="Stretch" |
||||
|
ItemsSource="{Binding Orientations}" |
||||
|
SelectedItem="{Binding Orientation}" /> |
||||
|
|
||||
|
<TextBlock Text="{Binding ItemSpacing, StringFormat='{}ItemSpacing: {0:F0}'}" |
||||
|
Margin="0,8,0,0" /> |
||||
|
<Slider Name="ItemSpacingSlider" |
||||
|
Minimum="0" |
||||
|
Maximum="40" |
||||
|
TickFrequency="5" |
||||
|
IsSnapToTickEnabled="True" |
||||
|
Value="{Binding ItemSpacing}" /> |
||||
|
|
||||
|
<TextBlock Text="{Binding LineSpacing, StringFormat='{}LineSpacing: {0:F0}'}" /> |
||||
|
<Slider Name="LineSpacingSlider" |
||||
|
Minimum="0" |
||||
|
Maximum="40" |
||||
|
TickFrequency="5" |
||||
|
IsSnapToTickEnabled="True" |
||||
|
Value="{Binding LineSpacing}" /> |
||||
|
|
||||
|
<CheckBox Name="ItemWidthCheckBox" Margin="0,8,0,0" |
||||
|
IsChecked="{Binding IsItemWidthEnabled}" |
||||
|
Content="{Binding ItemWidthValue, StringFormat='{}ItemWidth: {0:F0}'}" /> |
||||
|
<Slider Name="ItemWidthSlider" |
||||
|
Minimum="20" |
||||
|
Maximum="250" |
||||
|
TickFrequency="10" |
||||
|
IsSnapToTickEnabled="True" |
||||
|
Value="{Binding ItemWidthValue}" |
||||
|
IsEnabled="{Binding IsItemWidthEnabled}" /> |
||||
|
|
||||
|
<CheckBox Name="ItemHeightCheckBox" |
||||
|
IsChecked="{Binding IsItemHeightEnabled}" |
||||
|
Content="{Binding ItemHeightValue, StringFormat='{}ItemHeight: {0:F0}'}" /> |
||||
|
<Slider Name="ItemHeightSlider" |
||||
|
Minimum="20" |
||||
|
Maximum="250" |
||||
|
TickFrequency="10" |
||||
|
IsSnapToTickEnabled="True" |
||||
|
Value="{Binding ItemHeightValue}" |
||||
|
IsEnabled="{Binding IsItemHeightEnabled}" /> |
||||
|
|
||||
|
</StackPanel> |
||||
|
|
||||
|
</Grid> |
||||
|
|
||||
|
</DockPanel> |
||||
|
</ContentPage> |
||||
@ -0,0 +1,13 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using ControlCatalog.ViewModels; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class WrapPanelPage : ContentPage |
||||
|
{ |
||||
|
public WrapPanelPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
DataContext = new WrapPanelPageViewModel(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Layout; |
||||
|
using Avalonia.Media; |
||||
|
using MiniMvvm; |
||||
|
|
||||
|
namespace ControlCatalog.ViewModels |
||||
|
{ |
||||
|
public sealed class FlexItemViewModel : ViewModelBase |
||||
|
{ |
||||
|
internal const FlexAlignItems AlignSelfAuto = (FlexAlignItems)(-1); |
||||
|
|
||||
|
private FlexAlignItems _alignSelf; |
||||
|
|
||||
|
private bool _isSelected; |
||||
|
private bool _isVisible = true; |
||||
|
|
||||
|
private FlexAlignItems _alignSelfItem = AlignSelfAuto; |
||||
|
private int _order; |
||||
|
private double _shrink = 1.0; |
||||
|
private double _grow; |
||||
|
private double _basisValue = 100.0; |
||||
|
private FlexBasisKind _basisKind; |
||||
|
private HorizontalAlignment _horizontalAlignment; |
||||
|
private VerticalAlignment _verticalAlignment; |
||||
|
|
||||
|
public FlexItemViewModel(int value) |
||||
|
{ |
||||
|
Value = value; |
||||
|
|
||||
|
_alignSelf = AlignSelfItem == AlignSelfAuto ? default(FlexAlignItems) : AlignSelfItem; |
||||
|
|
||||
|
var color = Random.Shared.Next(); |
||||
|
|
||||
|
Color = new SolidColorBrush((uint)color); |
||||
|
} |
||||
|
|
||||
|
public int Value { get; } |
||||
|
|
||||
|
public Brush Color { get; } |
||||
|
|
||||
|
public bool IsSelected |
||||
|
{ |
||||
|
get => _isSelected; |
||||
|
set => this.RaiseAndSetIfChanged(ref _isSelected, value); |
||||
|
} |
||||
|
|
||||
|
public bool IsVisible |
||||
|
{ |
||||
|
get => _isVisible; |
||||
|
set => this.RaiseAndSetIfChanged(ref _isVisible, value); |
||||
|
} |
||||
|
|
||||
|
public FlexAlignItems AlignSelfItem |
||||
|
{ |
||||
|
get => _alignSelfItem; |
||||
|
set |
||||
|
{ |
||||
|
this.RaiseAndSetIfChanged(ref _alignSelfItem, value); |
||||
|
this.RaisePropertyChanged(nameof(AlignSelf)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public FlexAlignItems? AlignSelf => _alignSelf; |
||||
|
|
||||
|
public int Order |
||||
|
{ |
||||
|
get => _order; |
||||
|
set => this.RaiseAndSetIfChanged(ref _order, value); |
||||
|
} |
||||
|
|
||||
|
public double Shrink |
||||
|
{ |
||||
|
get => _shrink; |
||||
|
set => this.RaiseAndSetIfChanged(ref _shrink, value); |
||||
|
} |
||||
|
|
||||
|
public double Grow |
||||
|
{ |
||||
|
get => _grow; |
||||
|
set => this.RaiseAndSetIfChanged(ref _grow, value); |
||||
|
} |
||||
|
|
||||
|
public double BasisValue |
||||
|
{ |
||||
|
get => _basisValue; |
||||
|
set |
||||
|
{ |
||||
|
this.RaiseAndSetIfChanged(ref _basisValue, value); |
||||
|
this.RaisePropertyChanged(nameof(Basis)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public FlexBasisKind BasisKind |
||||
|
{ |
||||
|
get => _basisKind; |
||||
|
set |
||||
|
{ |
||||
|
this.RaiseAndSetIfChanged(ref _basisKind, value); |
||||
|
this.RaisePropertyChanged(nameof(Basis)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public FlexBasis Basis => new(_basisValue, _basisKind); |
||||
|
|
||||
|
public HorizontalAlignment HorizontalAlignment |
||||
|
{ |
||||
|
get => _horizontalAlignment; |
||||
|
set => this.RaiseAndSetIfChanged(ref _horizontalAlignment, value); |
||||
|
} |
||||
|
|
||||
|
public VerticalAlignment VerticalAlignment |
||||
|
{ |
||||
|
get => _verticalAlignment; |
||||
|
set => this.RaiseAndSetIfChanged(ref _verticalAlignment, value); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
using System; |
||||
|
using System.Collections; |
||||
|
using System.Collections.ObjectModel; |
||||
|
using System.Linq; |
||||
|
using System.Windows.Input; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Layout; |
||||
|
using MiniMvvm; |
||||
|
|
||||
|
namespace ControlCatalog.ViewModels |
||||
|
{ |
||||
|
public sealed class FlexViewModel : ViewModelBase |
||||
|
{ |
||||
|
private readonly ObservableCollection<FlexItemViewModel> _numbers; |
||||
|
|
||||
|
private FlexDirection _direction = FlexDirection.Row; |
||||
|
private FlexJustifyContent _justifyContent = FlexJustifyContent.FlexStart; |
||||
|
private FlexAlignItems _alignItems = FlexAlignItems.FlexStart; |
||||
|
private FlexAlignContent _alignContent = FlexAlignContent.FlexStart; |
||||
|
private FlexWrap _wrap = FlexWrap.Wrap; |
||||
|
|
||||
|
private int _columnSpacing = 64; |
||||
|
private int _rowSpacing = 32; |
||||
|
|
||||
|
private int _currentNumber = 41; |
||||
|
|
||||
|
private FlexItemViewModel? _selectedItem; |
||||
|
|
||||
|
public FlexViewModel() |
||||
|
{ |
||||
|
_numbers = new ObservableCollection<FlexItemViewModel>(Enumerable.Range(1, 40).Select(x => new FlexItemViewModel(x))); |
||||
|
|
||||
|
Numbers = new ReadOnlyObservableCollection<FlexItemViewModel>(_numbers); |
||||
|
|
||||
|
AddItemCommand = MiniCommand.Create(AddItem); |
||||
|
RemoveItemCommand = MiniCommand.Create(RemoveItem); |
||||
|
} |
||||
|
|
||||
|
public IEnumerable DirectionValues { get; } = Enum.GetValues<FlexDirection>(); |
||||
|
|
||||
|
public IEnumerable JustifyContentValues { get; } = Enum.GetValues<FlexJustifyContent>(); |
||||
|
|
||||
|
public IEnumerable AlignItemsValues { get; } = Enum.GetValues<FlexAlignItems>(); |
||||
|
|
||||
|
public IEnumerable AlignContentValues { get; } = Enum.GetValues<FlexAlignContent>(); |
||||
|
|
||||
|
public IEnumerable WrapValues { get; } = Enum.GetValues<FlexWrap>(); |
||||
|
|
||||
|
public IEnumerable FlexBasisKindValues { get; } = Enum.GetValues<FlexBasisKind>(); |
||||
|
|
||||
|
public IEnumerable HorizontalAlignmentValues { get; } = Enum.GetValues<HorizontalAlignment>(); |
||||
|
|
||||
|
public IEnumerable VerticalAlignmentValues { get; } = Enum.GetValues<VerticalAlignment>(); |
||||
|
|
||||
|
public IEnumerable AlignSelfValues { get; } = Enum.GetValues<FlexAlignItems>().Cast<FlexAlignItems>().Prepend(FlexItemViewModel.AlignSelfAuto); |
||||
|
|
||||
|
public FlexDirection Direction |
||||
|
{ |
||||
|
get => _direction; |
||||
|
set => this.RaiseAndSetIfChanged(ref _direction, value); |
||||
|
} |
||||
|
|
||||
|
public FlexJustifyContent JustifyContent |
||||
|
{ |
||||
|
get => _justifyContent; |
||||
|
set => this.RaiseAndSetIfChanged(ref _justifyContent, value); |
||||
|
} |
||||
|
|
||||
|
public FlexAlignItems AlignItems |
||||
|
{ |
||||
|
get => _alignItems; |
||||
|
set => this.RaiseAndSetIfChanged(ref _alignItems, value); |
||||
|
} |
||||
|
|
||||
|
public FlexAlignContent AlignContent |
||||
|
{ |
||||
|
get => _alignContent; |
||||
|
set => this.RaiseAndSetIfChanged(ref _alignContent, value); |
||||
|
} |
||||
|
|
||||
|
public FlexWrap Wrap |
||||
|
{ |
||||
|
get => _wrap; |
||||
|
set => this.RaiseAndSetIfChanged(ref _wrap, value); |
||||
|
} |
||||
|
|
||||
|
public int ColumnSpacing |
||||
|
{ |
||||
|
get => _columnSpacing; |
||||
|
set => this.RaiseAndSetIfChanged(ref _columnSpacing, value); |
||||
|
} |
||||
|
|
||||
|
public int RowSpacing |
||||
|
{ |
||||
|
get => _rowSpacing; |
||||
|
set => this.RaiseAndSetIfChanged(ref _rowSpacing, value); |
||||
|
} |
||||
|
|
||||
|
public ReadOnlyObservableCollection<FlexItemViewModel> Numbers { get; } |
||||
|
|
||||
|
public FlexItemViewModel? SelectedItem |
||||
|
{ |
||||
|
get => _selectedItem; |
||||
|
set => this.RaiseAndSetIfChanged(ref _selectedItem, value); |
||||
|
} |
||||
|
|
||||
|
public ICommand AddItemCommand { get; } |
||||
|
|
||||
|
public ICommand RemoveItemCommand { get; } |
||||
|
|
||||
|
private void AddItem() => _numbers.Add(new FlexItemViewModel(_currentNumber++)); |
||||
|
|
||||
|
private void RemoveItem() |
||||
|
{ |
||||
|
if (SelectedItem is null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_numbers.Remove(SelectedItem); |
||||
|
|
||||
|
SelectedItem.IsSelected = false; |
||||
|
SelectedItem = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using MiniMvvm; |
||||
|
|
||||
|
namespace ControlCatalog.ViewModels; |
||||
|
|
||||
|
public class PlatformSettingsViewModel : ViewModelBase |
||||
|
{ |
||||
|
private IPlatformSettings? _platformSettings; |
||||
|
private PlatformColorValues? _colorValues; |
||||
|
private string? _preferredLanguage; |
||||
|
|
||||
|
public void Subscribe(IPlatformSettings? platformSettings) |
||||
|
{ |
||||
|
_platformSettings = platformSettings; |
||||
|
|
||||
|
if (platformSettings is not null) |
||||
|
{ |
||||
|
OnColorValuesChanged(platformSettings, platformSettings.GetColorValues()); |
||||
|
OnPreferredLanguageChanged(platformSettings, EventArgs.Empty); |
||||
|
|
||||
|
platformSettings?.ColorValuesChanged += OnColorValuesChanged; |
||||
|
platformSettings?.PreferredApplicationLanguageChanged += OnPreferredLanguageChanged; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Unsubscribe() |
||||
|
{ |
||||
|
if (_platformSettings != null) |
||||
|
{ |
||||
|
_platformSettings.ColorValuesChanged -= OnColorValuesChanged; |
||||
|
_platformSettings.PreferredApplicationLanguageChanged -= OnPreferredLanguageChanged; |
||||
|
_platformSettings = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnColorValuesChanged(object? sender, PlatformColorValues e) |
||||
|
{ |
||||
|
_colorValues = e; |
||||
|
RaisePropertyChanged(nameof(ThemeVariant)); |
||||
|
RaisePropertyChanged(nameof(ContrastPreference)); |
||||
|
RaisePropertyChanged(nameof(AccentColor1)); |
||||
|
RaisePropertyChanged(nameof(AccentColor2)); |
||||
|
RaisePropertyChanged(nameof(AccentColor3)); |
||||
|
} |
||||
|
|
||||
|
private void OnPreferredLanguageChanged(object? sender, EventArgs e) |
||||
|
{ |
||||
|
_preferredLanguage = ((IPlatformSettings)sender!).PreferredApplicationLanguage; |
||||
|
RaisePropertyChanged(nameof(PreferredLanguage)); |
||||
|
} |
||||
|
|
||||
|
public string PreferredLanguage => _preferredLanguage ?? "Not available"; |
||||
|
|
||||
|
public string ThemeVariant => _colorValues?.ThemeVariant.ToString() ?? "Not available"; |
||||
|
|
||||
|
public string ContrastPreference => _colorValues?.ContrastPreference.ToString() ?? "Not available"; |
||||
|
|
||||
|
public Color AccentColor1 => _colorValues?.AccentColor1 ?? Colors.Gray; |
||||
|
|
||||
|
public Color AccentColor2 => _colorValues?.AccentColor2 ?? Colors.Gray; |
||||
|
|
||||
|
public Color AccentColor3 => _colorValues?.AccentColor3 ?? Colors.Gray; |
||||
|
|
||||
|
public string HoldWaitDuration => _platformSettings?.HoldWaitDuration.ToString() ?? "Not available"; |
||||
|
|
||||
|
public string TapSizeTouch => _platformSettings?.GetTapSize(PointerType.Touch).ToString() ?? "Not available"; |
||||
|
|
||||
|
public string TapSizeMouse => _platformSettings?.GetTapSize(PointerType.Mouse).ToString() ?? "Not available"; |
||||
|
|
||||
|
public string DoubleTapSizeTouch => _platformSettings?.GetDoubleTapSize(PointerType.Touch).ToString() ?? "Not available"; |
||||
|
|
||||
|
public string DoubleTapSizeMouse => _platformSettings?.GetDoubleTapSize(PointerType.Mouse).ToString() ?? "Not available"; |
||||
|
|
||||
|
public string DoubleTapTimeTouch => _platformSettings?.GetDoubleTapTime(PointerType.Touch).ToString() ?? "Not available"; |
||||
|
|
||||
|
public string DoubleTapTimeMouse => _platformSettings?.GetDoubleTapTime(PointerType.Mouse).ToString() ?? "Not available"; |
||||
|
} |
||||
@ -0,0 +1,246 @@ |
|||||
|
using System.Collections.ObjectModel; |
||||
|
using MiniMvvm; |
||||
|
|
||||
|
namespace ControlCatalog.ViewModels; |
||||
|
|
||||
|
public sealed class TableViewPageViewModel : ViewModelBase |
||||
|
{ |
||||
|
public ObservableCollection<Country> Countries { get; } = |
||||
|
[ |
||||
|
new("Afghanistan", "ASIA (EX. NEAR EAST)", 31056997, 647500, 700), |
||||
|
new("Albania", "EASTERN EUROPE", 3581655, 28748, 4500), |
||||
|
new("Algeria", "NORTHERN AFRICA", 32930091, 2381740, 6000), |
||||
|
new("American Samoa", "OCEANIA", 57794, 199, 8000), |
||||
|
new("Andorra", "WESTERN EUROPE", 71201, 468, 19000), |
||||
|
new("Angola", "SUB-SAHARAN AFRICA", 12127071, 1246700, 1900), |
||||
|
new("Anguilla", "LATIN AMER. & CARIB", 13477, 102, 8600), |
||||
|
new("Antigua & Barbuda", "LATIN AMER. & CARIB", 69108, 443, 11000), |
||||
|
new("Argentina", "LATIN AMER. & CARIB", 39921833, 2766890, 11200), |
||||
|
new("Armenia", "C.W. OF IND. STATES", 2976372, 29800, 3500), |
||||
|
new("Aruba", "LATIN AMER. & CARIB", 71891, 193, 28000), |
||||
|
new("Australia", "OCEANIA", 20264082, 7686850, 29000), |
||||
|
new("Austria", "WESTERN EUROPE", 8192880, 83870, 30000), |
||||
|
new("Azerbaijan", "C.W. OF IND. STATES", 7961619, 86600, 3400), |
||||
|
new("The Bahamas", "LATIN AMER. & CARIB", 303770, 13940, 16700), |
||||
|
new("Bahrain", "NEAR EAST", 698585, 665, 16900), |
||||
|
new("Bangladesh", "ASIA (EX. NEAR EAST)", 147365352, 144000, 1900), |
||||
|
new("Barbados", "LATIN AMER. & CARIB", 279912, 431, 15700), |
||||
|
new("Belarus", "C.W. OF IND. STATES", 10293011, 207600, 6100), |
||||
|
new("Belgium", "WESTERN EUROPE", 10379067, 30528, 29100), |
||||
|
new("Belize", "LATIN AMER. & CARIB", 287730, 22966, 4900), |
||||
|
new("Benin", "SUB-SAHARAN AFRICA", 7862944, 112620, 1100), |
||||
|
new("Bermuda", "NORTHERN AMERICA", 65773, 53, 36000), |
||||
|
new("Bhutan", "ASIA (EX. NEAR EAST)", 2279723, 47000, 1300), |
||||
|
new("Bolivia", "LATIN AMER. & CARIB", 8989046, 1098580, 2400), |
||||
|
new("Bosnia & Herzegovina", "EASTERN EUROPE", 4498976, 51129, 6100), |
||||
|
new("Botswana", "SUB-SAHARAN AFRICA", 1639833, 600370, 9000), |
||||
|
new("Brazil", "LATIN AMER. & CARIB", 188078227, 8511965, 7600), |
||||
|
new("British Virgin Is.", "LATIN AMER. & CARIB", 23098, 153, 16000), |
||||
|
new("Brunei", "ASIA (EX. NEAR EAST)", 379444, 5770, 18600), |
||||
|
new("Bulgaria", "EASTERN EUROPE", 7385367, 110910, 7600), |
||||
|
new("Burkina Faso", "SUB-SAHARAN AFRICA", 13902972, 274200, 1100), |
||||
|
new("Burma", "ASIA (EX. NEAR EAST)", 47382633, 678500, 1800), |
||||
|
new("Burundi", "SUB-SAHARAN AFRICA", 8090068, 27830, 600), |
||||
|
new("Cambodia", "ASIA (EX. NEAR EAST)", 13881427, 181040, 1900), |
||||
|
new("Cameroon", "SUB-SAHARAN AFRICA", 17340702, 475440, 1800), |
||||
|
new("Canada", "NORTHERN AMERICA", 33098932, 9984670, 29800), |
||||
|
new("Cape Verde", "SUB-SAHARAN AFRICA", 420979, 4033, 1400), |
||||
|
new("Cayman Islands", "LATIN AMER. & CARIB", 45436, 262, 35000), |
||||
|
new("Central African Rep.", "SUB-SAHARAN AFRICA", 4303356, 622984, 1100), |
||||
|
new("Chad", "SUB-SAHARAN AFRICA", 9944201, 1284000, 1200), |
||||
|
new("Chile", "LATIN AMER. & CARIB", 16134219, 756950, 9900), |
||||
|
new("China", "ASIA (EX. NEAR EAST)", 1313973713, 9596960, 5000), |
||||
|
new("Colombia", "LATIN AMER. & CARIB", 43593035, 1138910, 6300), |
||||
|
new("Comoros", "SUB-SAHARAN AFRICA", 690948, 2170, 700), |
||||
|
new("Congo, Dem.Rep.", "SUB - SAHARAN AFRICA", 62660551, 2345410, 700), |
||||
|
new("Congo, Repub.of the", "SUB - SAHARAN AFRICA", 3702314, 342000, 700), |
||||
|
new("Cook Islands", "OCEANIA", 21388, 240, 5000), |
||||
|
new("Costa Rica", "LATIN AMER. & CARIB", 4075261, 51100, 9100), |
||||
|
new("Cote d'Ivoire", "SUB-SAHARAN AFRICA", 17654843, 322460, 1400), |
||||
|
new("Croatia", "EASTERN EUROPE", 4494749, 56542, 10600), |
||||
|
new("Cuba", "LATIN AMER. & CARIB", 11382820, 110860, 2900), |
||||
|
new("Cyprus", "NEAR EAST", 784301, 9250, 19200), |
||||
|
new("Czech Republic", "EASTERN EUROPE", 10235455, 78866, 15700), |
||||
|
new("Denmark", "WESTERN EUROPE", 5450661, 43094, 31100), |
||||
|
new("Djibouti", "SUB-SAHARAN AFRICA", 486530, 23000, 1300), |
||||
|
new("Dominica", "LATIN AMER. & CARIB", 68910, 754, 5400), |
||||
|
new("Dominican Republic", "LATIN AMER. & CARIB", 9183984, 48730, 6000), |
||||
|
new("East Timor", "ASIA (EX. NEAR EAST)", 1062777, 15007, 500), |
||||
|
new("Ecuador", "LATIN AMER. & CARIB", 13547510, 283560, 3300), |
||||
|
new("Egypt", "NORTHERN AFRICA", 78887007, 1001450, 4000), |
||||
|
new("El Salvador", "LATIN AMER. & CARIB", 6822378, 21040, 4800), |
||||
|
new("Equatorial Guinea", "SUB-SAHARAN AFRICA", 540109, 28051, 2700), |
||||
|
new("Eritrea", "SUB-SAHARAN AFRICA", 4786994, 121320, 700), |
||||
|
new("Estonia", "BALTICS", 1324333, 45226, 12300), |
||||
|
new("Ethiopia", "SUB-SAHARAN AFRICA", 74777981, 1127127, 700), |
||||
|
new("Faroe Islands", "WESTERN EUROPE", 47246, 1399, 22000), |
||||
|
new("Fiji", "OCEANIA", 905949, 18270, 5800), |
||||
|
new("Finland", "WESTERN EUROPE", 5231372, 338145, 27400), |
||||
|
new("France", "WESTERN EUROPE", 60876136, 547030, 27600), |
||||
|
new("French Guiana", "LATIN AMER. & CARIB", 199509, 91000, 8300), |
||||
|
new("French Polynesia", "OCEANIA", 274578, 4167, 17500), |
||||
|
new("Gabon", "SUB-SAHARAN AFRICA", 1424906, 267667, 5500), |
||||
|
new("Gambia, The", "SUB - SAHARAN AFRICA", 1641564, 11300, 1700), |
||||
|
new("Gaza Strip", "NEAR EAST", 1428757, 360, 600), |
||||
|
new("Georgia", "C.W. OF IND. STATES", 4661473, 69700, 2500), |
||||
|
new("Germany", "WESTERN EUROPE", 82422299, 357021, 27600), |
||||
|
new("Ghana", "SUB-SAHARAN AFRICA", 22409572, 239460, 2200), |
||||
|
new("Gibraltar", "WESTERN EUROPE", 27928, 7, 17500), |
||||
|
new("Greece", "WESTERN EUROPE", 10688058, 131940, 20000), |
||||
|
new("Greenland", "NORTHERN AMERICA", 56361, 2166086, 20000), |
||||
|
new("Grenada", "LATIN AMER. & CARIB", 89703, 344, 5000), |
||||
|
new("Guadeloupe", "LATIN AMER. & CARIB", 452776, 1780, 8000), |
||||
|
new("Guam", "OCEANIA", 171019, 541, 21000), |
||||
|
new("Guatemala", "LATIN AMER. & CARIB", 12293545, 108890, 4100), |
||||
|
new("Guernsey", "WESTERN EUROPE", 65409, 78, 20000), |
||||
|
new("Guinea", "SUB-SAHARAN AFRICA", 9690222, 245857, 2100), |
||||
|
new("Guinea-Bissau", "SUB-SAHARAN AFRICA", 1442029, 36120, 800), |
||||
|
new("Guyana", "LATIN AMER. & CARIB", 767245, 214970, 4000), |
||||
|
new("Haiti", "LATIN AMER. & CARIB", 8308504, 27750, 1600), |
||||
|
new("Honduras", "LATIN AMER. & CARIB", 7326496, 112090, 2600), |
||||
|
new("Hong Kong", "ASIA (EX. NEAR EAST)", 6940432, 1092, 28800), |
||||
|
new("Hungary", "EASTERN EUROPE", 9981334, 93030, 13900), |
||||
|
new("Iceland", "WESTERN EUROPE", 299388, 103000, 30900), |
||||
|
new("India", "ASIA (EX. NEAR EAST)", 1095351995, 3287590, 2900), |
||||
|
new("Indonesia", "ASIA (EX. NEAR EAST)", 245452739, 1919440, 3200), |
||||
|
new("Iran", "ASIA (EX. NEAR EAST)", 68688433, 1648000, 7000), |
||||
|
new("Iraq", "NEAR EAST", 26783383, 437072, 1500), |
||||
|
new("Ireland", "WESTERN EUROPE", 4062235, 70280, 29600), |
||||
|
new("Isle of Man", "WESTERN EUROPE", 75441, 572, 21000), |
||||
|
new("Israel", "NEAR EAST", 6352117, 20770, 19800), |
||||
|
new("Italy", "WESTERN EUROPE", 58133509, 301230, 26700), |
||||
|
new("Jamaica", "LATIN AMER. & CARIB", 2758124, 10991, 3900), |
||||
|
new("Japan", "ASIA (EX. NEAR EAST)", 127463611, 377835, 28200), |
||||
|
new("Jersey", "WESTERN EUROPE", 91084, 116, 24800), |
||||
|
new("Jordan", "NEAR EAST", 5906760, 92300, 4300), |
||||
|
new("Kazakhstan", "C.W. OF IND. STATES", 15233244, 2717300, 6300), |
||||
|
new("Kenya", "SUB-SAHARAN AFRICA", 34707817, 582650, 1000), |
||||
|
new("Kiribati", "OCEANIA", 105432, 811, 800), |
||||
|
new("North Korea", "ASIA(EX.NEAR EAST)", 23113019, 120540, 1300), |
||||
|
new("South Korea", "ASIA(EX.NEAR EAST)", 48846823, 98480, 17800), |
||||
|
new("Kuwait", "NEAR EAST", 2418393, 17820, 19000), |
||||
|
new("Kyrgyzstan", "C.W. OF IND. STATES", 5213898, 198500, 1600), |
||||
|
new("Laos", "ASIA (EX. NEAR EAST)", 6368481, 236800, 1700), |
||||
|
new("Latvia", "BALTICS", 2274735, 64589, 10200), |
||||
|
new("Lebanon", "NEAR EAST", 3874050, 10400, 4800), |
||||
|
new("Lesotho", "SUB-SAHARAN AFRICA", 2022331, 30355, 3000), |
||||
|
new("Liberia", "SUB-SAHARAN AFRICA", 3042004, 111370, 1000), |
||||
|
new("Libya", "NORTHERN AFRICA", 5900754, 1759540, 6400), |
||||
|
new("Liechtenstein", "WESTERN EUROPE", 33987, 160, 25000), |
||||
|
new("Lithuania", "BALTICS", 3585906, 65200, 11400), |
||||
|
new("Luxembourg", "WESTERN EUROPE", 474413, 2586, 55100), |
||||
|
new("Macau", "ASIA (EX. NEAR EAST)", 453125, 28, 19400), |
||||
|
new("Macedonia", "EASTERN EUROPE", 2050554, 25333, 6700), |
||||
|
new("Madagascar", "SUB-SAHARAN AFRICA", 18595469, 587040, 800), |
||||
|
new("Malawi", "SUB-SAHARAN AFRICA", 13013926, 118480, 600), |
||||
|
new("Malaysia", "ASIA (EX. NEAR EAST)", 24385858, 329750, 9000), |
||||
|
new("Maldives", "ASIA (EX. NEAR EAST)", 359008, 300, 3900), |
||||
|
new("Mali", "SUB-SAHARAN AFRICA", 11716829, 1240000, 900), |
||||
|
new("Malta", "WESTERN EUROPE", 400214, 316, 17700), |
||||
|
new("Marshall Islands", "OCEANIA", 60422, 11854, 1600), |
||||
|
new("Martinique", "LATIN AMER. & CARIB", 436131, 1100, 14400), |
||||
|
new("Mauritania", "SUB-SAHARAN AFRICA", 3177388, 1030700, 1800), |
||||
|
new("Mauritius", "SUB-SAHARAN AFRICA", 1240827, 2040, 11400), |
||||
|
new("Mayotte", "SUB-SAHARAN AFRICA", 201234, 374, 2600), |
||||
|
new("Mexico", "LATIN AMER. & CARIB", 107449525, 1972550, 9000), |
||||
|
new("Micronesia, Fed.St.", "OCEANIA", 108004, 702, 2000), |
||||
|
new("Moldova", "C.W. OF IND. STATES", 4466706, 33843, 1800), |
||||
|
new("Monaco", "WESTERN EUROPE", 32543, 2, 27000), |
||||
|
new("Mongolia", "ASIA (EX. NEAR EAST)", 2832224, 1564116, 1800), |
||||
|
new("Montserrat", "LATIN AMER. & CARIB", 9439, 102, 3400), |
||||
|
new("Morocco", "NORTHERN AFRICA", 33241259, 446550, 4000), |
||||
|
new("Mozambique", "SUB-SAHARAN AFRICA", 19686505, 801590, 1200), |
||||
|
new("Namibia", "SUB-SAHARAN AFRICA", 2044147, 825418, 7200), |
||||
|
new("Nauru", "OCEANIA", 13287, 21, 5000), |
||||
|
new("Nepal", "ASIA (EX. NEAR EAST)", 28287147, 147181, 1400), |
||||
|
new("Netherlands", "WESTERN EUROPE", 16491461, 41526, 28600), |
||||
|
new("Netherlands Antilles", "LATIN AMER. & CARIB", 221736, 960, 11400), |
||||
|
new("New Caledonia", "OCEANIA", 219246, 19060, 15000), |
||||
|
new("New Zealand", "OCEANIA", 4076140, 268680, 21600), |
||||
|
new("Nicaragua", "LATIN AMER. & CARIB", 5570129, 129494, 2300), |
||||
|
new("Niger", "SUB-SAHARAN AFRICA", 12525094, 1267000, 800), |
||||
|
new("Nigeria", "SUB-SAHARAN AFRICA", 131859731, 923768, 900), |
||||
|
new("N. Mariana Islands", "OCEANIA", 82459, 477, 12500), |
||||
|
new("Norway", "WESTERN EUROPE", 4610820, 323802, 37800), |
||||
|
new("Oman", "NEAR EAST", 3102229, 212460, 13100), |
||||
|
new("Pakistan", "ASIA (EX. NEAR EAST)", 165803560, 803940, 2100), |
||||
|
new("Palau", "OCEANIA", 20579, 458, 9000), |
||||
|
new("Panama", "LATIN AMER. & CARIB", 3191319, 78200, 6300), |
||||
|
new("Papua New Guinea", "OCEANIA", 5670544, 462840, 2200), |
||||
|
new("Paraguay", "LATIN AMER. & CARIB", 6506464, 406750, 4700), |
||||
|
new("Peru", "LATIN AMER. & CARIB", 28302603, 1285220, 5100), |
||||
|
new("Philippines", "ASIA (EX. NEAR EAST)", 89468677, 300000, 4600), |
||||
|
new("Poland", "EASTERN EUROPE", 38536869, 312685, 11100), |
||||
|
new("Portugal", "WESTERN EUROPE", 10605870, 92391, 18000), |
||||
|
new("Puerto Rico", "LATIN AMER. & CARIB", 3927188, 13790, 16800), |
||||
|
new("Qatar", "NEAR EAST", 885359, 11437, 21500), |
||||
|
new("Reunion", "SUB-SAHARAN AFRICA", 787584, 2517, 5800), |
||||
|
new("Romania", "EASTERN EUROPE", 22303552, 237500, 7000), |
||||
|
new("Russia", "C.W. OF IND. STATES", 142893540, 17075200, 8900), |
||||
|
new("Rwanda", "SUB-SAHARAN AFRICA", 8648248, 26338, 1300), |
||||
|
new("Saint Helena", "SUB-SAHARAN AFRICA", 7502, 413, 2500), |
||||
|
new("Saint Kitts & Nevis", "LATIN AMER. & CARIB", 39129, 261, 8800), |
||||
|
new("Saint Lucia", "LATIN AMER. & CARIB", 168458, 616, 5400), |
||||
|
new("St Pierre & Miquelon", "NORTHERN AMERICA", 7026, 242, 6900), |
||||
|
new("Saint Vincent and the Grenadines", "LATIN AMER. & CARIB", 117848, 389, 2900), |
||||
|
new("Samoa", "OCEANIA", 176908, 2944, 5600), |
||||
|
new("San Marino", "WESTERN EUROPE", 29251, 61, 34600), |
||||
|
new("Sao Tome & Principe", "SUB-SAHARAN AFRICA", 193413, 1001, 1200), |
||||
|
new("Saudi Arabia", "NEAR EAST", 27019731, 1960582, 11800), |
||||
|
new("Senegal", "SUB-SAHARAN AFRICA", 11987121, 196190, 1600), |
||||
|
new("Serbia", "EASTERN EUROPE", 9396411, 88361, 2200), |
||||
|
new("Seychelles", "SUB-SAHARAN AFRICA", 81541, 455, 7800), |
||||
|
new("Sierra Leone", "SUB-SAHARAN AFRICA", 6005250, 71740, 500), |
||||
|
new("Singapore", "ASIA (EX. NEAR EAST)", 4492150, 693, 23700), |
||||
|
new("Slovakia", "EASTERN EUROPE", 5439448, 48845, 13300), |
||||
|
new("Slovenia", "EASTERN EUROPE", 2010347, 20273, 19000), |
||||
|
new("Solomon Islands", "OCEANIA", 552438, 28450, 1700), |
||||
|
new("Somalia", "SUB-SAHARAN AFRICA", 8863338, 637657, 500), |
||||
|
new("South Africa", "SUB-SAHARAN AFRICA", 44187637, 1219912, 10700), |
||||
|
new("Spain", "WESTERN EUROPE", 40397842, 504782, 22000), |
||||
|
new("Sri Lanka", "ASIA (EX. NEAR EAST)", 20222240, 65610, 3700), |
||||
|
new("Sudan", "SUB-SAHARAN AFRICA", 41236378, 2505810, 1900), |
||||
|
new("Suriname", "LATIN AMER. & CARIB", 439117, 163270, 4000), |
||||
|
new("Swaziland", "SUB-SAHARAN AFRICA", 1136334, 17363, 4900), |
||||
|
new("Sweden", "WESTERN EUROPE", 9016596, 449964, 26800), |
||||
|
new("Switzerland", "WESTERN EUROPE", 7523934, 41290, 32700), |
||||
|
new("Syria", "NEAR EAST", 18881361, 185180, 3300), |
||||
|
new("Taiwan", "ASIA (EX. NEAR EAST)", 23036087, 35980, 23400), |
||||
|
new("Tajikistan", "C.W. OF IND. STATES", 7320815, 143100, 1000), |
||||
|
new("Tanzania", "SUB-SAHARAN AFRICA", 37445392, 945087, 600), |
||||
|
new("Thailand", "ASIA (EX. NEAR EAST)", 64631595, 514000, 7400), |
||||
|
new("Togo", "SUB-SAHARAN AFRICA", 5548702, 56785, 1500), |
||||
|
new("Tonga", "OCEANIA", 114689, 748, 2200), |
||||
|
new("Trinidad & Tobago", "LATIN AMER. & CARIB", 1065842, 5128, 9500), |
||||
|
new("Tunisia", "NORTHERN AFRICA", 10175014, 163610, 6900), |
||||
|
new("Turkey", "NEAR EAST", 70413958, 780580, 6700), |
||||
|
new("Turkmenistan", "C.W. OF IND. STATES", 5042920, 488100, 5800), |
||||
|
new("Turks & Caicos Is", "LATIN AMER. & CARIB", 21152, 430, 9600), |
||||
|
new("Tuvalu", "OCEANIA", 11810, 26, 1100), |
||||
|
new("Uganda", "SUB-SAHARAN AFRICA", 28195754, 236040, 1400), |
||||
|
new("Ukraine", "C.W. OF IND. STATES", 46710816, 603700, 5400), |
||||
|
new("United Arab Emirates", "NEAR EAST", 2602713, 82880, 23200), |
||||
|
new("United Kingdom", "WESTERN EUROPE", 60609153, 244820, 27700), |
||||
|
new("United States", "NORTHERN AMERICA", 298444215, 9631420, 37800), |
||||
|
new("Uruguay", "LATIN AMER. & CARIB", 3431932, 176220, 12800), |
||||
|
new("Uzbekistan", "C.W. OF IND. STATES", 27307134, 447400, 1700), |
||||
|
new("Vanuatu", "OCEANIA", 208869, 12200, 2900), |
||||
|
new("Venezuela", "LATIN AMER. & CARIB", 25730435, 912050, 4800), |
||||
|
new("Vietnam", "ASIA (EX. NEAR EAST)", 84402966, 329560, 2500), |
||||
|
new("Virgin Islands", "LATIN AMER. & CARIB", 108605, 1910, 17200), |
||||
|
new("Wallis and Futuna", "OCEANIA", 16025, 274, 3700), |
||||
|
new("West Bank", "NEAR EAST", 2460492, 5860, 800), |
||||
|
new("Yemen", "NEAR EAST", 21456188, 527970, 800), |
||||
|
new("Zambia", "SUB-SAHARAN AFRICA", 11502010, 752614, 800), |
||||
|
new("Zimbabwe", "SUB-SAHARAN AFRICA", 12236805, 390580, 1900) |
||||
|
]; |
||||
|
|
||||
|
public sealed class Country(string name, string region, int population, int area, int gdp) |
||||
|
{ |
||||
|
public string Name { get; set; } = name; |
||||
|
public string Region { get; set; } = region; |
||||
|
public int Population { get; set; } = population; |
||||
|
public int Area { get; set; } = area; // Square miles
|
||||
|
public int GDP { get; set; } = gdp; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,105 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Avalonia; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Layout; |
||||
|
using MiniMvvm; |
||||
|
|
||||
|
namespace ControlCatalog.ViewModels; |
||||
|
|
||||
|
public class WrapPanelPageViewModel : ViewModelBase |
||||
|
{ |
||||
|
public WrapPanelItemsAlignment[] ItemsAlignments { get; } = Enum.GetValues<WrapPanelItemsAlignment>(); |
||||
|
|
||||
|
public Orientation[] Orientations { get; } = Enum.GetValues<Orientation>(); |
||||
|
|
||||
|
public IReadOnlyList<WrapPanelItemViewModel> Items { get; } = CreateItems(); |
||||
|
|
||||
|
public WrapPanelItemsAlignment ItemsAlignment |
||||
|
{ |
||||
|
get; |
||||
|
set => RaiseAndSetIfChanged(ref field, value); |
||||
|
} = WrapPanelItemsAlignment.Start; |
||||
|
|
||||
|
public Orientation Orientation |
||||
|
{ |
||||
|
get; |
||||
|
set => RaiseAndSetIfChanged(ref field, value); |
||||
|
} = Orientation.Horizontal; |
||||
|
|
||||
|
public double ItemSpacing |
||||
|
{ |
||||
|
get; |
||||
|
set => RaiseAndSetIfChanged(ref field, value); |
||||
|
} = 5; |
||||
|
|
||||
|
public double LineSpacing |
||||
|
{ |
||||
|
get; |
||||
|
set => RaiseAndSetIfChanged(ref field, value); |
||||
|
} = 5; |
||||
|
|
||||
|
public bool IsItemWidthEnabled |
||||
|
{ |
||||
|
get; |
||||
|
set |
||||
|
{ |
||||
|
if (RaiseAndSetIfChanged(ref field, value)) |
||||
|
RaisePropertyChanged(nameof(EffectiveItemWidth)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public double ItemWidthValue |
||||
|
{ |
||||
|
get; |
||||
|
set |
||||
|
{ |
||||
|
if (RaiseAndSetIfChanged(ref field, value)) |
||||
|
RaisePropertyChanged(nameof(EffectiveItemWidth)); |
||||
|
} |
||||
|
} = 140; |
||||
|
|
||||
|
public bool IsItemHeightEnabled |
||||
|
{ |
||||
|
get; |
||||
|
set |
||||
|
{ |
||||
|
if (RaiseAndSetIfChanged(ref field, value)) |
||||
|
RaisePropertyChanged(nameof(EffectiveItemHeight)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public double ItemHeightValue |
||||
|
{ |
||||
|
get; |
||||
|
set |
||||
|
{ |
||||
|
if (RaiseAndSetIfChanged(ref field, value)) |
||||
|
RaisePropertyChanged(nameof(EffectiveItemHeight)); |
||||
|
} |
||||
|
} = 90; |
||||
|
|
||||
|
public double EffectiveItemWidth |
||||
|
=> IsItemWidthEnabled ? ItemWidthValue : double.NaN; |
||||
|
|
||||
|
public double EffectiveItemHeight |
||||
|
=> IsItemHeightEnabled ? ItemHeightValue : double.NaN; |
||||
|
|
||||
|
private static WrapPanelItemViewModel[] CreateItems() |
||||
|
{ |
||||
|
var random = new Random(42); |
||||
|
var items = new WrapPanelItemViewModel[50]; |
||||
|
|
||||
|
for (var i = 0; i < items.Length; ++i) |
||||
|
items[i] = new WrapPanelItemViewModel(i + 1, new Thickness(random.Next(15, 56), random.Next(8, 31))); |
||||
|
|
||||
|
return items; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public class WrapPanelItemViewModel(int number, Thickness padding) |
||||
|
{ |
||||
|
public int Number { get; } = number; |
||||
|
|
||||
|
public Thickness Padding { get; } = padding; |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<Application |
||||
|
x:Class="ControlGallery.WinUI.App" |
||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
xmlns:local="using:ControlGallery.WinUI"> |
||||
|
<Application.Resources> |
||||
|
<ResourceDictionary> |
||||
|
<ResourceDictionary.MergedDictionaries> |
||||
|
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" /> |
||||
|
<!-- Other merged dictionaries here --> |
||||
|
</ResourceDictionary.MergedDictionaries> |
||||
|
<!-- Other app resources here --> |
||||
|
</ResourceDictionary> |
||||
|
</Application.Resources> |
||||
|
</Application> |
||||
@ -0,0 +1,40 @@ |
|||||
|
using global::Avalonia; |
||||
|
using global::Avalonia.Controls; |
||||
|
using global::Avalonia.Controls.ApplicationLifetimes; |
||||
|
using global::Avalonia.Skia; |
||||
|
using global::Avalonia.Win32; |
||||
|
using WinUIApplication = Microsoft.UI.Xaml.Application; |
||||
|
using LaunchActivatedEventArgs = Microsoft.UI.Xaml.LaunchActivatedEventArgs; |
||||
|
using Window = Microsoft.UI.Xaml.Window; |
||||
|
|
||||
|
namespace ControlGallery.WinUI |
||||
|
{ |
||||
|
public partial class App : WinUIApplication |
||||
|
{ |
||||
|
private Window? _window; |
||||
|
|
||||
|
internal static SingleViewLifetime Lifetime { get; } = new(); |
||||
|
|
||||
|
public App() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
} |
||||
|
|
||||
|
protected override void OnLaunched(LaunchActivatedEventArgs args) |
||||
|
{ |
||||
|
AppBuilder.Configure<ControlCatalog.App>() |
||||
|
.UseWin32() |
||||
|
.UseSkia() |
||||
|
.UseHarfBuzz() |
||||
|
.SetupWithLifetime(Lifetime); |
||||
|
|
||||
|
_window = new MainWindow(); |
||||
|
_window.Activate(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
internal sealed class SingleViewLifetime : ISingleViewApplicationLifetime |
||||
|
{ |
||||
|
public Control? MainView { get; set; } |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 432 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 456 B |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue