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: |
|||
- `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.Runtime.InteropServices; |
|||
|
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Threading; |
|||
|
|||
using ControlCatalog.Pages; |
|||
|
|||
using MonoMac.Foundation; |
|||
using MonoMac.WebKit; |
|||
|
|||
namespace ControlCatalog.Desktop; |
|||
|
|||
public class EmbedSampleMac : INativeDemoControl |
|||
{ |
|||
private static bool? s_webKitLoaded; |
|||
|
|||
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
|||
{ |
|||
// Note: We are using MonoMac for example purposes
|
|||
// It shouldn't be used in production apps
|
|||
MacHelper.EnsureInitialized(); |
|||
s_webKitLoaded ??= NativeLibrary.TryLoad("/System/Library/Frameworks/WebKit.framework/WebKit", out _); |
|||
|
|||
var webView = new WebView(); |
|||
Dispatcher.UIThread.Post(() => |
|||
if (s_webKitLoaded is false) |
|||
{ |
|||
webView.MainFrame.LoadRequest(new NSUrlRequest(new NSUrl( |
|||
isSecond ? "https://bing.com" : "https://google.com/"))); |
|||
}); |
|||
return createDefault(); |
|||
} |
|||
|
|||
// 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); |
|||
} |
|||
|
|||
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" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:pages="using:ControlCatalog.Pages" |
|||
Header="Composition" |
|||
x:Class="ControlCatalog.Pages.CompositionPage"> |
|||
<ContentPage Theme="{StaticResource ScrollPage}" Header="Implicit animations"> |
|||
<StackPanel> |
|||
<Grid ColumnDefinitions="*,10,40" Margin="0 0 40 0"> |
|||
<ItemsControl x:Name="Items"> |
|||
<ItemsControl.ItemsPanel> |
|||
<ItemsPanelTemplate> |
|||
<WrapPanel/> |
|||
</ItemsPanelTemplate> |
|||
</ItemsControl.ItemsPanel> |
|||
<ItemsControl.DataTemplates> |
|||
<DataTemplate DataType="pages:CompositionPageColorItem"> |
|||
<Border |
|||
pages:CompositionPage.EnableAnimations="True" |
|||
Padding="10" BorderBrush="Gray" BorderThickness="2" |
|||
Background="{Binding ColorBrush}" Width="100" Height="100" Margin="10"> |
|||
<TextBlock Text="{Binding ColorHexValue}"/> |
|||
</Border> |
|||
</DataTemplate> |
|||
</ItemsControl.DataTemplates> |
|||
</ItemsControl> |
|||
<GridSplitter Margin="2" BorderThickness="1" BorderBrush="Gray" |
|||
Background="#e0e0e0" Grid.Column="1" |
|||
ResizeDirection="Columns" ResizeBehavior="PreviousAndNext" |
|||
/> |
|||
<Border Grid.Column="2"> |
|||
<LayoutTransformControl HorizontalAlignment="Center" MinWidth="30"> |
|||
<LayoutTransformControl.LayoutTransform> |
|||
<RotateTransform Angle="90"/> |
|||
</LayoutTransformControl.LayoutTransform> |
|||
<TextBlock>Resize me</TextBlock> |
|||
</LayoutTransformControl> |
|||
</Border> |
|||
</Grid> |
|||
</StackPanel> |
|||
</ContentPage> |
|||
<ContentPage Theme="{StaticResource ScrollPage}" Header="Animation"> |
|||
<DockPanel> |
|||
<Button DockPanel.Dock="Top" Margin="10" Click="ButtonThreadSleep">Thread.Sleep(10000);</Button> |
|||
<Control x:Name="SolidVisualHost" /> |
|||
</DockPanel> |
|||
</ContentPage> |
|||
<ContentPage Theme="{StaticResource ScrollPage}" Header="Custom"> |
|||
<DockPanel> |
|||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal"> |
|||
<Button Margin="10" Click="ButtonThreadSleep">Thread.Sleep(10000);</Button> |
|||
<Button Margin="10" Click="ButtonStartCustomVisual">Start</Button> |
|||
<Button Margin="10" Click="ButtonStopCustomVisual">Stop</Button> |
|||
<CheckBox Margin="10" |
|||
x:Name="PreciseDirtyRectsCheckboxCustomVisual" |
|||
IsCheckedChanged="PreciseDirtyRectsCheckboxCustomVisualChanged" |
|||
>Precise dirty rects</CheckBox> |
|||
</StackPanel> |
|||
<Control x:Name="CustomVisualHost" /> |
|||
</DockPanel> |
|||
</ContentPage> |
|||
<TabbedPage |
|||
x:Class="ControlCatalog.Pages.CompositionPage" |
|||
xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:pages="using:ControlCatalog.Pages"> |
|||
<ContentPage Header="Implicit"> |
|||
<StackPanel> |
|||
<Grid Margin="0,0,40,0" ColumnDefinitions="*,10,40"> |
|||
<ItemsControl x:Name="Items"> |
|||
<ItemsControl.ItemsPanel> |
|||
<ItemsPanelTemplate> |
|||
<WrapPanel /> |
|||
</ItemsPanelTemplate> |
|||
</ItemsControl.ItemsPanel> |
|||
<ItemsControl.DataTemplates> |
|||
<DataTemplate DataType="pages:CompositionPageColorItem"> |
|||
<Border |
|||
Width="100" |
|||
Height="100" |
|||
Margin="10" |
|||
Padding="10" |
|||
pages:CompositionPage.EnableAnimations="True" |
|||
Background="{Binding ColorBrush}" |
|||
BorderBrush="Gray" |
|||
BorderThickness="2"> |
|||
<TextBlock Text="{Binding ColorHexValue}" /> |
|||
</Border> |
|||
</DataTemplate> |
|||
</ItemsControl.DataTemplates> |
|||
</ItemsControl> |
|||
<GridSplitter |
|||
Grid.Column="1" |
|||
Margin="2" |
|||
Background="#e0e0e0" |
|||
BorderBrush="Gray" |
|||
BorderThickness="1" |
|||
ResizeBehavior="PreviousAndNext" |
|||
ResizeDirection="Columns" /> |
|||
<Border Grid.Column="2"> |
|||
<LayoutTransformControl MinWidth="30" HorizontalAlignment="Center"> |
|||
<LayoutTransformControl.LayoutTransform> |
|||
<RotateTransform Angle="90" /> |
|||
</LayoutTransformControl.LayoutTransform> |
|||
<TextBlock>Resize me</TextBlock> |
|||
</LayoutTransformControl> |
|||
</Border> |
|||
</Grid> |
|||
</StackPanel> |
|||
</ContentPage> |
|||
<ContentPage Header="Animations"> |
|||
<DockPanel> |
|||
<Button |
|||
Margin="10" |
|||
Click="ButtonThreadSleep" |
|||
DockPanel.Dock="Top"> |
|||
Thread.Sleep(10000); |
|||
</Button> |
|||
<Control x:Name="SolidVisualHost" /> |
|||
</DockPanel> |
|||
</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> |
|||
|
|||
@ -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