Compare commits

..

2 Commits

Author SHA1 Message Date
qhy040404
3b86783493 migrate to TokenizingTextBox 2024-02-23 18:08:30 +08:00
qhy040404
e3adc2e595 finish suggestion methods 2024-02-23 14:10:09 +08:00
851 changed files with 8373 additions and 25010 deletions

View File

@@ -40,7 +40,7 @@ body:
attributes:
label: Snap Hutao 版本
description: 在应用标题,应用程序的反馈中心界面中可以找到
placeholder: 1.9.9.0
placeholder: 1.4.15.0
validations:
required: true
@@ -62,19 +62,20 @@ body:
description: 请设置一个你认为合适的分类,这将帮助我们快速定位问题
options:
- 安装和环境
- 游戏启动器
- 祈愿记录
- 成就管理
- 我的角色
- 角色信息面板
- 游戏启动器
- 实时便笺
- 养成计算
- 深境螺旋/胡桃数据库
- Wiki
- 米游社账号面板
- 每日签到奖励
- 胡桃通行证/胡桃云
- 用户界面
- 文件缓存
- 祈愿记录
- 玩家查询
- 胡桃数据库
- 用户界面
- 胡桃云
- 胡桃帐号
- 签到
- Wiki
- 公告
- 其它
validations:

View File

@@ -40,7 +40,7 @@ body:
attributes:
label: Snap Hutao Version
description: You can find the version in application's title bar
placeholder: e.g. 1.9.9.0
placeholder: e.g. 1.4.15.0
validations:
required: true
@@ -62,19 +62,20 @@ body:
description: Please select the most associated category of your issue
options:
- Installation and Environment
- Game Launcher
- Wish Export
- Achievement
- My Character
- Game Launcher
- Realtime Note
- Develop Plan
- Spiral Abyss
- Wiki
- MiHoYo Account Panel
- Daily Checkin Reward
- Hutao Passport/Hutao Cloud
- User Interface
- File Cache
- Wish Export
- Game Record
- Hutao Database
- User Interface
- Snap Hutao Cloud
- Snap Hutao Account
- Checkin
- Wiki
- Announcement
- Other
validations:

View File

@@ -1,15 +0,0 @@
<!--- Hi, thanks for considering make a PR contribution to Snap Hutao, we appreciate your work. -->
<!--- Before you create this PR, please fill the following form and checklist -->
## Description
<!--- Describe your changes -->
## Related Issue
<!--- If there's an associated issue, please use [GitHub Keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests) to link it -->
<!-- e.g. fix #999, resolve #999, close #999 -->
## Checklist
- [ ] The target PR branch is `develop` branch

View File

@@ -5,7 +5,6 @@ on:
branches:
- main
- develop
- 'feat/*'
paths-ignore:
- '.gitattributes'
- '.github/**'
@@ -74,7 +73,7 @@ jobs:
> [!IMPORTANT]
> 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
>
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 到 `受信任的根证书颁发机构` 以安装测试版安装包
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 以安装测试版安装包
"
echo $summary >> $Env:GITHUB_STEP_SUMMARY

View File

@@ -1,36 +1,41 @@
![HutaoRepoBanner3-en](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/7289da68-59cf-409b-bd85-4b5a01d0c091)
![HutaoRepoBanner2-20231222](https://github.com/DGP-Studio/Snap.Hutao/assets/10614984/2d178de1-95bc-44a1-a95e-20c5f11a8628)
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新功能相结合,提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed for modern Windows platform to improve the gaming experience for desktop players. By combining existing official resources with new features designed by the development team, it provides a complete and useful set of tools without the need to rely on mobile devices. Snap Hutao does not take any destructive modification to the game client to ensure the security of the toolkit.
## 安装 / Installation
## 下载使用 / Download
![](https://ci.appveyor.com/api/projects/status/n4s40t9llru4si9y?svg=true) [![GitHub Release](https://img.shields.io/github/release/DGP-Studio/Snap.Hutao?style=flat)](https://github.com/DGP-Studio/Snap.Hutao/releases/latest) [![Github All Releases](https://img.shields.io/github/downloads/DGP-Studio/Snap.Hutao/total.svg?style=flat)]()
---
你可以按照[快速开始](https://hut.ao/zh/quick-start.html)文档中提供的流程安装并设置 Snap Hutao
#### 使用安装器安装 / Install with Snap.Hutao.Depolyment Installer
You can follow the instructions in the [Quick Start](https://hut.ao/en/quick-start.html) document to install and set up Snap Hutao.
Snap.Hutao.Depolyment 是一个由 DGP-Studio 重新包装的 Windows 应用安装器,适用于缺少专业计算机知识的一般用户,可以在安装时同时解决缺少必要系统环境的问题。
## 本地化翻译 / Localization
Snap.Hutao.Depolyment is a Windows application installer repackaged by DGP-Studio for the users who lacks computer knowledge and can solve the problem of missing necessary system environment at the same time as the installation.
[![zh-TW translation](https://img.shields.io/badge/dynamic/json?color=blue&label=zh-TW&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27zh-TW%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![en translation](https://img.shields.io/badge/dynamic/json?color=blue&label=en&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27en%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![id translation](https://img.shields.io/badge/dynamic/json?color=blue&label=id&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27id%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![ja translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ja&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ja%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![ko translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ko&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ko%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![pt-PT translation](https://img.shields.io/badge/dynamic/json?color=blue&label=pt-PT&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27pt-PT%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao) [![ru translation](https://img.shields.io/badge/dynamic/json?color=blue&label=ru&style=flat&logo=crowdin&query=%24.progress[?(@.data.languageId==%27ru%27)].data.translationProgress&url=https%3A%2F%2Fbadges.awesome-crowdin.com%2Fstats-15670597-565845.json)](https://crowdin.com/project/snap-hutao)
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao.Deployment/releases/latest)
Snap Hutao 使用 [Crowdin](https://translate.hut.ao/) 作为客户端文本翻译平台,在该平台上你可以为你熟悉的语言提交翻译文本。我们感谢每一个为 Snap Hutao 做出贡献的社区成员,并且欢迎更多的朋友能参与到这个项目中。
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao.Deployment/-/releases)
Snap Hutao uses [Crowdin](https://translate.hut.ao/) as a client text translation platform where you can submit translated text for languages you are familiar with. We are grateful to every community member who has contributed to Snap Hutao and welcome more friends to participate in this project.
#### 使用 MSIX 包安装 / Install with MSIX Package
## 社区 / Community
直接使用 Snap Hutao MSIX 安装包,使用 Windows 内置的 App Installer 即可安装。如在安装中出现问题,请查阅我们的[常见问题](https://hut.ao/zh/advanced/FAQ.html)文档
[![Discord](https://img.shields.io/discord/952488447753465916?color=5865f2&label=%20Discord)](https://discord.gg/CcH5XtDtvR) [![QQ](https://img.shields.io/badge/QQ-EB1923?logo=tencent-qq&logoColor=white&label=567908135)](https://qm.qq.com/q/WJKykrY9W)
Install with Snap Hutao MSIX package, can be installed with Windows built-in App Installer. If you faced any issue, please check our [FAQ](https://hut.ao/en/advanced/FAQ.html) document.
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao/releases/latest)
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao/-/releases)
## 贡献 / Contribute
* [向我们提交 PR / Make Pull Requests](https://hut.ao/development/contribute.html)
* [为我们更新文档 / Enhance our Document](https://github.com/DGP-Studio/Snap.Hutao.Docs)
* [向我们提交 PR / Make Pull Requests](https://github.com/DGP-Studio/Snap.Hutao/pulls)
* [在 Crowdin 上进行本地化 / Translate Project on Crowdin](https://translate.hut.ao/)
* [为我们更新文档 / Enhance our Document ](https://github.com/DGP-Studio/Snap.Hutao.Docs)
## 特别感谢 / Special Thanks

View File

@@ -4,8 +4,8 @@
| Version | Supported |
| ------- | ------------------ |
| >=1.9.0 | :white_check_mark: |
| <1.9.0 | :x: |
| >=1.6.0 | :white_check_mark: |
| <1.6.0 | :x: |
## Reporting a Vulnerability

View File

@@ -11,15 +11,6 @@ var version = "version";
var repoDir = "repoDir";
var outputPath = "outputPath";
// Extension
static ProcessArgumentBuilder AppendIf(this ProcessArgumentBuilder builder, string text, bool condition)
{
return condition ? builder.Append(text) : builder;
}
// Properties
string solution
{
get => System.IO.Path.Combine(repoDir, "src", "Snap.Hutao", "Snap.Hutao.sln");
@@ -78,15 +69,6 @@ else if (AppVeyor.IsRunningOnAppVeyor)
})[..^2];
Information($"Version: {version}");
}
else // Local
{
repoDir = System.Environment.CurrentDirectory;
outputPath = System.IO.Path.Combine(repoDir, "src", "output");
version = System.DateTime.Now.ToString("yyyy.M.d.") + ((int)((System.DateTime.Now - System.DateTime.Today).TotalSeconds / 86400 * 65535)).ToString();
Information($"Version: {version}");
}
Task("Build")
.IsDependentOn("Build binary package")
@@ -130,17 +112,6 @@ Task("Generate AppxManifest")
Information("Using Release configuration");
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"CN=SignPath Foundation, O=SignPath Foundation, L=Lewes, S=Delaware, C=US\"");
}
else
{
Information("Using Local configuration.");
content = content
.Replace("Snap Hutao", "Snap Hutao Local")
.Replace("胡桃", "胡桃 Local")
.Replace("DGP Studio", "DGP Studio CI");
content = System.Text.RegularExpressions.Regex.Replace(content, " Name=\"([^\"]*)\"", " Name=\"E8B6E2B3-D2A0-4435-A81D-2A16AAF405C7\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Publisher=\"([^\"]*)\"", " Publisher=\"E=admin@dgp-studio.cn, CN=DGP Studio CI, OU=CI, O=DGP-Studio, L=San Jose, S=CA, C=US\"");
content = System.Text.RegularExpressions.Regex.Replace(content, " Version=\"([0-9\\.]+)\"", $" Version=\"{version}\"");
}
System.IO.File.WriteAllText(manifest, content);
@@ -166,7 +137,6 @@ Task("Build binary package")
.Append("/p:AppxPackageSigningEnabled=false")
.Append("/p:AppxBundle=Never")
.Append("/p:AppxPackageOutput=" + outputPath)
.AppendIf("/p:AlphaConstants=IS_ALPHA_BUILD", !AppVeyor.IsRunningOnAppVeyor)
};
DotNetBuild(project, settings);
@@ -203,10 +173,6 @@ Task("Build MSIX")
{
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao-{version}.msix");
}
else
{
arguments = "pack /d " + binPath + " /p " + System.IO.Path.Combine(outputPath, $"Snap.Hutao.Local-{version}.msix");
}
var p = StartProcess(
"makeappx.exe",
new ProcessSettings

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -110,6 +110,7 @@ dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.IDE0005.severity = warning
dotnet_diagnostic.IDE0060.severity = none
dotnet_diagnostic.IDE0290.severity = none
# SA1208: System using directives should be placed before other using directives
dotnet_diagnostic.SA1208.severity = none
@@ -320,8 +321,7 @@ dotnet_diagnostic.CA2227.severity = suggestion
# CA2251: 使用 “string.Equals”
dotnet_diagnostic.CA2251.severity = suggestion
csharp_style_prefer_primary_constructors = false:none
csharp_style_prefer_primary_constructors = true:suggestion
[*.vb]
#### 命名样式 ####

View File

@@ -1,28 +0,0 @@
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Test.BaseClassLibrary;
[TestClass]
public class UnsafeAccessorTest
{
[TestMethod]
public void UnsafeAccessorCanGetInterfaceProperty()
{
TestClass test = new();
int value = InternalGetInterfaceProperty(test);
Assert.AreEqual(3, value);
}
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_TestProperty")]
private static extern int InternalGetInterfaceProperty(ITestInterface instance);
internal interface ITestInterface
{
internal int TestProperty { get; }
}
internal sealed class TestClass : ITestInterface
{
public int TestProperty { get; } = 3;
}
}

View File

@@ -1,7 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
namespace Snap.Hutao.Test.PlatformExtensions;
@@ -12,10 +10,7 @@ public sealed class DependencyInjectionTest
.AddSingleton<IService, ServiceA>()
.AddSingleton<IService, ServiceB>()
.AddScoped<IScopedService, ServiceA>()
.AddKeyedTransient<IKeyedService, KeyedServiceA>("A")
.AddKeyedTransient<IKeyedService, KeyedServiceB>("B")
.AddTransient(typeof(IGenericService<>), typeof(GenericService<>))
.AddLogging(builder => builder.AddConsole())
.BuildServiceProvider();
[TestMethod]
@@ -46,22 +41,6 @@ public sealed class DependencyInjectionTest
}
}
[TestMethod]
public void LoggerWithInterfaceTypeCanBeResolved()
{
Assert.IsNotNull(services.GetService<ILogger<IScopedService>>());
Assert.IsNotNull(services.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(IScopedService)));
}
[TestMethod]
public void KeyedServicesCanBeResolvedAsEnumerable()
{
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("A"));
Assert.IsNotNull(services.GetRequiredKeyedService<IKeyedService>("B"));
Assert.AreEqual(0, services.GetServices<IKeyedService>().Count());
}
private interface IService
{
Guid Id { get; }
@@ -107,14 +86,4 @@ public sealed class DependencyInjectionTest
{
}
}
private interface IKeyedService;
private sealed class KeyedServiceA : IKeyedService
{
}
private sealed class KeyedServiceB : IKeyedService
{
}
}

View File

@@ -12,11 +12,10 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.3.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.3.1" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PackageReference Include="MSTest.TestAdapter" Version="3.2.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.2.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -6,7 +6,6 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.SettingsControls/SettingsCard/SettingsCard.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Loading.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Image/CachedImage.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Card.xaml"/>
@@ -23,14 +22,11 @@
<ResourceDictionary Source="ms-appx:///Control/Theme/PageOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/PivotOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/ScrollViewer.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/SegmentedOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/SettingsStyle.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Thickness.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/TransitionCollection.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/TokenizingTextBox/TokenizingTextBox.xaml"/>
<ResourceDictionary Source="ms-appx:///View/Card/Primitive/CardProgressBar.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style

View File

@@ -7,8 +7,7 @@ using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Core.Shell;
using System.Diagnostics;
namespace Snap.Hutao;
@@ -22,24 +21,24 @@ namespace Snap.Hutao;
[SuppressMessage("", "SH001")]
public sealed partial class App : Application
{
private const string ConsoleBanner = $"""
private const string ConsoleBanner = """
----------------------------------------------------------------
_____ _ _ _
/ ____| | | | | | |
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
_____ _ _ _
/ ____| | | | | | |
| (___ _ __ __ _ _ __ | |__| | _ _ | |_ __ _ ___
\___ \ | '_ \ / _` || '_ \ | __ || | | || __|/ _` | / _ \
____) || | | || (_| || |_) |_ | | | || |_| || |_| (_| || (_) |
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
| |
|_|
|_____/ |_| |_| \__,_|| .__/(_)|_| |_| \__,_| \__|\__,_| \___/
| |
|_|
Snap.Hutao is a open source software developed by DGP Studio.
Copyright (C) 2022 - 2024 DGP Studio, All Rights Reserved.
----------------------------------------------------------------
""";
private readonly IServiceProvider serviceProvider;
private readonly IAppActivation activation;
private readonly IActivation activation;
private readonly ILogger<App> logger;
/// <summary>
@@ -50,19 +49,13 @@ public sealed partial class App : Application
{
// Load app resource
InitializeComponent();
activation = serviceProvider.GetRequiredService<IAppActivation>();
activation = serviceProvider.GetRequiredService<IActivation>();
logger = serviceProvider.GetRequiredService<ILogger<App>>();
serviceProvider.GetRequiredService<ExceptionRecorder>().Record(this);
this.serviceProvider = serviceProvider;
}
public new void Exit()
{
XamlWindowLifetime.ApplicationExiting = true;
base.Exit();
}
/// <inheritdoc/>
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
@@ -76,16 +69,18 @@ public sealed partial class App : Application
return;
}
logger.LogColorizedInformation((ConsoleBanner, ConsoleColor.DarkYellow));
logger.LogInformation(ConsoleBanner);
LogDiagnosticInformation();
// Manually invoke
// manually invoke
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
activation.PostInitialization();
activation.Initialize();
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
}
catch (Exception ex)
catch
{
Debug.WriteLine(ex);
// AppInstance.GetCurrent() calls failed
Process.GetCurrentProcess().Kill();
}
}
@@ -94,8 +89,8 @@ public sealed partial class App : Application
{
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
logger.LogColorizedInformation(("FamilyName: {Name}", ConsoleColor.Blue), (runtimeOptions.FamilyName, ConsoleColor.Cyan));
logger.LogColorizedInformation(("Version: {Version}", ConsoleColor.Blue), (runtimeOptions.Version, ConsoleColor.Cyan));
logger.LogColorizedInformation(("LocalCache: {Path}", ConsoleColor.Blue), (runtimeOptions.LocalCache, ConsoleColor.Cyan));
logger.LogInformation("FamilyName: {name}", runtimeOptions.FamilyName);
logger.LogInformation("Version: {version}", runtimeOptions.Version);
logger.LogInformation("LocalCache: {folder}", runtimeOptions.LocalCache);
}
}

View File

@@ -1,83 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Control.TokenizingTextBox;
using System.Collections;
namespace Snap.Hutao.Control.AutoSuggestBox;
[DependencyProperty("FilterCommand", typeof(ICommand))]
[DependencyProperty("FilterCommandParameter", typeof(object))]
[DependencyProperty("AvailableTokens", typeof(IReadOnlyDictionary<string, SearchToken>))]
internal sealed partial class AutoSuggestTokenBox : TokenizingTextBox.TokenizingTextBox
{
public AutoSuggestTokenBox()
{
DefaultStyleKey = typeof(TokenizingTextBox.TokenizingTextBox);
TextChanged += OnFilterSuggestionRequested;
QuerySubmitted += OnQuerySubmitted;
TokenItemAdding += OnTokenItemAdding;
TokenItemAdded += OnTokenItemCollectionChanged;
TokenItemRemoved += OnTokenItemCollectionChanged;
}
private void OnFilterSuggestionRequested(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if (string.IsNullOrWhiteSpace(Text))
{
sender.ItemsSource = AvailableTokens
.OrderBy(kvp => kvp.Value.Kind)
.Select(kvp => kvp.Value);
}
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
{
sender.ItemsSource = AvailableTokens
.Where(kvp => kvp.Value.Value.Contains(Text, StringComparison.OrdinalIgnoreCase))
.OrderBy(kvp => kvp.Value.Kind)
.ThenBy(kvp => kvp.Value.Order)
.Select(kvp => kvp.Value)
.DefaultIfEmpty(SearchToken.NotFound);
}
}
private void OnQuerySubmitted(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
if (args.ChosenSuggestion is not null)
{
return;
}
CommandInvocation.TryExecute(FilterCommand, FilterCommandParameter);
}
private void OnTokenItemAdding(TokenizingTextBox.TokenizingTextBox sender, TokenItemAddingEventArgs args)
{
if (string.IsNullOrWhiteSpace(args.TokenText))
{
return;
}
if (AvailableTokens.GetValueOrDefault(args.TokenText) is { } token)
{
args.Item = token;
}
else
{
args.Cancel = true;
}
}
private void OnTokenItemCollectionChanged(TokenizingTextBox.TokenizingTextBox sender, object args)
{
if (args is SearchToken { Kind: SearchTokenKind.None } token)
{
((IList)sender.ItemsSource).Remove(token);
}
FilterCommand.TryExecute(FilterCommandParameter);
}
}

View File

@@ -1,38 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.UI;
namespace Snap.Hutao.Control.AutoSuggestBox;
internal sealed class SearchToken
{
public static readonly SearchToken NotFound = new(SearchTokenKind.None, SH.ControlAutoSuggestBoxNotFoundValue, 0);
public SearchToken(SearchTokenKind kind, string value, int order, Uri? iconUri = null, Uri? sideIconUri = null, Color? quality = null)
{
Value = value;
Kind = kind;
IconUri = iconUri;
SideIconUri = sideIconUri;
Quality = quality;
Order = order;
}
public SearchTokenKind Kind { get; }
public string Value { get; set; } = default!;
public Uri? IconUri { get; }
public Uri? SideIconUri { get; }
public Color? Quality { get; }
public int Order { get; }
public override string ToString()
{
return Value;
}
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.AutoSuggestBox;
internal enum SearchTokenKind
{
None,
ItemQuality,
WeaponType,
FightProperty,
ElementName,
AssociationType,
BodyType,
Avatar,
Weapon,
}

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.Control.Behavior;
@@ -46,6 +45,10 @@ internal sealed partial class InvokeCommandOnLoadedBehavior : BehaviorBase<UIEle
return;
}
executed = Command.TryExecute(CommandParameter);
if (Command is not null && Command.CanExecute(CommandParameter))
{
Command.Execute(CommandParameter);
executed = true;
}
}
}

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Extension;
namespace Snap.Hutao.Control.Behavior;
@@ -33,7 +32,6 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
protected override bool Uninitialize()
{
periodicTimerCancellationTokenSource.Cancel();
AssociatedObject.ActualThemeChanged -= OnActualThemeChanged;
return true;
}
@@ -51,7 +49,10 @@ internal sealed partial class PeriodicInvokeCommandOrOnActualThemeChangedBehavio
return;
}
Command.TryExecute(CommandParameter);
if (Command is not null && Command.CanExecute(CommandParameter))
{
Command.Execute(CommandParameter);
}
}
private async ValueTask RunCoreAsync()

View File

@@ -13,4 +13,6 @@ namespace Snap.Hutao.Control;
/// </summary>
[HighQuality]
[DependencyProperty("DataContext", typeof(object))]
internal sealed partial class BindingProxy : DependencyObject;
internal sealed partial class BindingProxy : DependencyObject
{
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal class ButtonBaseBuilder<TButton> : IButtonBaseBuilder<TButton>
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase, new()
{
public TButton Button { get; } = new();
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction.Extension;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal static class ButtonBaseBuilderExtension
{
public static TBuilder SetContent<TBuilder, TButton>(this TBuilder builder, object? content)
where TBuilder : IButtonBaseBuilder<TButton>
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
{
builder.Configure(builder => builder.Button.Content = content);
return builder;
}
public static TBuilder SetCommand<TBuilder, TButton>(this TBuilder builder, ICommand command)
where TBuilder : IButtonBaseBuilder<TButton>
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
{
builder.Configure(builder => builder.Button.Command = command);
return builder;
}
}

View File

@@ -1,8 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal sealed class ButtonBuilder : ButtonBaseBuilder<Button>;

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal static class ButtonBuilderExtension
{
public static ButtonBuilder SetContent(this ButtonBuilder builder, object? content)
{
return builder.SetContent<ButtonBuilder, Button>(content);
}
public static ButtonBuilder SetCommand(this ButtonBuilder builder, ICommand command)
{
return builder.SetCommand<ButtonBuilder, Button>(command);
}
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Control.Builder.ButtonBase;
internal interface IButtonBaseBuilder<TButton> : IBuilder
where TButton : Microsoft.UI.Xaml.Controls.Primitives.ButtonBase
{
TButton Button { get; }
}

View File

@@ -30,7 +30,7 @@ internal sealed class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, IN
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
public AdvancedCollectionView()
: this([])
: this(new List<T>(0))
{
}

View File

@@ -7,7 +7,7 @@ namespace Snap.Hutao.Control.Collection.AdvancedCollectionView;
internal sealed class VectorChangedEventArgs : IVectorChangedEventArgs
{
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = default!)
public VectorChangedEventArgs(CollectionChange cc, int index = -1, object item = null!)
{
CollectionChange = cc;
Index = (uint)index;

View File

@@ -6,7 +6,6 @@ using Windows.Foundation.Collections;
namespace Snap.Hutao.Control.Collection.Alternating;
[Obsolete("Use SettingsCard instead")]
[DependencyProperty("ItemAlternateBackground", typeof(Microsoft.UI.Xaml.Media.Brush))]
internal sealed partial class AlternatingItemsControl : ItemsControl
{

View File

@@ -3,7 +3,6 @@
namespace Snap.Hutao.Control.Collection.Alternating;
[Obsolete("Use SettingsCard instead")]
internal interface IAlternatingItem
{
public Microsoft.UI.Xaml.Media.Brush? Background { get; set; }

View File

@@ -3,7 +3,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Control;
@@ -41,6 +40,6 @@ internal abstract class DependencyValueConverter<TFrom, TTo> : DependencyObject,
/// <returns>源</returns>
public virtual TFrom ConvertBack(TTo to)
{
throw HutaoException.NotSupported();
throw Must.NeverHappen();
}
}

View File

@@ -1,18 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.Extension;
internal static class CommandInvocation
{
public static bool TryExecute(this ICommand? command, object? parameter = null)
{
if (command is not null && command.CanExecute(parameter))
{
command.Execute(parameter);
return true;
}
return false;
}
}

View File

@@ -2,13 +2,11 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Control.Extension;
internal static class DependencyObjectExtension
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static IServiceProvider ServiceProvider(this DependencyObject obj)
{
return Ioc.Default;

View File

@@ -28,20 +28,4 @@ internal static class FrameworkElementExtension
frameworkElement.IsRightTapEnabled = false;
frameworkElement.IsTabStop = false;
}
public static void InitializeDataContext<TDataContext>(this FrameworkElement frameworkElement, IServiceProvider? serviceProvider = default)
where TDataContext : class
{
IServiceProvider service = serviceProvider ?? Ioc.Default;
try
{
frameworkElement.DataContext = service.GetRequiredService<TDataContext>();
}
catch (Exception ex)
{
ILogger? logger = service.GetRequiredService(typeof(ILogger<>).MakeGenericType([frameworkElement.GetType()])) as ILogger;
logger?.LogError(ex, "Failed to initialize DataContext");
throw;
}
}
}

View File

@@ -7,8 +7,6 @@ namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("SquareLength", typeof(double), 0D, nameof(OnSquareLengthChanged), IsAttached = true, AttachedType = typeof(FrameworkElement))]
[DependencyProperty("IsActualThemeBindingEnabled", typeof(bool), false, nameof(OnIsActualThemeBindingEnabled), IsAttached = true, AttachedType = typeof(FrameworkElement))]
[DependencyProperty("ActualTheme", typeof(ElementTheme), ElementTheme.Default, IsAttached = true, AttachedType = typeof(FrameworkElement))]
public sealed partial class FrameworkElementHelper
{
private static void OnSquareLengthChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
@@ -17,22 +15,4 @@ public sealed partial class FrameworkElementHelper
element.Width = (double)e.NewValue;
element.Height = (double)e.NewValue;
}
private static void OnIsActualThemeBindingEnabled(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
FrameworkElement element = (FrameworkElement)dp;
if ((bool)e.NewValue)
{
element.ActualThemeChanged += OnActualThemeChanged;
}
else
{
element.ActualThemeChanged -= OnActualThemeChanged;
}
static void OnActualThemeChanged(FrameworkElement sender, object args)
{
SetActualTheme(sender, sender.ActualTheme);
}
}
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Control.Helper;
[SuppressMessage("", "SH001")]
[DependencyProperty("VisibilityObject", typeof(object), null, nameof(OnVisibilityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))]
[DependencyProperty("OpacityObject", typeof(object), null, nameof(OnOpacityObjectChanged), IsAttached = true, AttachedType = typeof(UIElement))]
public sealed partial class UIElementHelper
{
private static void OnVisibilityObjectChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)dp;
element.Visibility = e.NewValue is null ? Visibility.Collapsed : Visibility.Visible;
}
private static void OnOpacityObjectChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)dp;
element.Opacity = e.NewValue is null ? 0D : 1D;
}
}

View File

@@ -1,9 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Control.Extension;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.ExceptionService;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Image;
@@ -19,21 +19,22 @@ internal sealed class CachedImage : Implementation.ImageEx
/// </summary>
public CachedImage()
{
DefaultStyleKey = typeof(CachedImage);
DefaultStyleResourceUri = "ms-appx:///Control/Image/CachedImage.xaml".ToUri();
IsCacheEnabled = true;
EnableLazyLoading = false;
}
/// <inheritdoc/>
protected override async Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
protected override async Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
// We can only use Ioc to retrieve IImageCache, no IServiceProvider is available.
IImageCache imageCache = Ioc.Default.GetRequiredService<IImageCache>();
try
{
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
Verify.Operation(!string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
return file.ToUri();
return new BitmapImage(file.ToUri()); // BitmapImage initialize with a uri will increase image quality and loading speed.
}
catch (COMException)
{

View File

@@ -6,6 +6,7 @@
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{ThemeResource ApplicationForegroundThemeBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="LazyLoadingThreshold" Value="256"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="shci:CachedImage">

View File

@@ -168,7 +168,6 @@ internal abstract partial class CompositionImage : Microsoft.UI.Xaml.Controls.Co
if (surface.DecodedPhysicalSize.Size() <= 0D)
{
await Task.WhenAny(surfaceLoadTaskCompletionSource.Task, Task.Delay(5000, token)).ConfigureAwait(true);
await Task.Delay(50, token).ConfigureAwait(true);
}
LoadImageSurfaceCompleted(surface);

View File

@@ -7,14 +7,21 @@ using Windows.Media.Casting;
namespace Snap.Hutao.Control.Image.Implementation;
[DependencyProperty("NineGrid", typeof(Thickness))]
internal partial class ImageEx : ImageExBase
internal class ImageEx : ImageExBase
{
private static readonly DependencyProperty NineGridProperty = DependencyProperty.Register(nameof(NineGrid), typeof(Thickness), typeof(ImageEx), new PropertyMetadata(default(Thickness)));
public ImageEx()
: base()
{
}
public Thickness NineGrid
{
get => (Thickness)GetValue(NineGridProperty);
set => SetValue(NineGridProperty, value);
}
public override CompositionBrush GetAlphaMask()
{
if (IsInitialized && Image is Microsoft.UI.Xaml.Controls.Image image)

View File

@@ -6,6 +6,8 @@ using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using System.IO;
using Windows.Foundation;
namespace Snap.Hutao.Control.Image.Implementation;
@@ -18,6 +20,12 @@ namespace Snap.Hutao.Control.Image.Implementation;
[TemplatePart(Name = PartImage, Type = typeof(object))]
[TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))]
[DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("DecodePixelHeight", typeof(int), 0)]
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
[DependencyProperty("PlaceholderSource", typeof(object), default(object))]
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
[DependencyProperty("PlaceholderMargin", typeof(Thickness))]
@@ -33,6 +41,8 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
protected const string FailedState = "Failed";
private CancellationTokenSource? tokenSource;
private object? lazyLoadingSource;
private bool isInViewport;
public bool IsInitialized { get; private set; }
@@ -47,10 +57,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
public abstract CompositionBrush GetAlphaMask();
protected virtual Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
// By default we just use the built-in UWP image cache provided within the Image control.
return Task.FromResult<Uri?>(imageUri);
return Task.FromResult<ImageSource?>(new BitmapImage(imageUri));
}
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
@@ -69,10 +79,19 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
RemoveImageFailed(OnImageFailed);
Image = GetTemplateChild(PartImage);
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
IsInitialized = true;
SetSource(Source);
if (Source is null || !EnableLazyLoading || isInViewport)
{
lazyLoadingSource = null;
SetSource(Source);
}
else
{
lazyLoadingSource = Source;
}
AttachImageOpened(OnImageOpened);
AttachImageFailed(OnImageFailed);
@@ -128,6 +147,34 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
}
}
private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ImageExBase control)
{
return;
}
bool value = (bool)e.NewValue;
if (value)
{
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
control.InvalidateLazyLoading();
}
else
{
control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated;
}
}
private static void LazyLoadingThresholdChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is ImageExBase control && control.EnableLazyLoading)
{
control.InvalidateLazyLoading();
}
}
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ImageExBase control)
@@ -140,7 +187,15 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return;
}
control.SetSource(e.NewValue);
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
{
control.lazyLoadingSource = null;
control.SetSource(e.NewValue);
}
else
{
control.lazyLoadingSource = e.NewValue;
}
}
private static bool IsHttpUri(Uri uri)
@@ -148,8 +203,11 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return uri.IsAbsoluteUri && (uri.Scheme == "http" || uri.Scheme == "https");
}
private void AttachSource(BitmapImage? source, Uri? uri)
private void AttachSource(ImageSource? source)
{
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (Image is Microsoft.UI.Xaml.Controls.Image image)
{
image.Source = source;
@@ -163,16 +221,17 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
{
VisualStateManager.GoToState(this, UnloadedState, true);
}
else
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
{
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
source.UriSource = uri;
VisualStateManager.GoToState(this, LoadedState, true);
}
}
private void AttachPlaceholderSource(BitmapImage? source, Uri? uri)
private void AttachPlaceholderSource(ImageSource? source)
{
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
// We only need to call those methods if we fail in other cases before we get here.
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
{
image.Source = source;
@@ -181,17 +240,6 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
{
brush.ImageSource = source;
}
if (source is null)
{
VisualStateManager.GoToState(this, UnloadedState, true);
}
else
{
// https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-animations-and-media#optimize-image-resources
source.UriSource = uri;
VisualStateManager.GoToState(this, LoadedState, true);
}
}
private async void SetSource(object? source)
@@ -202,9 +250,10 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
}
tokenSource?.Cancel();
tokenSource = new CancellationTokenSource();
AttachSource(default, default);
AttachSource(null);
if (source is null)
{
@@ -213,6 +262,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
VisualStateManager.GoToState(this, LoadingState, true);
if (source as ImageSource is { } imageSource)
{
AttachSource(imageSource);
return;
}
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
@@ -255,15 +311,23 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
}
tokenSource?.Cancel();
tokenSource = new();
AttachPlaceholderSource(default, default);
tokenSource = new CancellationTokenSource();
AttachPlaceholderSource(null);
if (source is null)
{
return;
}
if (source as ImageSource is { } imageSource)
{
AttachPlaceholderSource(imageSource);
return;
}
if (source as Uri is not { } uri)
{
string? url = source as string ?? source.ToString();
@@ -285,13 +349,13 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return;
}
Uri? actualUri = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachPlaceholderSource(new BitmapImage(), actualUri);
AttachPlaceholderSource(img);
}
}
catch (OperationCanceledException)
@@ -310,13 +374,98 @@ internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control
return;
}
Uri? actualUri = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
if (IsCacheEnabled)
{
// Only attach our image if we still have a valid request.
AttachSource(new BitmapImage(), actualUri);
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
// Only attach our image if we still have a valid request.
AttachSource(img);
}
}
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
{
string source = imageUri.OriginalString;
const string base64Head = "base64,";
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
if (index >= 0)
{
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
ArgumentNullException.ThrowIfNull(tokenSource);
if (!tokenSource.IsCancellationRequested)
{
AttachSource(bitmap);
}
}
}
else
{
AttachSource(new BitmapImage(imageUri)
{
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
});
}
}
private void OnImageExBaseLayoutUpdated(object? sender, object e)
{
InvalidateLazyLoading();
}
private void InvalidateLazyLoading()
{
if (!IsLoaded)
{
isInViewport = false;
return;
}
// Find the first ascendant ScrollViewer, if not found, use the root element.
FrameworkElement? hostElement = default;
IEnumerable<FrameworkElement> ascendants = this.FindAscendants().OfType<FrameworkElement>();
foreach (FrameworkElement ascendant in ascendants)
{
hostElement = ascendant;
if (hostElement is Microsoft.UI.Xaml.Controls.ScrollViewer)
{
break;
}
}
if (hostElement is null)
{
isInViewport = false;
return;
}
Rect controlRect = TransformToVisual(hostElement)
.TransformBounds(new Rect(0, 0, ActualWidth, ActualHeight));
double lazyLoadingThreshold = LazyLoadingThreshold;
Rect hostRect = new(
0 - lazyLoadingThreshold,
0 - lazyLoadingThreshold,
hostElement.ActualWidth + (2 * lazyLoadingThreshold),
hostElement.ActualHeight + (2 * lazyLoadingThreshold));
if (controlRect.IntersectsWith(hostRect))
{
isInViewport = true;
if (lazyLoadingSource is not null)
{
object source = lazyLoadingSource;
lazyLoadingSource = null;
SetSource(source);
}
}
else
{
isInViewport = false;
}
}
}

View File

@@ -0,0 +1,151 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using System.Numerics;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
internal sealed class DefaultItemCollectionTransitionProvider : ItemCollectionTransitionProvider
{
private const double DefaultAnimationDurationInMs = 300.0;
static DefaultItemCollectionTransitionProvider()
{
AnimationSlowdownFactor = 1.0;
}
public static double AnimationSlowdownFactor { get; set; }
protected override bool ShouldAnimateCore(ItemCollectionTransition transition)
{
return true;
}
protected override void StartTransitions(IList<ItemCollectionTransition> transitions)
{
List<ItemCollectionTransition> addTransitions = [];
List<ItemCollectionTransition> removeTransitions = [];
List<ItemCollectionTransition> moveTransitions = [];
foreach (ItemCollectionTransition transition in addTransitions)
{
switch (transition.Operation)
{
case ItemCollectionTransitionOperation.Add:
addTransitions.Add(transition);
break;
case ItemCollectionTransitionOperation.Remove:
removeTransitions.Add(transition);
break;
case ItemCollectionTransitionOperation.Move:
moveTransitions.Add(transition);
break;
}
}
StartAddTransitions(addTransitions, removeTransitions.Count > 0, moveTransitions.Count > 0);
StartRemoveTransitions(removeTransitions);
StartMoveTransitions(moveTransitions, removeTransitions.Count > 0);
}
private static void StartAddTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveTransitions, bool hasMoveTransitions)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation fadeInAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeInAnimation.InsertKeyFrame(0.0f, 0.0f);
if (hasMoveTransitions && hasRemoveTransitions)
{
fadeInAnimation.InsertKeyFrame(0.66f, 0.0f);
}
else if (hasMoveTransitions || hasRemoveTransitions)
{
fadeInAnimation.InsertKeyFrame(0.5f, 0.0f);
}
fadeInAnimation.InsertKeyFrame(1.0f, 1.0f);
fadeInAnimation.Duration = TimeSpan.FromMilliseconds(
DefaultAnimationDurationInMs * ((hasRemoveTransitions ? 1 : 0) + (hasMoveTransitions ? 1 : 0) + 1) * AnimationSlowdownFactor);
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
visual.StartAnimation("Opacity", fadeInAnimation);
batch.End();
batch.Completed += (_, _) => progress.Complete();
}
}
private static void StartRemoveTransitions(IList<ItemCollectionTransition> transitions)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation fadeOutAnimation = compositor.CreateScalarKeyFrameAnimation();
fadeOutAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue");
fadeOutAnimation.InsertKeyFrame(1.0f, 0.0f);
fadeOutAnimation.Duration = TimeSpan.FromMilliseconds(DefaultAnimationDurationInMs * AnimationSlowdownFactor);
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
visual.StartAnimation(nameof(Visual.Opacity), fadeOutAnimation);
batch.End();
batch.Completed += (_, _) =>
{
visual.Opacity = 1.0f;
progress.Complete();
};
}
}
private static void StartMoveTransitions(IList<ItemCollectionTransition> transitions, bool hasRemoveAnimations)
{
foreach (ItemCollectionTransition transition in transitions)
{
ItemCollectionTransitionProgress progress = transition.Start();
Visual visual = ElementCompositionPreview.GetElementVisual(progress.Element);
Compositor compositor = visual.Compositor;
CompositionScopedBatch batch = compositor.CreateScopedBatch(CompositionBatchTypes.Animation);
// Animate offset.
if (transition.OldBounds.X != transition.NewBounds.X ||
transition.OldBounds.Y != transition.NewBounds.Y)
{
AnimateOffset(visual, compositor, transition.OldBounds, transition.NewBounds, hasRemoveAnimations);
}
batch.End();
batch.Completed += (_, _) => progress.Complete();
}
}
private static void AnimateOffset(Visual visual, Compositor compositor, Rect oldBounds, Rect newBounds, bool hasRemoveAnimations)
{
Vector2KeyFrameAnimation offsetAnimation = compositor.CreateVector2KeyFrameAnimation();
offsetAnimation.SetVector2Parameter("delta", new Vector2(
(float)(oldBounds.X - newBounds.X),
(float)(oldBounds.Y - newBounds.Y)));
offsetAnimation.SetVector2Parameter("final", default);
offsetAnimation.InsertExpressionKeyFrame(0.0f, "this.CurrentValue + delta");
if (hasRemoveAnimations)
{
offsetAnimation.InsertExpressionKeyFrame(0.5f, "delta");
}
offsetAnimation.InsertExpressionKeyFrame(1.0f, "final");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(
DefaultAnimationDurationInMs * ((hasRemoveAnimations ? 1 : 0) + 1) * AnimationSlowdownFactor);
visual.StartAnimation("TransformMatrix._41_42", offsetAnimation);
}
}

View File

@@ -18,12 +18,14 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = new UniformStaggeredLayoutState(context);
base.InitializeForContextCore(context);
}
/// <inheritdoc/>
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = null;
base.UninitializeForContextCore(context);
}
/// <inheritdoc/>
@@ -63,12 +65,12 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
/// <inheritdoc/>
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
if (context.ItemCount is 0)
if (context.ItemCount == 0)
{
return new Size(availableSize.Width, 0);
}
if ((context.RealizationRect.Width is 0) && (context.RealizationRect.Height is 0))
if ((context.RealizationRect.Width == 0) && (context.RealizationRect.Height == 0))
{
return new Size(availableSize.Width, 0.0f);
}
@@ -80,10 +82,16 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
(int numberOfColumns, double columnWidth) = GetNumberOfColumnsAndWidth(availableWidth, MinItemWidth, MinColumnSpacing);
if (columnWidth != state.ColumnWidth)
{
// The items will need to be remeasured
state.Clear();
}
state.ColumnWidth = columnWidth;
double totalWidth = ((state.ColumnWidth + MinColumnSpacing) * numberOfColumns) - MinColumnSpacing;
// adjust for column spacing on all columns expect the first
double totalWidth = state.ColumnWidth + ((numberOfColumns - 1) * (state.ColumnWidth + MinColumnSpacing));
if (totalWidth > availableWidth)
{
numberOfColumns--;
@@ -95,6 +103,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
if (numberOfColumns != state.NumberOfColumns)
{
// The items will not need to be remeasured, but they will need to go into new columns
state.ClearColumns();
}
@@ -161,7 +170,7 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
item.Element.Measure(new Size(state.ColumnWidth, availableHeight));
if (item.Height != item.Element.DesiredSize.Height)
{
// this item changed size; we need to recalculate layout for everything after this item
// this item changed size; we need to recalculate layout for everything after this
state.RemoveFromIndex(i + 1);
item.Height = item.Element.DesiredSize.Height;
columnHeights[columnIndex] = item.Top + item.Height;
@@ -192,16 +201,16 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
// Cycle through each column and arrange the items that are within the realization bounds
for (int columnIndex = 0; columnIndex < state.NumberOfColumns; columnIndex++)
{
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(state.GetColumnLayout(columnIndex)))
UniformStaggeredColumnLayout layout = state.GetColumnLayout(columnIndex);
foreach (ref readonly UniformStaggeredItem item in CollectionsMarshal.AsSpan(layout))
{
double bottom = item.Top + item.Height;
if (bottom < context.RealizationRect.Top)
{
// Element is above the realization bounds
// element is above the realization bounds
continue;
}
// Partial or fully in the view
if (item.Top <= context.RealizationRect.Bottom)
{
double itemHorizontalOffset = (state.ColumnWidth * columnIndex) + (MinColumnSpacing * columnIndex);
@@ -220,22 +229,21 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
return finalSize;
}
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double columnSpacing)
private static (int NumberOfColumns, double ColumnWidth) GetNumberOfColumnsAndWidth(double availableWidth, double minItemWidth, double minColumnSpacing)
{
// test if the width can fit in 2 items
if ((2 * minItemWidth) + columnSpacing > availableWidth)
if ((2 * minItemWidth) + minColumnSpacing > availableWidth)
{
return (1, availableWidth);
}
int columnCount = Math.Max(1, (int)((availableWidth + columnSpacing) / (minItemWidth + columnSpacing)));
double columnWidthWithSpacing = (availableWidth + columnSpacing) / columnCount;
return (columnCount, columnWidthWithSpacing - columnSpacing);
int columnCount = Math.Max(1, (int)((availableWidth + minColumnSpacing) / (minItemWidth + minColumnSpacing)));
double columnWidthAddSpacing = (availableWidth + minColumnSpacing) / columnCount;
return (columnCount, columnWidthAddSpacing - minColumnSpacing);
}
private static int GetLowestColumnIndex(in ReadOnlySpan<double> columnHeights)
{
// We want to find the leftest column with the lowest height
int columnIndex = 0;
double height = columnHeights[0];
for (int j = 1; j < columnHeights.Length; j++)
@@ -252,11 +260,13 @@ internal sealed partial class UniformStaggeredLayout : VirtualizingLayout
private static void OnMinItemWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((UniformStaggeredLayout)d).InvalidateMeasure();
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
}
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((UniformStaggeredLayout)d).InvalidateMeasure();
UniformStaggeredLayout panel = (UniformStaggeredLayout)d;
panel.InvalidateMeasure();
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Runtime.InteropServices;
@@ -66,6 +67,46 @@ internal sealed class UniformStaggeredLayoutState
return columnLayout[columnIndex];
}
/// <summary>
/// Clear everything that has been calculated.
/// </summary>
internal void Clear()
{
// https://github.com/DGP-Studio/Snap.Hutao/issues/1079
// The first element must be force refreshed otherwise
// it will use the old one realized
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Now we need to refresh the first element of each column
// https://github.com/DGP-Studio/Snap.Hutao/issues/1099
// Finally we need to refresh the whole layout when we reset
if (context.ItemCount > 0)
{
for (int i = 0; i < context.ItemCount; i++)
{
RecycleElementAt(i);
}
}
columnLayout.Clear();
items.Clear();
}
/// <summary>
/// Clear the layout columns so they will be recalculated.
/// </summary>
internal void ClearColumns()
{
columnLayout.Clear();
}
/// <summary>
/// Gets the estimated height of the layout.
/// </summary>
/// <returns>The estimated height of the layout.</returns>
/// <remarks>
/// If all of the items have been calculated then the actual height will be returned.
/// If all of the items have not been calculated then an estimated height will be calculated based on the average height of the items.
/// </remarks>
internal double GetHeight()
{
double desiredHeight = columnLayout.Values.Max(c => c.Height);
@@ -98,37 +139,10 @@ internal sealed class UniformStaggeredLayoutState
return desiredHeight;
}
internal void Clear()
{
RecycleElements();
ClearColumns();
ClearItems();
}
internal void ClearColumns()
{
columnLayout.Clear();
}
internal void ClearItems()
{
items.Clear();
}
internal void RecycleElements()
{
if (context.ItemCount > 0)
{
for (int i = 0; i < items.Count; i++)
{
RecycleElementAt(i);
}
}
}
internal void RecycleElementAt(int index)
{
context.RecycleElement(context.GetOrCreateElementAt(index));
UIElement element = context.GetOrCreateElementAt(index);
context.RecycleElement(element);
}
internal void RemoveFromIndex(int index)
@@ -161,7 +175,7 @@ internal sealed class UniformStaggeredLayoutState
{
for (int i = startIndex; i <= endIndex; i++)
{
if (i >= items.Count)
if (i > items.Count)
{
break;
}
@@ -170,7 +184,7 @@ internal sealed class UniformStaggeredLayoutState
item.Height = 0;
item.Top = 0;
// We must recycle all removed elements to ensure that it gets the correct context
// We must recycle all elements to ensure that it gets the correct context
RecycleElementAt(i);
}

View File

@@ -1,25 +0,0 @@
// Licensed to the .NET Fou// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
internal sealed class WrapItem
{
public WrapItem(int index)
{
Index = index;
}
public static Point EmptyPosition { get; } = new(float.NegativeInfinity, float.NegativeInfinity);
public int Index { get; }
public Size Size { get; set; } = Size.Empty;
public Point Position { get; set; } = EmptyPosition;
public UIElement? Element { get; set; }
}

View File

@@ -1,220 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System.Collections.Specialized;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
[DependencyProperty("HorizontalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))]
[DependencyProperty("VerticalSpacing", typeof(double), 0D, nameof(LayoutPropertyChanged))]
internal sealed partial class WrapLayout : VirtualizingLayout
{
protected override void InitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = new WrapLayoutState(context);
}
protected override void UninitializeForContextCore(VirtualizingLayoutContext context)
{
context.LayoutState = default;
}
protected override void OnItemsChangedCore(VirtualizingLayoutContext context, object source, NotifyCollectionChangedEventArgs args)
{
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
state.RemoveFromIndex(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Move:
int minIndex = Math.Min(args.NewStartingIndex, args.OldStartingIndex);
state.RemoveFromIndex(minIndex);
state.RecycleElementAt(args.OldStartingIndex);
state.RecycleElementAt(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Remove:
state.RemoveFromIndex(args.OldStartingIndex);
break;
case NotifyCollectionChangedAction.Replace:
state.RemoveFromIndex(args.NewStartingIndex);
state.RecycleElementAt(args.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Reset:
state.Clear();
break;
}
base.OnItemsChangedCore(context, source, args);
}
protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
if (context.ItemCount is 0)
{
return new Size(availableSize.Width, 0);
}
if ((context.RealizationRect.Width is 0) && (context.RealizationRect.Height is 0))
{
return new Size(availableSize.Width, 0.0f);
}
Size spacing = new(HorizontalSpacing, VerticalSpacing);
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
if (spacing != state.Spacing || state.AvailableWidth != availableSize.Width)
{
state.ClearPositions();
state.Spacing = spacing;
state.AvailableWidth = availableSize.Width;
}
double currentHeight = 0;
Point itemPosition = default;
for (int i = 0; i < context.ItemCount; ++i)
{
bool itemMeasured = false;
WrapItem item = state.GetItemAt(i);
if (item.Size == Size.Empty)
{
item.Element = context.GetOrCreateElementAt(i);
item.Element.Measure(availableSize);
item.Size = item.Element.DesiredSize;
itemMeasured = true;
}
Size itemSize = item.Size;
if (item.Position == WrapItem.EmptyPosition)
{
if (availableSize.Width < itemPosition.X + itemSize.Width)
{
// New Row
itemPosition.X = 0;
itemPosition.Y += currentHeight + spacing.Height;
currentHeight = 0;
}
item.Position = itemPosition;
}
itemPosition = item.Position;
double bottom = itemPosition.Y + itemSize.Height;
if (bottom < context.RealizationRect.Top)
{
// Item is "above" the bounds
if (item.Element is not null)
{
context.RecycleElement(item.Element);
item.Element = default;
}
continue;
}
else if (itemPosition.Y > context.RealizationRect.Bottom)
{
// Item is "below" the bounds.
if (item.Element is not null)
{
context.RecycleElement(item.Element);
item.Element = default;
}
// We don't need to measure anything below the bounds
break;
}
else if (!itemMeasured)
{
// Always measure elements that are within the bounds
item.Element = context.GetOrCreateElementAt(i);
item.Element.Measure(availableSize);
itemSize = item.Element.DesiredSize;
if (itemSize != item.Size)
{
// this item changed size; we need to recalculate layout for everything after this
state.RemoveFromIndex(i + 1);
item.Size = itemSize;
// did the change make it go into the new row?
if (availableSize.Width < itemPosition.X + itemSize.Width)
{
// New Row
itemPosition.X = 0;
itemPosition.Y += currentHeight + spacing.Height;
currentHeight = 0;
}
item.Position = itemPosition;
}
}
itemPosition.X += itemSize.Width + spacing.Width;
currentHeight = Math.Max(itemSize.Height, currentHeight);
}
return new Size(double.IsInfinity(availableSize.Width) ? 0 : Math.Ceiling(availableSize.Width), state.GetHeight());
}
protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize)
{
if (context.ItemCount > 0)
{
WrapLayoutState state = (WrapLayoutState)context.LayoutState;
for (int i = 0; i < context.ItemCount; ++i)
{
if (!ArrangeItem(context, state.GetItemAt(i)))
{
break;
}
}
}
return finalSize;
static bool ArrangeItem(VirtualizingLayoutContext context, WrapItem item)
{
if (item.Size == Size.Empty || item.Position == WrapItem.EmptyPosition)
{
return false;
}
Size size = item.Size;
Point position = item.Position;
if (context.RealizationRect.Top <= position.Y + size.Height && position.Y <= context.RealizationRect.Bottom)
{
// place the item
UIElement child = context.GetOrCreateElementAt(item.Index);
child.Arrange(new Rect(position, size));
}
else if (position.Y > context.RealizationRect.Bottom)
{
return false;
}
return true;
}
}
private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is WrapLayout layout)
{
layout.InvalidateMeasure();
layout.InvalidateArrange();
}
}
}

View File

@@ -1,109 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Controls;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.Control.Layout;
internal sealed class WrapLayoutState
{
private readonly List<WrapItem> items = [];
private readonly VirtualizingLayoutContext context;
public WrapLayoutState(VirtualizingLayoutContext context)
{
this.context = context;
}
public Orientation Orientation { get; private set; }
public Size Spacing { get; set; }
public double AvailableWidth { get; set; }
public WrapItem GetItemAt(int index)
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
if (index <= (items.Count - 1))
{
return items[index];
}
else
{
WrapItem item = new(index);
items.Add(item);
return item;
}
}
public void Clear()
{
for (int i = 0; i < items.Count; i++)
{
RecycleElementAt(i);
}
items.Clear();
}
public void RemoveFromIndex(int index)
{
if (index >= items.Count)
{
// Item was added/removed but we haven't realized that far yet
return;
}
int numToRemove = items.Count - index;
items.RemoveRange(index, numToRemove);
}
public void ClearPositions()
{
foreach (ref readonly WrapItem item in CollectionsMarshal.AsSpan(items))
{
item.Position = WrapItem.EmptyPosition;
}
}
public double GetHeight()
{
if (items.Count is 0)
{
return 0;
}
Point? lastPosition = default;
double maxHeight = 0;
Span<WrapItem> itemSpan = CollectionsMarshal.AsSpan(items);
for (int i = items.Count - 1; i >= 0; --i)
{
ref readonly WrapItem item = ref itemSpan[i];
if (item.Position == WrapItem.EmptyPosition || item.Size == Size.Empty)
{
continue;
}
if (lastPosition is not null && lastPosition.Value.Y > item.Position.Y)
{
// This is a row above the last item.
break;
}
lastPosition = item.Position;
maxHeight = Math.Max(maxHeight, item.Size.Height);
}
return lastPosition?.Y + maxHeight ?? 0;
}
public void RecycleElementAt(int index)
{
context.RecycleElement(context.GetOrCreateElementAt(index));
}
}

View File

@@ -17,7 +17,7 @@ internal class Loading : Microsoft.UI.Xaml.Controls.ContentControl
public Loading()
{
DefaultStyleKey = typeof(Loading);
DefaultStyleResourceUri = "ms-appx:///Control/Loading.xaml".ToUri();
DefaultStyleResourceUri = new("ms-appx:///Control/Loading.xaml");
}
public bool IsLoading

View File

@@ -12,6 +12,7 @@ internal sealed class UInt32Extension : MarkupExtension
protected override object ProvideValue()
{
return XamlBindingHelper.ConvertValue(typeof(uint), Value);
_ = uint.TryParse(Value, out uint result);
return result;
}
}

View File

@@ -8,19 +8,45 @@ using Windows.UI;
namespace Snap.Hutao.Control.Media;
/// <summary>
/// RGBA 颜色
/// </summary>
[HighQuality]
internal struct Rgba32
{
/// <summary>
/// R
/// </summary>
public byte R;
/// <summary>
/// G
/// </summary>
public byte G;
/// <summary>
/// B
/// </summary>
public byte B;
/// <summary>
/// A
/// </summary>
public byte A;
/// <summary>
/// 构造一个新的 RGBA8 颜色
/// </summary>
/// <param name="hex">色值字符串</param>
public Rgba32(string hex)
: this(hex.Length == 6 ? Convert.ToUInt32($"{hex}FF", 16) : Convert.ToUInt32(hex, 16))
{
}
/// <summary>
/// 使用 RGBA 代码初始化新的结构
/// </summary>
/// <param name="xrgbaCode">RGBA 代码</param>
public unsafe Rgba32(uint xrgbaCode)
{
// uint layout: 0xRRGGBBAA is AABBGGRR
@@ -54,6 +80,11 @@ internal struct Rgba32
return *(Color*)&rgba;
}
/// <summary>
/// 从 HSL 颜色转换
/// </summary>
/// <param name="hsl">HSL 颜色</param>
/// <returns>RGBA8颜色</returns>
public static Rgba32 FromHsl(Hsla32 hsl)
{
double chroma = (1 - Math.Abs((2 * hsl.L) - 1)) * hsl.S;
@@ -107,6 +138,10 @@ internal struct Rgba32
return new(r, g, b, a);
}
/// <summary>
/// 转换到 HSL 颜色
/// </summary>
/// <returns>HSL 颜色</returns>
public readonly Hsla32 ToHsl()
{
const double toDouble = 1.0 / 255;

View File

@@ -1,14 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
// Some part of this file came from:
// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
namespace Snap.Hutao.Control.Media;
internal struct Rgba64
{
public Half R;
public Half G;
public Half B;
public Half A;
}

View File

@@ -39,6 +39,24 @@ internal static class SoftwareBitmapExtension
}
}
public static unsafe double Luminance(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
{
using (IMemoryBufferReference reference = buffer.CreateReference())
{
reference.As<IMemoryBufferByteAccess>().GetBuffer(out Span<Bgra32> bytes);
double sum = 0;
foreach (ref readonly Bgra32 pixel in bytes)
{
sum += pixel.Luminance;
}
return sum / bytes.Length;
}
}
}
public static unsafe Bgra32 GetAccentColor(this SoftwareBitmap softwareBitmap)
{
using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))

View File

@@ -1,89 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Data;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("Spacing", typeof(double), default(double), nameof(OnSpacingChanged))]
internal partial class EqualPanel : Microsoft.UI.Xaml.Controls.Panel
{
private double maxItemWidth;
private double maxItemHeight;
private int visibleItemsCount;
public EqualPanel()
{
RegisterPropertyChangedCallback(HorizontalAlignmentProperty, OnHorizontalAlignmentChanged);
}
protected override Size MeasureOverride(Size availableSize)
{
maxItemWidth = 0;
maxItemHeight = 0;
List<UIElement> elements = [.. Children.Where(element => element.Visibility == Visibility.Visible)];
visibleItemsCount = elements.Count;
foreach (ref readonly UIElement child in CollectionsMarshal.AsSpan(elements))
{
child.Measure(availableSize);
maxItemWidth = Math.Max(maxItemWidth, child.DesiredSize.Width);
maxItemHeight = Math.Max(maxItemHeight, child.DesiredSize.Height);
}
if (visibleItemsCount > 0)
{
// Return equal widths based on the widest item
// In very specific edge cases the AvailableWidth might be infinite resulting in a crash.
if (HorizontalAlignment is not HorizontalAlignment.Stretch || double.IsInfinity(availableSize.Width))
{
return new Size((maxItemWidth * visibleItemsCount) + (Spacing * (visibleItemsCount - 1)), maxItemHeight);
}
else
{
// Equal columns based on the available width, adjust for spacing
double totalWidth = availableSize.Width - (Spacing * (visibleItemsCount - 1));
maxItemWidth = totalWidth / visibleItemsCount;
return new Size(availableSize.Width, maxItemHeight);
}
}
else
{
return new Size(0, 0);
}
}
protected override Size ArrangeOverride(Size finalSize)
{
double x = 0;
// Check if there's more (little) width available - if so, set max item width to the maximum possible as we have an almost perfect height.
if (finalSize.Width > (visibleItemsCount * maxItemWidth) + (Spacing * (visibleItemsCount - 1)))
{
maxItemWidth = (finalSize.Width - (Spacing * (visibleItemsCount - 1))) / visibleItemsCount;
}
IEnumerable<UIElement> elements = Children.Where(static e => e.Visibility == Visibility.Visible);
foreach (UIElement child in elements)
{
child.Arrange(new Rect(x, 0, maxItemWidth, maxItemHeight));
x += maxItemWidth + Spacing;
}
return finalSize;
}
private static void OnSpacingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as EqualPanel)?.InvalidateMeasure();
}
private static void OnHorizontalAlignmentChanged(DependencyObject d, DependencyProperty dp)
{
(d as EqualPanel)?.InvalidateMeasure();
}
}

View File

@@ -1,62 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Runtime.InteropServices;
using Windows.Foundation;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))]
[DependencyProperty("Spacing", typeof(double))]
internal partial class HorizontalEqualPanel : Microsoft.UI.Xaml.Controls.Panel
{
public HorizontalEqualPanel()
{
Loaded += OnLoaded;
SizeChanged += OnSizeChanged;
}
protected override Size MeasureOverride(Size availableSize)
{
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
{
// ScrollViewer will always return an Infinity Size, we should use ActualWidth for this situation.
double availableWidth = double.IsInfinity(availableSize.Width) ? ActualWidth : availableSize.Width;
double childAvailableWidth = (availableWidth + Spacing) / visibleChildren.Count;
double childMaxAvailableWidth = Math.Max(MinItemWidth, childAvailableWidth);
visibleChild.Measure(new(childMaxAvailableWidth - Spacing, ActualHeight));
}
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
List<UIElement> visibleChildren = Children.Where(child => child.Visibility is Visibility.Visible).ToList();
double availableItemWidth = (finalSize.Width - (Spacing * (visibleChildren.Count - 1))) / visibleChildren.Count;
double actualItemWidth = Math.Max(MinItemWidth, availableItemWidth);
double offset = 0;
foreach (ref readonly UIElement visibleChild in CollectionsMarshal.AsSpan(visibleChildren))
{
visibleChild.Arrange(new Rect(offset, 0, actualItemWidth, finalSize.Height));
offset += actualItemWidth + Spacing;
}
return finalSize;
}
private static void OnLoaded(object sender, RoutedEventArgs e)
{
HorizontalEqualPanel panel = (HorizontalEqualPanel)sender;
int vivibleChildrenCount = panel.Children.Count(child => child.Visibility is Visibility.Visible);
panel.MinWidth = (panel.MinItemWidth * vivibleChildrenCount) + (panel.Spacing * (vivibleChildrenCount - 1));
}
private static void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
((HorizontalEqualPanel)sender).InvalidateMeasure();
}
}

View File

@@ -6,7 +6,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
Style="{StaticResource DefaultSegmentedStyle}"
mc:Ignorable="d">
<cwc:SegmentedItem

View File

@@ -4,7 +4,6 @@
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Setting;
using System.Collections.Frozen;
namespace Snap.Hutao.Control.Panel;
@@ -20,11 +19,11 @@ internal sealed partial class PanelSelector : Segmented
public const string List = nameof(List);
public const string Grid = nameof(Grid);
private static readonly FrozenDictionary<int, string> IndexTypeMap = FrozenDictionary.ToFrozenDictionary(
[
KeyValuePair.Create(0, List),
KeyValuePair.Create(1, Grid),
]);
private static readonly Dictionary<int, string> IndexTypeMap = new()
{
[0] = List,
[1] = Grid,
};
private readonly RoutedEventHandler loadedEventHandler;
private readonly RoutedEventHandler unloadedEventHandler;

View File

@@ -1,53 +1,21 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Windows.Foundation;
using CommunityToolkit.WinUI.Controls;
namespace Snap.Hutao.Control.Panel;
[DependencyProperty("MinItemWidth", typeof(double))]
[DependencyProperty("ColumnSpacing", typeof(double))]
[DependencyProperty("RowSpacing", typeof(double))]
internal sealed partial class UniformPanel : Microsoft.UI.Xaml.Controls.Panel
internal sealed partial class UniformPanel : UniformGrid
{
private int columns;
protected override Size MeasureOverride(Size availableSize)
public UniformPanel()
{
columns = (int)((availableSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
double availableItemWidth = ((availableSize.Width + ColumnSpacing) / columns) - ColumnSpacing;
double maxDesiredHeight = 0;
foreach (UIElement child in Children)
{
child.Measure(new Size(availableItemWidth, availableSize.Height));
maxDesiredHeight = Math.Max(maxDesiredHeight, child.DesiredSize.Height);
}
int desiredRows = (int)Math.Ceiling(Children.Count / (double)columns);
double desiredHeight = ((maxDesiredHeight + RowSpacing) * desiredRows) - RowSpacing;
return new Size(availableSize.Width, desiredHeight);
Columns = 1;
SizeChanged += OnSizeChanged;
}
protected override Size ArrangeOverride(Size finalSize)
private void OnSizeChanged(object sender, Microsoft.UI.Xaml.SizeChangedEventArgs e)
{
double itemWidth = ((finalSize.Width + ColumnSpacing) / columns) - ColumnSpacing;
for (int index = 0; index < Children.Count; index++)
{
UIElement child = Children[index];
int row = index / columns;
int column = index % columns;
double x = column * (itemWidth + ColumnSpacing);
double y = row * (child.DesiredSize.Height + RowSpacing);
child.Arrange(new Rect(x, y, itemWidth, child.DesiredSize.Height));
}
return finalSize;
Columns = (int)((e.NewSize.Width + ColumnSpacing) / (MinItemWidth + ColumnSpacing));
}
}

View File

@@ -15,7 +15,7 @@ internal class ScopedPage : Page
{
private readonly RoutedEventHandler unloadEventHandler;
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope pageScope;
private readonly IServiceScope currentScope;
private bool inFrame = true;
@@ -23,7 +23,7 @@ internal class ScopedPage : Page
{
unloadEventHandler = OnUnloaded;
Unloaded += unloadEventHandler;
pageScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
currentScope = Ioc.Default.GetRequiredService<IScopedPageScopeReferenceTracker>().CreateScope();
}
public async ValueTask NotifyRecipientAsync(INavigationData extra)
@@ -41,20 +41,14 @@ internal class ScopedPage : Page
/// 应当在 InitializeComponent() 前调用
/// </summary>
/// <typeparam name="TViewModel">视图模型类型</typeparam>
protected void InitializeWith<TViewModel>()
protected TViewModel InitializeWith<TViewModel>()
where TViewModel : class, IViewModel
{
try
{
IViewModel viewModel = pageScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
catch (Exception ex)
{
pageScope.ServiceProvider.GetRequiredService<ILogger<ScopedPage>>().LogError(ex, "Failed to initialize view model.");
throw;
}
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
return (TViewModel)viewModel;
}
/// <inheritdoc/>
@@ -80,11 +74,7 @@ internal class ScopedPage : Page
DisposeViewModel();
}
if (this.IsDisposed())
{
return;
}
DataContext = null;
Unloaded -= unloadEventHandler;
}
@@ -103,8 +93,7 @@ internal class ScopedPage : Page
viewModel.IsViewDisposed = true;
// Dispose the scope
pageScope.Dispose();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
currentScope.Dispose();
}
}
}

View File

@@ -22,6 +22,7 @@ internal sealed partial class ScopedPageScopeReferenceTracker : IScopedPageScope
public IServiceScope CreateScope()
{
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
IServiceScope currentScope = serviceProvider.CreateScope();
// In case previous one is not disposed.

View File

@@ -45,7 +45,15 @@ internal sealed partial class DescriptionTextBlock : ContentControl
private static void OnDescriptionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextBlock textBlock = (TextBlock)((DescriptionTextBlock)d).Content;
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
try
{
UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
}
catch (Exception ex)
{
_ = ex;
}
}
private static void OnTextStyleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

View File

@@ -14,7 +14,6 @@ using Windows.UI;
namespace Snap.Hutao.Control.Text;
// TODO: change the parsing to syntax tree
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
internal sealed partial class HtmlDescriptionTextBlock : ContentControl

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
@@ -29,7 +27,7 @@ internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
{
MiHoYoColorKind.Rgba => new(Position.Start + 17, Position.End - 8),
MiHoYoColorKind.Rgb => new(Position.Start + 15, Position.End - 8),
_ => throw HutaoException.NotSupported(),
_ => throw Must.NeverHappen(),
};
}
}
@@ -42,7 +40,7 @@ internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
{
MiHoYoColorKind.Rgba => new(Position.Start + 8, Position.Start + 16),
MiHoYoColorKind.Rgb => new(Position.Start + 8, Position.Start + 14),
_ => throw HutaoException.NotSupported(),
_ => throw Must.NeverHappen(),
};
}
}

View File

@@ -1,11 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
// TODO: Pooling syntax nodes to reduce memory allocation
internal sealed class MiHoYoSyntaxTree
{
public MiHoYoSyntaxNode Root { get; set; } = default!;
@@ -78,7 +75,7 @@ internal sealed class MiHoYoSyntaxTree
{
17 => MiHoYoColorKind.Rgba,
15 => MiHoYoColorKind.Rgb,
_ => throw HutaoException.NotSupported(),
_ => throw Must.NeverHappen(),
};
TextPosition position = new(0, endOfXmlColorRightClosingAtUnprocessedContent);

View File

@@ -4,19 +4,21 @@
xmlns:cwm="using:CommunityToolkit.WinUI.Media">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<x:Double x:Key="CompatShadowThemeOpacity">0.14</x:Double>
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="0.14"
Offset="0,4,0"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<x:Double x:Key="CompatShadowThemeOpacity">0.28</x:Double>
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="0.28"
Offset="0,4,0"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<cwm:AttachedCardShadow
x:Key="CompatCardShadow"
BlurRadius="8"
Opacity="{ThemeResource CompatShadowThemeOpacity}"
Offset="0,4,0"/>
<Style x:Key="BorderCardStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>

View File

@@ -8,9 +8,6 @@
<ItemsPanelTemplate x:Key="WrapPanelSpacing0Template">
<cwcont:WrapPanel/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="WrapPanelSpacing2Template">
<cwcont:WrapPanel HorizontalSpacing="2" VerticalSpacing="2"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="WrapPanelSpacing4Template">
<cwcont:WrapPanel HorizontalSpacing="4" VerticalSpacing="4"/>
</ItemsPanelTemplate>
@@ -20,9 +17,6 @@
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing2Template">
<StackPanel Orientation="Horizontal" Spacing="2"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing4Template">
<StackPanel Orientation="Horizontal" Spacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</ItemsPanelTemplate>

View File

@@ -11,6 +11,4 @@ internal static class KnownColors
public static readonly Color Orange = StructMarshal.Color(0xFFBC6932);
public static readonly Color Purple = StructMarshal.Color(0xFFA156E0);
public static readonly Color Blue = StructMarshal.Color(0xFF5180CB);
public static readonly Color Green = StructMarshal.Color(0xFF2A8F72);
public static readonly Color White = StructMarshal.Color(0xFF72778B);
}

View File

@@ -1,115 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cw="using:CommunityToolkit.WinUI"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
xmlns:shcp="using:Snap.Hutao.Control.Panel"
xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Controls.Segmented/SegmentedItem/SegmentedItem.xaml"/>
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<StaticResource x:Key="SegmentedBackground" ResourceKey="ControlAltFillColorSecondaryBrush"/>
<StaticResource x:Key="SegmentedBorderBrush" ResourceKey="ControlStrokeColorDefaultBrush"/>
<Thickness x:Key="SegmentedBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="SegmentedBackground" ResourceKey="ControlAltFillColorSecondaryBrush"/>
<StaticResource x:Key="SegmentedBorderBrush" ResourceKey="ControlStrokeColorDefaultBrush"/>
<Thickness x:Key="SegmentedBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<StaticResource x:Key="SegmentedBackground" ResourceKey="SystemColorButtonFaceColor"/>
<StaticResource x:Key="SegmentedBorderBrush" ResourceKey="SystemColorHighlightColorBrush"/>
<Thickness x:Key="SegmentedBorderThickness">1</Thickness>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<x:Double x:Key="SegmentedItemSpacing">1</x:Double>
<x:Double x:Key="ButtonItemSpacing">2</x:Double>
<Style BasedOn="{StaticResource DefaultSegmentedStyle}" TargetType="cwc:Segmented"/>
<Style x:Key="DefaultSegmentedStyle" TargetType="cwc:Segmented">
<Style.Setters>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Background" Value="{ThemeResource SegmentedBackground}"/>
<Setter Property="BorderBrush" Value="{ThemeResource SegmentedBorderBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource SegmentedBorderThickness}"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="SelectionMode" Value="Single"/>
<Setter Property="IsItemClickEnabled" Value="False"/>
<win:Setter Property="SingleSelectionFollowsFocus" Value="False"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="TabNavigation" Value="Once"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<shcp:EqualPanel
HorizontalAlignment="{Binding (cw:FrameworkElementExtensions.Ancestor).HorizontalAlignment, RelativeSource={RelativeSource Self}}"
cw:FrameworkElementExtensions.AncestorType="cwc:Segmented"
Spacing="{ThemeResource SegmentedItemSpacing}"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="cwc:Segmented">
<Grid>
<Border
VerticalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"/>
<ItemsPresenter Margin="{TemplateBinding Padding}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
<Style
x:Key="PivotSegmentedStyle"
BasedOn="{StaticResource DefaultSegmentedStyle}"
TargetType="cwc:Segmented">
<Style.Setters>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource PivotSegmentedItemStyle}"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="{ThemeResource SegmentedItemSpacing}"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
<Style
x:Key="ButtonSegmentedStyle"
BasedOn="{StaticResource DefaultSegmentedStyle}"
TargetType="cwc:Segmented">
<Style.Setters>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="ItemContainerStyle" Value="{StaticResource ButtonSegmentedItemStyle}"/>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="{ThemeResource ButtonItemSpacing}"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>
</ResourceDictionary>

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Win32;
using Windows.UI;
namespace Snap.Hutao.Control.Theme;
internal static class SystemColors
{
public static Color BaseLowColor(bool isDarkMode)
{
return isDarkMode ? StructMarshal.Color(0x33FFFFFF) : StructMarshal.Color(0x33000000);
}
public static Color BaseMediumLowColor(bool isDarkMode)
{
return isDarkMode ? StructMarshal.Color(0x66FFFFFF) : StructMarshal.Color(0x66000000);
}
public static Color BaseHighColor(bool isDarkMode)
{
return isDarkMode ? StructMarshal.Color(0xFFFFFFFF) : StructMarshal.Color(0xFF000000);
}
}

View File

@@ -17,13 +17,8 @@
<x:String x:Key="UI_Icon_Intee_Explore_1">https://api.snapgenshin.com/static/raw/Bg/UI_Icon_Intee_Explore_1.png</x:String>
<x:String x:Key="UI_ImgSign_ItemIcon">https://api.snapgenshin.com/static/raw/Bg/UI_ImgSign_ItemIcon.png</x:String>
<x:String x:Key="UI_ItemIcon_None">https://api.snapgenshin.com/static/raw/Bg/UI_ItemIcon_None.png</x:String>
<!-- Mark -->
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Events_Start.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Proce">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Proce.png</x:String>
<x:String x:Key="UI_MarkQuest_Main_Start">https://api.snapgenshin.com/static/raw/Mark/UI_MarkQuest_Main_Start.png</x:String>
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Mark/UI_MarkTower.png</x:String>
<x:String x:Key="UI_MarkQuest_Events_Proce">https://api.snapgenshin.com/static/raw/Bg/UI_MarkQuest_Events_Proce.png</x:String>
<x:String x:Key="UI_MarkTower">https://api.snapgenshin.com/static/raw/Bg/UI_MarkTower.png</x:String>
<!-- ItemIcon -->
<x:String x:Key="UI_ItemIcon_201">https://api.snapgenshin.com/static/raw/ItemIcon/UI_ItemIcon_201.png</x:String>
@@ -35,7 +30,6 @@
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
<x:String x:Key="UI_EmotionIcon52">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon52.png</x:String>
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
<x:String x:Key="UI_EmotionIcon89">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon89.png</x:String>
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Control.TokenizingTextBox;
internal interface ITokenStringContainer
{
string Text { get; set; }
bool IsLast { get; }
}

View File

@@ -1,350 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Helpers;
using System.Collections;
using System.Collections.Specialized;
namespace Snap.Hutao.Control.TokenizingTextBox;
internal sealed class InterspersedObservableCollection : IList, IEnumerable<object>, INotifyCollectionChanged
{
private readonly Dictionary<int?, object> interspersedObjects = [];
private bool isInsertingOriginal;
public InterspersedObservableCollection(object itemsSource)
{
if (itemsSource is not IList list)
{
throw new ArgumentException("The input items source must be assignable to the System.Collections.IList type.");
}
ItemsSource = list;
if (ItemsSource is INotifyCollectionChanged notifier)
{
WeakEventListener<InterspersedObservableCollection, object?, NotifyCollectionChangedEventArgs> weakPropertyChangedListener = new(this)
{
OnEventAction = static (instance, source, eventArgs) => instance.ItemsSource_CollectionChanged(source, eventArgs),
OnDetachAction = (weakEventListener) => notifier.CollectionChanged -= weakEventListener.OnEvent, // Use Local Reference Only
};
notifier.CollectionChanged += weakPropertyChangedListener.OnEvent;
}
}
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public IList ItemsSource { get; private set; }
public bool IsFixedSize => false;
public bool IsReadOnly => false;
public int Count => ItemsSource.Count + interspersedObjects.Count;
public bool IsSynchronized => false;
public object SyncRoot => new();
public object? this[int index]
{
get
{
if (interspersedObjects.TryGetValue(index, out object? value))
{
return value;
}
// Find out the number of elements in our dictionary with keys below ours.
return ItemsSource[ToInnerIndex(index)];
}
set => throw new NotImplementedException();
}
public void Insert(int index, object? obj)
{
MoveKeysForward(index, 1); // Move existing keys at index over to make room for new item
ArgumentNullException.ThrowIfNull(obj);
interspersedObjects[index] = obj;
CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Add, obj, index));
}
public void InsertAt(int outerIndex, object obj)
{
// Find out our closest index based on interspersed keys
int index = outerIndex - interspersedObjects.Keys.Count(key => key!.Value < outerIndex); // Note: we exclude the = from ToInnerIndex here
// If we're inserting where we would normally, then just do that, otherwise we need extra room to not move other keys
if (index != outerIndex)
{
MoveKeysForward(outerIndex, 1); // Skip over until the current spot unlike normal
isInsertingOriginal = true; // Prevent Collection callback from moving keys forward on insert
}
// Insert into original collection
ItemsSource.Insert(index, obj);
// TODO: handle manipulation/notification if not observable
}
public IEnumerator<object> GetEnumerator()
{
int i = 0; // Index of our current 'virtual' position
int count = 0;
int realized = 0;
foreach (object element in ItemsSource)
{
while (interspersedObjects.TryGetValue(i++, out object? obj))
{
realized++; // Track interspersed items used
yield return obj;
}
count++; // Track original items used
yield return element;
}
// Add any remaining items in our interspersed collection past the index we reached in the original collection
if (realized < interspersedObjects.Count)
{
// Only select items past our current index, but make sure we've sorted them by their index as well.
foreach ((int? _, object value) in interspersedObjects.Where(kvp => kvp.Key >= i).OrderBy(kvp => kvp.Key))
{
yield return value;
}
}
}
public int Add(object? value)
{
ArgumentNullException.ThrowIfNull(value);
int index = ItemsSource.Add(value); //// TODO: If the collection isn't observable, we should do manipulations/notifications here...?
return ToOuterIndex(index);
}
public void Clear()
{
ItemsSource.Clear();
interspersedObjects.Clear();
}
public bool Contains(object? value)
{
ArgumentNullException.ThrowIfNull(value);
return interspersedObjects.ContainsValue(value) || ItemsSource.Contains(value);
}
public int IndexOf(object? value)
{
ArgumentNullException.ThrowIfNull(value);
(int? key, object _) = ItemKeySearch(value);
if (key is int k)
{
return k;
}
int index = ItemsSource.IndexOf(value);
// Find out the number of elements in our dictionary with keys below ours.
return index == -1 ? -1 : ToOuterIndex(index);
}
public void Remove(object? value)
{
ArgumentNullException.ThrowIfNull(value);
(int? key, object obj) = ItemKeySearch(value);
if (key is int k)
{
interspersedObjects.Remove(k);
MoveKeysBackward(k, 1); // Move other interspersed items back
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, obj, k));
}
else
{
ItemsSource.Remove(value); // TODO: If not observable, update indices?
}
}
public void RemoveAt(int index)
{
throw new NotImplementedException();
}
public void CopyTo(Array array, int index)
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
private void ItemsSource_CollectionChanged(object? source, NotifyCollectionChangedEventArgs eventArgs)
{
switch (eventArgs.Action)
{
case NotifyCollectionChangedAction.Add:
// Shift any existing interspersed items after the inserted item
ArgumentNullException.ThrowIfNull(eventArgs.NewItems);
int count = eventArgs.NewItems.Count;
if (count > 0)
{
if (!isInsertingOriginal)
{
MoveKeysForward(eventArgs.NewStartingIndex, count);
}
isInsertingOriginal = false;
CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Add, eventArgs.NewItems, ToOuterIndex(eventArgs.NewStartingIndex)));
}
break;
case NotifyCollectionChangedAction.Remove:
ArgumentNullException.ThrowIfNull(eventArgs.OldItems);
count = eventArgs.OldItems.Count;
if (count > 0)
{
int outerIndex = ToOuterIndexAfterRemoval(eventArgs.OldStartingIndex);
MoveKeysBackward(outerIndex, count);
CollectionChanged?.Invoke(this, new(NotifyCollectionChangedAction.Remove, eventArgs.OldItems, outerIndex));
}
break;
case NotifyCollectionChangedAction.Reset:
ReadjustKeys();
// TODO: ListView doesn't like this notification and throws a visual tree duplication exception...
// Not sure what to do with that yet...
CollectionChanged?.Invoke(this, eventArgs);
break;
}
}
private void MoveKeysForward(int pivot, int amount)
{
// Sort in reverse order to work from highest to lowest
foreach (int? key in interspersedObjects.Keys.OrderByDescending(v => v))
{
if (key < pivot) //// If it's the last item in the collection, we still want to move our last key, otherwise we'd use <=
{
break;
}
interspersedObjects[key + amount] = interspersedObjects[key];
interspersedObjects.Remove(key);
}
}
private void MoveKeysBackward(int pivot, int amount)
{
// Sort in regular order to work from the earliest indices onwards
foreach (int? key in interspersedObjects.Keys.OrderBy(v => v))
{
// Skip elements before the pivot point
if (key <= pivot) //// Include pivot point as that's the point where we start modifying beyond
{
continue;
}
interspersedObjects[key - amount] = interspersedObjects[key];
interspersedObjects.Remove(key);
}
}
private void ReadjustKeys()
{
int count = ItemsSource.Count;
int existing = 0;
foreach (int? key in interspersedObjects.Keys.OrderBy(v => v))
{
if (key <= count)
{
existing++;
continue;
}
interspersedObjects[count + existing++] = interspersedObjects[key];
interspersedObjects.Remove(key);
}
}
private int ToInnerIndex(int outerIndex)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(outerIndex, Count);
if (interspersedObjects.ContainsKey(outerIndex))
{
throw new ArgumentException("The outer index can't be inserted as a key to the original collection.");
}
return outerIndex - interspersedObjects.Keys.Count(key => key!.Value <= outerIndex);
}
private int ToOuterIndex(int innerIndex)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(innerIndex, ItemsSource.Count);
foreach ((int? key, object _) in interspersedObjects.OrderBy(v => v.Key))
{
if (innerIndex >= key)
{
innerIndex++;
}
else
{
break;
}
}
return innerIndex;
}
private int ToOuterIndexAfterRemoval(int innerIndexToProject)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(innerIndexToProject, ItemsSource.Count + 1);
//// TODO: Deal with bounds (0 / Count)? Or is it the same?
foreach ((int? key, object _) in interspersedObjects.OrderBy(v => v.Key))
{
if (innerIndexToProject >= key)
{
innerIndexToProject++;
}
else
{
break;
}
}
return innerIndexToProject;
}
private KeyValuePair<int?, object> ItemKeySearch(object value)
{
if (value is null)
{
return interspersedObjects.FirstOrDefault(kvp => kvp.Value is null);
}
return interspersedObjects.FirstOrDefault(kvp => kvp.Value.Equals(value));
}
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Control.TokenizingTextBox;
[DependencyProperty("Text", typeof(string))]
internal sealed partial class PretokenStringContainer : DependencyObject, ITokenStringContainer
{
public PretokenStringContainer(bool isLast = false)
{
IsLast = isLast;
}
public PretokenStringContainer(string text)
{
Text = text;
}
public bool IsLast { get; private set; }
public override string ToString()
{
return Text;
}
}

View File

@@ -1,18 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Common.Deferred;
namespace Snap.Hutao.Control.TokenizingTextBox;
internal sealed class TokenItemAddingEventArgs : DeferredCancelEventArgs
{
public TokenItemAddingEventArgs(string token)
{
TokenText = token;
}
public string TokenText { get; private set; }
public object? Item { get; set; }
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Common.Deferred;
namespace Snap.Hutao.Control.TokenizingTextBox;
internal sealed class TokenItemRemovingEventArgs : DeferredCancelEventArgs
{
public TokenItemRemovingEventArgs(object item, TokenizingTextBoxItem token)
{
Item = item;
Token = token;
}
public object Item { get; private set; }
public TokenizingTextBoxItem Token { get; private set; }
}

View File

@@ -1,951 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI;
using CommunityToolkit.WinUI.Deferred;
using CommunityToolkit.WinUI.Helpers;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using System.Collections.ObjectModel;
using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation;
using Windows.Foundation.Metadata;
using Windows.System;
using Windows.UI.Core;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
using DispatcherQueuePriority = Microsoft.UI.Dispatching.DispatcherQueuePriority;
namespace Snap.Hutao.Control.TokenizingTextBox;
[DependencyProperty("AutoSuggestBoxStyle", typeof(Style))]
[DependencyProperty("AutoSuggestBoxTextBoxStyle", typeof(Style))]
[DependencyProperty("MaximumTokens", typeof(int), -1, nameof(OnMaximumTokensChanged))]
[DependencyProperty("PlaceholderText", typeof(string))]
[DependencyProperty("QueryIcon", typeof(IconSource))]
[DependencyProperty("SuggestedItemsSource", typeof(object))]
[DependencyProperty("SuggestedItemTemplate", typeof(DataTemplate))]
[DependencyProperty("SuggestedItemTemplateSelector", typeof(DataTemplateSelector))]
[DependencyProperty("SuggestedItemContainerStyle", typeof(Style))]
[DependencyProperty("TabNavigateBackOnArrow", typeof(bool), false)]
[DependencyProperty("Text", typeof(string), default, nameof(TextPropertyChanged))]
[DependencyProperty("TextMemberPath", typeof(string))]
[DependencyProperty("TokenItemTemplate", typeof(DataTemplate))]
[DependencyProperty("TokenItemTemplateSelector", typeof(DataTemplateSelector))]
[DependencyProperty("TokenDelimiter", typeof(string), " ")]
[DependencyProperty("TokenSpacing", typeof(double))]
[TemplatePart(Name = NormalState, Type = typeof(VisualState))]
[TemplatePart(Name = PointerOverState, Type = typeof(VisualState))]
[TemplatePart(Name = FocusedState, Type = typeof(VisualState))]
[TemplatePart(Name = UnfocusedState, Type = typeof(VisualState))]
[TemplatePart(Name = MaxReachedState, Type = typeof(VisualState))]
[TemplatePart(Name = MaxUnreachedState, Type = typeof(VisualState))]
[SuppressMessage("", "SA1124")]
internal partial class TokenizingTextBox : ListViewBase
{
public const string NormalState = "Normal";
public const string PointerOverState = "PointerOver";
public const string FocusedState = "Focused";
public const string UnfocusedState = "Unfocused";
public const string MaxReachedState = "MaxReachedState";
public const string MaxUnreachedState = "MaxUnreachedState";
private DispatcherQueue dispatcherQueue;
private InterspersedObservableCollection innerItemsSource;
private ITokenStringContainer currentTextEdit; // Don't update this directly outside of initialization, use UpdateCurrentTextEdit Method
private ITokenStringContainer lastTextEdit;
public TokenizingTextBox()
{
// Setup our base state of our collection
innerItemsSource = new InterspersedObservableCollection(new ObservableCollection<object>()); // TODO: Test this still will let us bind to ItemsSource in XAML?
currentTextEdit = lastTextEdit = new PretokenStringContainer(true);
innerItemsSource.Insert(innerItemsSource.Count, currentTextEdit);
ItemsSource = innerItemsSource;
//// TODO: Consolidate with callback below for ItemsSourceProperty changed?
DefaultStyleKey = typeof(TokenizingTextBox);
// TODO: Do we want to support ItemsSource better? Need to investigate how that works with adding...
RegisterPropertyChangedCallback(ItemsSourceProperty, ItemsSource_PropertyChanged);
PreviewKeyDown += TokenizingTextBox_PreviewKeyDown;
PreviewKeyUp += TokenizingTextBox_PreviewKeyUp;
CharacterReceived += TokenizingTextBox_CharacterReceived;
ItemClick += TokenizingTextBox_ItemClick;
dispatcherQueue = DispatcherQueue;
}
public event TypedEventHandler<Microsoft.UI.Xaml.Controls.AutoSuggestBox, AutoSuggestBoxTextChangedEventArgs>? TextChanged;
public event TypedEventHandler<Microsoft.UI.Xaml.Controls.AutoSuggestBox, AutoSuggestBoxSuggestionChosenEventArgs>? SuggestionChosen;
public event TypedEventHandler<Microsoft.UI.Xaml.Controls.AutoSuggestBox, AutoSuggestBoxQuerySubmittedEventArgs>? QuerySubmitted;
public event TypedEventHandler<TokenizingTextBox, TokenItemAddingEventArgs>? TokenItemAdding;
public event TypedEventHandler<TokenizingTextBox, object>? TokenItemAdded;
public event TypedEventHandler<TokenizingTextBox, TokenItemRemovingEventArgs>? TokenItemRemoving;
public event TypedEventHandler<TokenizingTextBox, object>? TokenItemRemoved;
private enum MoveDirection
{
Next,
Previous,
}
public static bool IsShiftPressed => InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
public static bool IsXamlRootAvailable { get; } = ApiInformation.IsPropertyPresent("Windows.UI.Xaml.UIElement", "XamlRoot");
public static bool IsControlPressed => InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
public bool PauseTokenClearOnFocus { get; set; }
public bool IsClearingForClick { get; set; }
public string SelectedTokenText
{
get => PrepareSelectionForClipboard();
}
public void RaiseQuerySubmitted(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
QuerySubmitted?.Invoke(sender, args);
}
public void RaiseSuggestionChosen(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
{
SuggestionChosen?.Invoke(sender, args);
}
public void RaiseTextChanged(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
TextChanged?.Invoke(sender, args);
}
public void AddTokenItem(object data, bool atEnd = false)
{
_ = AddTokenAsync(data, atEnd);
}
public async ValueTask ClearAsync()
{
while (innerItemsSource.Count > 1)
{
if (ContainerFromItem(innerItemsSource[0]) is TokenizingTextBoxItem container)
{
if (!await RemoveTokenAsync(container, innerItemsSource[0]).ConfigureAwait(true))
{
// if a removal operation fails then stop the clear process
break;
}
}
}
// Clear the active pretoken string.
// Setting the text property directly avoids a delay when setting the text in the autosuggest box.
Text = string.Empty;
}
public async Task AddTokenAsync(object data, bool? atEnd = default)
{
if (MaximumTokens >= 0 && MaximumTokens <= innerItemsSource.ItemsSource.Count)
{
// No tokens for you
return;
}
if (data is string str && TokenItemAdding is not null)
{
TokenItemAddingEventArgs tiaea = new(str);
await TokenItemAdding.InvokeAsync(this, tiaea).ConfigureAwait(true);
if (tiaea.Cancel)
{
return;
}
if (tiaea.Item is not null)
{
data = tiaea.Item; // Transformed by event implementor
}
}
// If we've been typing in the last box, just add this to the end of our collection
if (atEnd == true || currentTextEdit == lastTextEdit)
{
innerItemsSource.InsertAt(innerItemsSource.Count - 1, data);
}
else
{
// Otherwise, we'll insert before our current box
ITokenStringContainer edit = currentTextEdit;
int index = innerItemsSource.IndexOf(edit);
// Insert our new data item at the location of our textbox
innerItemsSource.InsertAt(index, data);
// Remove our textbox
innerItemsSource.Remove(edit);
}
// Focus back to our end box as Outlook does.
TokenizingTextBoxItem last = (TokenizingTextBoxItem)ContainerFromItem(lastTextEdit);
last?.AutoSuggestTextBox.Focus(FocusState.Keyboard);
TokenItemAdded?.Invoke(this, data);
GuardAgainstPlaceholderTextLayoutIssue();
}
public async ValueTask RemoveAllSelectedTokens()
{
while (SelectedItems.Count > 0)
{
if (ContainerFromItem(SelectedItems[0]) is TokenizingTextBoxItem container)
{
if (IndexFromContainer(container) != Items.Count - 1)
{
// if its a text box, remove any selected text, and if its then empty remove the container, unless its focused
if (SelectedItems[0] is ITokenStringContainer)
{
TextBox asb = container.AutoSuggestTextBox;
// grab any selected text
string tempStr = asb.SelectionStart == 0
? string.Empty
: asb.Text[..asb.SelectionStart];
tempStr +=
asb.SelectionStart +
asb.SelectionLength < asb.Text.Length
? asb.Text[(asb.SelectionStart + asb.SelectionLength)..]
: string.Empty;
if (tempStr.Length is 0)
{
// Need to be careful not to remove the last item in the list
await RemoveTokenAsync(container).ConfigureAwait(true);
}
else
{
asb.Text = tempStr;
}
}
else
{
// if the item is a token just remove it.
await RemoveTokenAsync(container).ConfigureAwait(true);
}
}
else
{
if (SelectedItems.Count == 1)
{
// at this point we have one selection and its the default textbox.
// stop the iteration here
break;
}
}
}
}
}
public bool SelectPreviousItem(TokenizingTextBoxItem item)
{
return SelectNewItem(item, -1, i => i > 0);
}
public bool SelectNextItem(TokenizingTextBoxItem item)
{
return SelectNewItem(item, 1, i => i < Items.Count - 1);
}
public void SelectAllTokensAndText()
{
void SelectAllTokensAndTextCore()
{
this.SelectAllSafe();
// need to synchronize the select all and the focus behavior on the text box
// because there is no way to identify that the focus has been set from this point
// to avoid instantly clearing the selection of tokens
PauseTokenClearOnFocus = true;
foreach (object? item in Items)
{
if (item is ITokenStringContainer)
{
// grab any selected text
if (ContainerFromItem(item) is TokenizingTextBoxItem pretoken)
{
pretoken.AutoSuggestTextBox.SelectionStart = 0;
pretoken.AutoSuggestTextBox.SelectionLength = pretoken.AutoSuggestTextBox.Text.Length;
}
}
}
if (ContainerFromIndex(Items.Count - 1) is TokenizingTextBoxItem container)
{
container.Focus(FocusState.Programmatic);
}
}
_ = dispatcherQueue.EnqueueAsync(SelectAllTokensAndTextCore, DispatcherQueuePriority.Normal);
}
public void DeselectAllTokensAndText(TokenizingTextBoxItem? ignoreItem = default)
{
this.DeselectAll();
ClearAllTextSelections(ignoreItem);
}
protected void UpdateCurrentTextEdit(ITokenStringContainer edit)
{
currentTextEdit = edit;
Text = edit.Text; // Update our text property.
}
protected override AutomationPeer OnCreateAutomationPeer()
{
return new TokenizingTextBoxAutomationPeer(this);
}
/// <inheritdoc/>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
MenuFlyoutItem selectAllMenuItem = new()
{
Text = "Select all",
};
selectAllMenuItem.Click += (s, e) => SelectAllTokensAndText();
MenuFlyout menuFlyout = new();
menuFlyout.Items.Add(selectAllMenuItem);
if (IsXamlRootAvailable && XamlRoot is not null)
{
menuFlyout.XamlRoot = XamlRoot;
}
ContextFlyout = menuFlyout;
}
/// <inheritdoc/>
protected override DependencyObject GetContainerForItemOverride()
{
return new TokenizingTextBoxItem();
}
/// <inheritdoc/>
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is TokenizingTextBoxItem;
}
/// <inheritdoc/>
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
if (element is TokenizingTextBoxItem tokenitem)
{
tokenitem.Owner = this;
tokenitem.ContentTemplateSelector = TokenItemTemplateSelector;
tokenitem.ContentTemplate = TokenItemTemplate;
tokenitem.ClearClicked -= TokenizingTextBoxItem_ClearClicked;
tokenitem.ClearClicked += TokenizingTextBoxItem_ClearClicked;
tokenitem.ClearAllAction -= TokenizingTextBoxItem_ClearAllAction;
tokenitem.ClearAllAction += TokenizingTextBoxItem_ClearAllAction;
tokenitem.GotFocus -= TokenizingTextBoxItem_GotFocus;
tokenitem.GotFocus += TokenizingTextBoxItem_GotFocus;
tokenitem.LostFocus -= TokenizingTextBoxItem_LostFocus;
tokenitem.LostFocus += TokenizingTextBoxItem_LostFocus;
MenuFlyout menuFlyout = new();
MenuFlyoutItem removeMenuItem = new()
{
Text = "Remove",
};
removeMenuItem.Click += (s, e) => TokenizingTextBoxItem_ClearClicked(tokenitem, default);
menuFlyout.Items.Add(removeMenuItem);
if (IsXamlRootAvailable && XamlRoot is not null)
{
menuFlyout.XamlRoot = XamlRoot;
}
MenuFlyoutItem selectAllMenuItem = new()
{
Text = "Select all",
};
selectAllMenuItem.Click += (s, e) => SelectAllTokensAndText();
menuFlyout.Items.Add(selectAllMenuItem);
tokenitem.ContextFlyout = menuFlyout;
}
}
private static void TextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TokenizingTextBox { currentTextEdit: { } } ttb)
{
if (e.NewValue is string newValue)
{
ttb.currentTextEdit.Text = newValue;
// Notify inner container of text change, see issue #4749
TokenizingTextBoxItem item = (TokenizingTextBoxItem)ttb.ContainerFromItem(ttb.currentTextEdit);
item?.UpdateText(ttb.currentTextEdit.Text);
}
}
}
private static void OnMaximumTokensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TokenizingTextBox { MaximumTokens: >= 0 } ttb && e.NewValue is int newMaxTokens)
{
int tokenCount = ttb.innerItemsSource.ItemsSource.Count;
if (tokenCount > 0 && tokenCount > newMaxTokens)
{
int tokensToRemove = tokenCount - Math.Max(newMaxTokens, 0);
// Start at the end, remove any extra tokens.
for (int i = tokenCount; i > tokenCount - tokensToRemove; --i)
{
object? token = ttb.innerItemsSource.ItemsSource[i - 1];
if (token is not null)
{
// Force remove the items. No warning and no option to cancel.
ttb.innerItemsSource.Remove(token);
ttb.TokenItemRemoved?.Invoke(ttb, token);
}
}
}
}
}
private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProperty dp)
{
// If we're given a different ItemsSource, we need to wrap that collection in our helper class.
if (ItemsSource is { } and not InterspersedObservableCollection)
{
innerItemsSource = new(ItemsSource);
if (MaximumTokens >= 0 && innerItemsSource.ItemsSource.Count >= MaximumTokens)
{
// Reduce down to below the max as necessary.
int endCount = MaximumTokens > 0 ? MaximumTokens : 0;
for (int i = innerItemsSource.ItemsSource.Count - 1; i >= endCount; --i)
{
innerItemsSource.Remove(innerItemsSource[i]);
}
}
// Add our text box at the end of items and set its default value to our initial text, fix for #4749
currentTextEdit = lastTextEdit = new PretokenStringContainer(true) { Text = Text };
innerItemsSource.Insert(innerItemsSource.Count, currentTextEdit);
ItemsSource = innerItemsSource;
}
}
private void TokenizingTextBox_ItemClick(object sender, ItemClickEventArgs e)
{
// If the user taps an item in the list, make sure to clear any text selection as required
// Note, token selection is cleared by the listview default behavior
if (!IsControlPressed)
{
// Set class state flag to prevent click item being immediately deselected
IsClearingForClick = true;
ClearAllTextSelections(default);
}
}
private void TokenizingTextBox_PreviewKeyUp(object sender, KeyRoutedEventArgs e)
{
switch (e.Key)
{
case VirtualKey.Escape:
// Clear any selection and place the focus back into the text box
DeselectAllTokensAndText();
FocusPrimaryAutoSuggestBox();
break;
}
}
private void FocusPrimaryAutoSuggestBox()
{
if (Items?.Count > 0)
{
if (ContainerFromIndex(Items.Count - 1) is TokenizingTextBoxItem container)
{
container.Focus(FocusState.Programmatic);
}
}
}
private async void TokenizingTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
switch (e.Key)
{
case VirtualKey.C:
if (IsControlPressed)
{
CopySelectedToClipboard();
e.Handled = true;
return;
}
break;
case VirtualKey.X:
if (IsControlPressed)
{
CopySelectedToClipboard();
// now clear all selected tokens and text, or all if none are selected
await RemoveAllSelectedTokens().ConfigureAwait(false);
}
break;
// For moving between tokens
case VirtualKey.Left:
e.Handled = MoveFocusAndSelection(MoveDirection.Previous);
return;
case VirtualKey.Right:
e.Handled = MoveFocusAndSelection(MoveDirection.Next);
return;
case VirtualKey.A:
// modify the select-all behavior to ensure the text in the edit box gets selected.
if (IsControlPressed)
{
SelectAllTokensAndText();
e.Handled = true;
return;
}
break;
}
}
private async void TokenizingTextBox_CharacterReceived(UIElement sender, CharacterReceivedRoutedEventArgs args)
{
TokenizingTextBoxItem container = (TokenizingTextBoxItem)ContainerFromItem(currentTextEdit);
if (container is not null && !(GetFocusedElement().Equals(container.AutoSuggestTextBox) || char.IsControl(args.Character)))
{
if (SelectedItems.Count > 0)
{
int index = innerItemsSource.IndexOf(SelectedItems.First());
await RemoveAllSelectedTokens().ConfigureAwait(false);
void RemoveOldItems()
{
// If we're before the last textbox and it's empty, redirect focus to that one instead
if (index == innerItemsSource.Count - 1 && string.IsNullOrWhiteSpace(lastTextEdit.Text))
{
if (ContainerFromItem(lastTextEdit) is TokenizingTextBoxItem lastContainer)
{
lastContainer.UseCharacterAsUser = true; // Make sure we trigger a refresh of suggested items.
lastTextEdit.Text = string.Empty + args.Character;
UpdateCurrentTextEdit(lastTextEdit);
lastContainer.AutoSuggestTextBox.SelectionStart = 1; // Set position to after our new character inserted
lastContainer.AutoSuggestTextBox.Focus(FocusState.Keyboard);
}
}
else
{
//// Otherwise, create a new textbox for this text.
UpdateCurrentTextEdit(new PretokenStringContainer((string.Empty + args.Character).Trim())); // Trim so that 'space' isn't inserted and can be used to insert a new box.
innerItemsSource.Insert(index, currentTextEdit);
void Containerization()
{
if (ContainerFromIndex(index) is TokenizingTextBoxItem newContainer) // Should be our last text box
{
newContainer.UseCharacterAsUser = true; // Make sure we trigger a refresh of suggested items.
void WaitForLoad(object s, RoutedEventArgs eargs)
{
if (newContainer.AutoSuggestTextBox is not null)
{
newContainer.AutoSuggestTextBox.SelectionStart = 1; // Set position to after our new character inserted
newContainer.AutoSuggestTextBox.Focus(FocusState.Keyboard);
}
newContainer.Loaded -= WaitForLoad;
}
newContainer.AutoSuggestTextBoxLoaded += WaitForLoad;
}
}
// Need to wait for containerization
_ = DispatcherQueue.EnqueueAsync(Containerization, DispatcherQueuePriority.Normal);
}
}
// Wait for removal of old items
_ = DispatcherQueue.EnqueueAsync(RemoveOldItems, DispatcherQueuePriority.Normal);
}
else
{
// If no items are selected, send input to the last active string container.
// This code is only fires during an edgecase where an item is in the process of being deleted and the user inputs a character before the focus has been redirected to a string container.
if (innerItemsSource[^1] is ITokenStringContainer textToken)
{
if (ContainerFromIndex(Items.Count - 1) is TokenizingTextBoxItem last) // Should be our last text box
{
string text = last.AutoSuggestTextBox.Text;
int selectionStart = last.AutoSuggestTextBox.SelectionStart;
int position = selectionStart > text.Length ? text.Length : selectionStart;
textToken.Text = text[..position] + args.Character + text[position..];
last.AutoSuggestTextBox.SelectionStart = position + 1; // Set position to after our new character inserted
last.AutoSuggestTextBox.Focus(FocusState.Keyboard);
}
}
}
}
}
private object GetFocusedElement()
{
if (IsXamlRootAvailable && XamlRoot is not null)
{
return FocusManager.GetFocusedElement(XamlRoot);
}
else
{
return FocusManager.GetFocusedElement();
}
}
private void TokenizingTextBoxItem_GotFocus(object sender, RoutedEventArgs e)
{
// Keep track of our currently focused textbox
if (sender is TokenizingTextBoxItem { Content: ITokenStringContainer text })
{
UpdateCurrentTextEdit(text);
}
}
private void TokenizingTextBoxItem_LostFocus(object sender, RoutedEventArgs e)
{
// Keep track of our currently focused textbox
if (sender is TokenizingTextBoxItem { Content: ITokenStringContainer text } &&
string.IsNullOrWhiteSpace(text.Text) && text != lastTextEdit)
{
// We're leaving an inner textbox that's blank, so we'll remove it
innerItemsSource.Remove(text);
UpdateCurrentTextEdit(lastTextEdit);
GuardAgainstPlaceholderTextLayoutIssue();
}
}
private async ValueTask<bool> RemoveTokenAsync(TokenizingTextBoxItem item, object? data = null)
{
data ??= ItemFromContainer(item);
if (TokenItemRemoving is not null)
{
TokenItemRemovingEventArgs tirea = new(data, item);
await TokenItemRemoving.InvokeAsync(this, tirea).ConfigureAwait(true);
if (tirea.Cancel)
{
return false;
}
}
innerItemsSource.Remove(data);
TokenItemRemoved?.Invoke(this, data);
GuardAgainstPlaceholderTextLayoutIssue();
return true;
}
private void GuardAgainstPlaceholderTextLayoutIssue()
{
// If the *PlaceholderText is visible* on the last AutoSuggestBox, it can incorrectly layout itself
// when the *ASB has focus*. We think this is an optimization in the platform, but haven't been able to
// isolate a straight-reproduction of this issue outside of this control (though we have eliminated
// most Toolkit influences like ASB/TextBox Style, the InterspersedObservableCollection, etc...).
// The only Toolkit component involved here should be WrapPanel (which is a straight-forward Panel).
// We also know the ASB itself is adjusting it's size correctly, it's the inner component.
//
// To combat this issue:
// We toggle the visibility of the Placeholder ContentControl in order to force it's layout to update properly
FrameworkElement? placeholder = ContainerFromItem(lastTextEdit)?.FindDescendant("PlaceholderTextContentPresenter");
if (placeholder?.Visibility == Visibility.Visible)
{
placeholder.Visibility = Visibility.Collapsed;
// After we ensure we've hid the control, make it visible again (this is imperceptible to the user).
_ = CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() =>
{
placeholder.Visibility = Visibility.Visible;
});
}
}
private bool MoveFocusAndSelection(MoveDirection direction)
{
bool retVal = false;
if (GetCurrentContainerItem() is { } currentContainerItem)
{
object? currentItem = ItemFromContainer(currentContainerItem);
int previousIndex = Items.IndexOf(currentItem);
int index = previousIndex;
if (direction == MoveDirection.Previous)
{
if (previousIndex > 0)
{
index -= 1;
}
else
{
if (TabNavigateBackOnArrow)
{
FocusManager.TryMoveFocus(FocusNavigationDirection.Previous, new FindNextElementOptions
{
SearchRoot = XamlRoot.Content,
});
}
retVal = true;
}
}
else if (direction == MoveDirection.Next)
{
if (previousIndex < Items.Count - 1)
{
index += 1;
}
}
// Only do stuff if the index is actually changing
if (index != previousIndex)
{
if (ContainerFromIndex(index) is TokenizingTextBoxItem newItem)
{
// Check for the new item being a text control.
// this must happen before focus is set to avoid seeing the caret
// jump in come cases
if (Items[index] is ITokenStringContainer && !IsShiftPressed)
{
newItem.AutoSuggestTextBox.SelectionLength = 0;
newItem.AutoSuggestTextBox.SelectionStart = direction == MoveDirection.Next
? 0
: newItem.AutoSuggestTextBox.Text.Length;
}
newItem.Focus(FocusState.Keyboard);
// if no control keys are selected then the selection also becomes just this item
if (IsShiftPressed)
{
// What we do here depends on where the selection started
// if the previous item is between the start and new position then we add the new item to the selected range
// if the new item is between the start and the previous position then we remove the previous position
int newDistance = Math.Abs(SelectedIndex - index);
int oldDistance = Math.Abs(SelectedIndex - previousIndex);
if (newDistance > oldDistance)
{
SelectedItems.Add(Items[index]);
}
else
{
SelectedItems.Remove(Items[previousIndex]);
}
}
else if (!IsControlPressed)
{
SelectedIndex = index;
// This looks like a bug in the underlying ListViewBase control.
// Might need to be reviewed if the base behavior is fixed
// When two consecutive items are selected and the navigation moves between them,
// the first time that happens the old focused item is not unselected
if (SelectedItems.Count > 1)
{
SelectedItems.Clear();
SelectedIndex = index;
}
}
retVal = true;
}
}
}
return retVal;
}
private TokenizingTextBoxItem? GetCurrentContainerItem()
{
if (IsXamlRootAvailable && XamlRoot is not null)
{
return (TokenizingTextBoxItem)FocusManager.GetFocusedElement(XamlRoot);
}
else
{
return (TokenizingTextBoxItem)FocusManager.GetFocusedElement();
}
}
private void ClearAllTextSelections(TokenizingTextBoxItem? ignoreItem)
{
// Clear any selection in the text box
foreach (object? item in Items)
{
if (item is ITokenStringContainer)
{
if (ContainerFromItem(item) is TokenizingTextBoxItem container)
{
if (container != ignoreItem)
{
container.AutoSuggestTextBox.SelectionLength = 0;
}
}
}
}
}
private bool SelectNewItem(TokenizingTextBoxItem item, int increment, Func<int, bool> testFunc)
{
// find the item in the list
int currentIndex = IndexFromContainer(item);
// Select previous token item (if there is one).
if (testFunc(currentIndex))
{
if (ContainerFromItem(Items[currentIndex + increment]) is ListViewItem newItem)
{
newItem.Focus(FocusState.Keyboard);
SelectedItems.Add(Items[currentIndex + increment]);
return true;
}
}
return false;
}
private async void TokenizingTextBoxItem_ClearAllAction(TokenizingTextBoxItem sender, RoutedEventArgs args)
{
// find the first item selected
int newSelectedIndex = -1;
if (SelectedRanges.Count > 0)
{
newSelectedIndex = SelectedRanges[0].FirstIndex - 1;
}
await RemoveAllSelectedTokens().ConfigureAwait(true);
SelectedIndex = newSelectedIndex;
if (newSelectedIndex is -1)
{
newSelectedIndex = Items.Count - 1;
}
// focus the item prior to the first selected item
if (ContainerFromIndex(newSelectedIndex) is TokenizingTextBoxItem container)
{
container.Focus(FocusState.Keyboard);
}
}
private async void TokenizingTextBoxItem_ClearClicked(TokenizingTextBoxItem sender, RoutedEventArgs? args)
{
await RemoveTokenAsync(sender).ConfigureAwait(true);
}
private void CopySelectedToClipboard()
{
DataPackage dataPackage = new()
{
RequestedOperation = DataPackageOperation.Copy,
};
string tokenString = PrepareSelectionForClipboard();
if (!string.IsNullOrEmpty(tokenString))
{
dataPackage.SetText(tokenString);
Clipboard.SetContent(dataPackage);
}
}
private string PrepareSelectionForClipboard()
{
string tokenString = string.Empty;
bool addSeparator = false;
// Copy all items if none selected (and no text selected)
foreach (object? item in SelectedItems.Count > 0 ? SelectedItems : Items)
{
if (addSeparator)
{
tokenString += TokenDelimiter;
}
else
{
addSeparator = true;
}
if (item is ITokenStringContainer)
{
// grab any selected text
if (ContainerFromItem(item) is TokenizingTextBoxItem { AutoSuggestTextBox: { } textBox })
{
tokenString += textBox.Text.Substring(
textBox.SelectionStart,
textBox.SelectionLength);
}
}
else
{
tokenString += item.ToString();
}
}
return tokenString;
}
}

View File

@@ -1,172 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cw="using:CommunityToolkit.WinUI"
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
xmlns:shct="using:Snap.Hutao.Control.TokenizingTextBox"
xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="ms-appx:///Control/TokenizingTextBox/TokenizingTextBoxItem.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Resources for TokenizingTextBox -->
<Thickness x:Key="TokenizingTextBoxPadding">3,2,3,2</Thickness>
<Thickness x:Key="TokenizingTextBoxPresenterMargin">0,0,6,0</Thickness>
<x:Double x:Key="TokenizingTextBoxTokenSpacing">2</x:Double>
<shct:TokenizingTextBoxStyleSelector
x:Key="TokenizingTextBoxStyleSelector"
TextStyle="{StaticResource TokenizingTextBoxItemTextStyle}"
TokenStyle="{StaticResource TokenizingTextBoxItemTokenStyle}"/>
<!-- Default style for TokenizingTextBox -->
<Style BasedOn="{StaticResource DefaultTokenizingTextBoxStyle}" TargetType="shct:TokenizingTextBox"/>
<Style x:Key="DefaultTokenizingTextBoxStyle" TargetType="shct:TokenizingTextBox">
<Setter Property="AutoSuggestBoxTextBoxStyle" Value="{StaticResource TokenizingTextBoxTextBoxStyle}"/>
<Setter Property="Foreground" Value="{ThemeResource TextControlForeground}"/>
<Setter Property="Background" Value="{ThemeResource TextControlBackground}"/>
<Setter Property="BorderBrush" Value="{ThemeResource TextControlBorderBrush}"/>
<Setter Property="BorderThickness" Value="{StaticResource TextControlBorderThemeThickness}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="TabNavigation" Value="Once"/>
<win:Setter Property="IsSwipeEnabled" Value="False"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Padding" Value="{StaticResource TokenizingTextBoxPadding}"/>
<Setter Property="SelectionMode" Value="Extended"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Disabled"/>
<win:Setter Property="ScrollViewer.IsHorizontalRailEnabled" Value="True"/>
<Setter Property="ScrollViewer.VerticalScrollMode" Value="Enabled"/>
<win:Setter Property="ScrollViewer.IsVerticalRailEnabled" Value="True"/>
<Setter Property="ScrollViewer.ZoomMode" Value="Disabled"/>
<win:Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="False"/>
<Setter Property="ScrollViewer.BringIntoViewOnFocusChange" Value="True"/>
<Setter Property="TokenSpacing" Value="{StaticResource TokenizingTextBoxTokenSpacing}"/>
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}"/>
<Setter Property="IsItemClickEnabled" Value="True"/>
<win:Setter Property="ItemContainerTransitions">
<Setter.Value>
<TransitionCollection/>
</Setter.Value>
</win:Setter>
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<cwc:WrapPanel
cw:FrameworkElementExtensions.AncestorType="shct:TokenizingTextBox"
HorizontalSpacing="{Binding (cw:FrameworkElementExtensions.Ancestor).TokenSpacing, RelativeSource={RelativeSource Self}}"
StretchChild="Last"
VerticalSpacing="{Binding (cw:FrameworkElementExtensions.Ancestor).TokenSpacing, RelativeSource={RelativeSource Self}}"/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyleSelector" Value="{StaticResource TokenizingTextBoxStyleSelector}"/>
<Setter Property="Template" Value="{StaticResource TokenizingTextBoxTemplate}"/>
</Style>
<ControlTemplate x:Key="TokenizingTextBoxTemplate" TargetType="shct:TokenizingTextBox">
<Grid Name="RootPanel">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ContentPresenter
Margin="{ThemeResource TextBoxTopHeaderMargin}"
VerticalAlignment="Top"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="Normal"
Foreground="{ThemeResource TextControlHeaderForeground}"
TextWrapping="Wrap"
Transitions="{TemplateBinding HeaderTransitions}"/>
<Border
x:Name="BackgroundVisual"
Grid.Row="1"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"/>
<Border
x:Name="FocusVisual"
Grid.Row="1"
Background="{ThemeResource TextControlBackgroundFocused}"
BorderBrush="{ThemeResource TextControlBorderBrushFocused}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
Opacity="0"/>
<!-- Background in WinUI is TextControlBackgroundFocused, but that uses a different resource in WinUI than system -->
<ScrollViewer
x:Name="ScrollViewer"
Grid.Row="1"
win:AutomationProperties.AccessibilityView="Raw"
win:IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
win:IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
win:IsHorizontalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsHorizontalScrollChainingEnabled}"
win:IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
win:IsVerticalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsVerticalScrollChainingEnabled}"
BringIntoViewOnFocusChange="{TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
TabNavigation="{TemplateBinding TabNavigation}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}">
<ItemsPresenter Margin="{StaticResource TokenizingTextBoxPresenterMargin}" Padding="{TemplateBinding Padding}"/>
</ScrollViewer>
<ContentPresenter
Grid.Row="2"
VerticalAlignment="Top"
Content="{TemplateBinding Footer}"
ContentTemplate="{TemplateBinding FooterTemplate}"
FontWeight="Normal"
TextWrapping="Wrap"
Transitions="{TemplateBinding FooterTransitions}"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BackgroundVisual" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBackgroundDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BackgroundVisual" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBorderBrushDisabled}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BackgroundVisual" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="BackgroundVisual" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<VisualState.Setters>
<Setter Target="BackgroundVisual.BorderBrush" Value="Transparent"/>
<Setter Target="FocusVisual.BorderThickness" Value="{ThemeResource TextControlBorderThemeThicknessFocused}"/>
<Setter Target="FocusVisual.Opacity" Value="1"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unfocused"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</ResourceDictionary>

View File

@@ -1,84 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Automation.Peers;
using Microsoft.UI.Xaml.Automation.Provider;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.TokenizingTextBox;
internal class TokenizingTextBoxAutomationPeer : ListViewBaseAutomationPeer, IValueProvider
{
public TokenizingTextBoxAutomationPeer(TokenizingTextBox owner)
: base(owner)
{
}
public bool IsReadOnly => !OwningTokenizingTextBox.IsEnabled;
public string Value => OwningTokenizingTextBox.Text;
private TokenizingTextBox OwningTokenizingTextBox
{
get => (TokenizingTextBox)Owner;
}
public void SetValue(string value)
{
if (IsReadOnly)
{
throw new ElementNotEnabledException($"Could not set the value of the {nameof(TokenizingTextBox)} ");
}
OwningTokenizingTextBox.Text = value;
}
protected override string GetClassNameCore()
{
return Owner.GetType().Name;
}
protected override string GetNameCore()
{
string name = OwningTokenizingTextBox.Name;
if (!string.IsNullOrWhiteSpace(name))
{
return name;
}
name = AutomationProperties.GetName(OwningTokenizingTextBox);
return !string.IsNullOrWhiteSpace(name) ? name : base.GetNameCore();
}
protected override object GetPatternCore(PatternInterface patternInterface)
{
return patternInterface switch
{
PatternInterface.Value => this,
_ => base.GetPatternCore(patternInterface),
};
}
protected override IList<AutomationPeer> GetChildrenCore()
{
TokenizingTextBox owner = OwningTokenizingTextBox;
ItemCollection items = owner.Items;
if (items.Count <= 0)
{
return default!;
}
List<AutomationPeer> peers = new(items.Count);
for (int i = 0; i < items.Count; i++)
{
if (owner.ContainerFromIndex(i) is TokenizingTextBoxItem element)
{
peers.Add(FromElement(element) ?? CreatePeerForElement(element));
}
}
return peers;
}
}

View File

@@ -1,480 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Windows.Foundation;
using Windows.System;
namespace Snap.Hutao.Control.TokenizingTextBox;
[DependencyProperty("ClearButtonStyle", typeof(Style))]
[DependencyProperty("Owner", typeof(TokenizingTextBox))]
[TemplatePart(Name = PART_ClearButton, Type = typeof(ButtonBase))] //// Token case
[TemplatePart(Name = PART_AutoSuggestBox, Type = typeof(Microsoft.UI.Xaml.Controls.AutoSuggestBox))] //// String case
[TemplatePart(Name = PART_TokensCounter, Type = typeof(TextBlock))]
[SuppressMessage("", "SA1124")]
internal partial class TokenizingTextBoxItem : ListViewItem
{
private const string PART_ClearButton = "PART_RemoveButton";
private const string PART_AutoSuggestBox = "PART_AutoSuggestBox";
private const string PART_TokensCounter = "PART_TokensCounter";
private const string QueryButton = "QueryButton";
private Microsoft.UI.Xaml.Controls.AutoSuggestBox autoSuggestBox;
private TextBox autoSuggestTextBox;
private Button clearButton;
private bool isSelectedFocusOnFirstCharacter;
private bool isSelectedFocusOnLastCharacter;
public TokenizingTextBoxItem()
{
DefaultStyleKey = typeof(TokenizingTextBoxItem);
// TODO: only add these if token?
RightTapped += TokenizingTextBoxItem_RightTapped;
KeyDown += TokenizingTextBoxItem_KeyDown;
}
public event TypedEventHandler<TokenizingTextBoxItem, RoutedEventArgs>? AutoSuggestTextBoxLoaded;
public event TypedEventHandler<TokenizingTextBoxItem, RoutedEventArgs>? ClearClicked;
public event TypedEventHandler<TokenizingTextBoxItem, RoutedEventArgs>? ClearAllAction;
public TextBox AutoSuggestTextBox { get => autoSuggestTextBox; }
public bool UseCharacterAsUser { get; set; }
private bool IsCaretAtStart
{
get => autoSuggestTextBox?.SelectionStart is 0;
}
private bool IsCaretAtEnd
{
get => autoSuggestTextBox?.SelectionStart == autoSuggestTextBox?.Text.Length || autoSuggestTextBox?.SelectionStart + autoSuggestTextBox?.SelectionLength == autoSuggestTextBox?.Text.Length;
}
private bool IsAllSelected
{
get => autoSuggestTextBox?.SelectedText == autoSuggestTextBox?.Text && !string.IsNullOrEmpty(autoSuggestTextBox?.Text);
}
// Called to update text by link:TokenizingTextBox.Properties.cs:TextPropertyChanged
public void UpdateText(string text)
{
if (autoSuggestBox is not null)
{
autoSuggestBox.Text = text;
}
else
{
void WaitForLoad(object s, RoutedEventArgs eargs)
{
if (autoSuggestTextBox is not null)
{
autoSuggestTextBox.Text = text;
}
AutoSuggestTextBoxLoaded -= WaitForLoad;
}
AutoSuggestTextBoxLoaded += WaitForLoad;
}
}
/// <inheritdoc/>
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (GetTemplateChild(PART_AutoSuggestBox) is Microsoft.UI.Xaml.Controls.AutoSuggestBox suggestbox)
{
OnApplyTemplateAutoSuggestBox(suggestbox);
}
if (clearButton is not null)
{
clearButton.Click -= ClearButton_Click;
}
clearButton = (Button)GetTemplateChild(PART_ClearButton);
if (clearButton is not null)
{
clearButton.Click += ClearButton_Click;
}
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
ClearClicked?.Invoke(this, e);
}
private void TokenizingTextBoxItem_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
ContextFlyout.ShowAt(this);
}
private void TokenizingTextBoxItem_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (Content is not ITokenStringContainer)
{
// We only want to 'remove' our token if we're not a textbox.
switch (e.Key)
{
case VirtualKey.Back:
case VirtualKey.Delete:
{
ClearAllAction?.Invoke(this, e);
break;
}
}
}
}
/// Called from <see cref="OnApplyTemplate"/>
private void OnApplyTemplateAutoSuggestBox(Microsoft.UI.Xaml.Controls.AutoSuggestBox auto)
{
if (autoSuggestBox is not null)
{
autoSuggestBox.Loaded -= OnASBLoaded;
autoSuggestBox.QuerySubmitted -= AutoSuggestBox_QuerySubmitted;
autoSuggestBox.SuggestionChosen -= AutoSuggestBox_SuggestionChosen;
autoSuggestBox.TextChanged -= AutoSuggestBox_TextChanged;
autoSuggestBox.PointerEntered -= AutoSuggestBox_PointerEntered;
autoSuggestBox.PointerExited -= AutoSuggestBox_PointerExited;
autoSuggestBox.PointerCanceled -= AutoSuggestBox_PointerExited;
autoSuggestBox.PointerCaptureLost -= AutoSuggestBox_PointerExited;
autoSuggestBox.GotFocus -= AutoSuggestBox_GotFocus;
autoSuggestBox.LostFocus -= AutoSuggestBox_LostFocus;
// Remove any previous QueryIcon
autoSuggestBox.QueryIcon = default;
}
autoSuggestBox = auto;
if (autoSuggestBox is not null)
{
autoSuggestBox.Loaded += OnASBLoaded;
autoSuggestBox.QuerySubmitted += AutoSuggestBox_QuerySubmitted;
autoSuggestBox.SuggestionChosen += AutoSuggestBox_SuggestionChosen;
autoSuggestBox.TextChanged += AutoSuggestBox_TextChanged;
autoSuggestBox.PointerEntered += AutoSuggestBox_PointerEntered;
autoSuggestBox.PointerExited += AutoSuggestBox_PointerExited;
autoSuggestBox.PointerCanceled += AutoSuggestBox_PointerExited;
autoSuggestBox.PointerCaptureLost += AutoSuggestBox_PointerExited;
autoSuggestBox.GotFocus += AutoSuggestBox_GotFocus;
autoSuggestBox.LostFocus += AutoSuggestBox_LostFocus;
// Setup a binding to the QueryIcon of the Parent if we're the last box.
if (Content is ITokenStringContainer str)
{
// We need to set our initial text in all cases.
autoSuggestBox.Text = str.Text;
// We only set/bind some properties on the last textbox to mimic the autosuggestbox look
if (str.IsLast)
{
// Workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/2568
if (Owner.QueryIcon is FontIconSource fis &&
fis.ReadLocalValue(FontIconSource.FontSizeProperty) == DependencyProperty.UnsetValue)
{
// This can be expensive, could we optimize?
// Also, this is changing the FontSize on the IconSource (which could be shared?)
fis.FontSize = Owner.TryFindResource("TokenizingTextBoxIconFontSize") as double? ?? 16;
}
Binding iconBinding = new()
{
Source = Owner,
Path = new PropertyPath(nameof(Owner.QueryIcon)),
RelativeSource = new()
{
Mode = RelativeSourceMode.TemplatedParent,
},
};
IconSourceElement iconSourceElement = new();
iconSourceElement.SetBinding(IconSourceElement.IconSourceProperty, iconBinding);
autoSuggestBox.QueryIcon = iconSourceElement;
}
}
}
}
private async void AutoSuggestBox_QuerySubmitted(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
Owner.RaiseQuerySubmitted(sender, args);
object? chosenItem = default;
if (args.ChosenSuggestion is not null)
{
chosenItem = args.ChosenSuggestion;
}
else if (!string.IsNullOrWhiteSpace(args.QueryText))
{
chosenItem = args.QueryText;
}
if (chosenItem is not null)
{
await Owner.AddTokenAsync(chosenItem).ConfigureAwait(true); // TODO: Need to pass index?
sender.Text = string.Empty;
Owner.Text = string.Empty;
sender.Focus(FocusState.Programmatic);
}
}
private void AutoSuggestBox_SuggestionChosen(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args)
{
Owner.RaiseSuggestionChosen(sender, args);
}
private void AutoSuggestBox_TextChanged(Microsoft.UI.Xaml.Controls.AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if (sender.Text is null)
{
return;
}
if (!EqualityComparer<string>.Default.Equals(sender.Text, Owner.Text))
{
Owner.Text = sender.Text; // Update parent text property, if different
}
// Override our programmatic manipulation as we're redirecting input for the user
if (UseCharacterAsUser)
{
UseCharacterAsUser = false;
args.Reason = AutoSuggestionBoxTextChangeReason.UserInput;
}
Owner.RaiseTextChanged(sender, args);
string t = sender.Text?.Trim() ?? string.Empty;
// Look for Token Delimiters to create new tokens when text changes.
if (!string.IsNullOrEmpty(Owner.TokenDelimiter) && t.Contains(Owner.TokenDelimiter, StringComparison.OrdinalIgnoreCase))
{
bool lastDelimited = t[^1] == Owner.TokenDelimiter[0];
string[] tokens = t.Split(Owner.TokenDelimiter);
int numberToProcess = lastDelimited ? tokens.Length : tokens.Length - 1;
for (int position = 0; position < numberToProcess; position++)
{
string token = tokens[position];
token = token.Trim();
if (token.Length > 0)
{
_ = Owner.AddTokenAsync(token); //// TODO: Pass Index?
}
}
if (lastDelimited)
{
sender.Text = string.Empty;
}
else
{
sender.Text = tokens[^1].Trim();
}
}
}
private void AutoSuggestBox_PointerEntered(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(Owner, TokenizingTextBox.PointerOverState, true);
}
private void AutoSuggestBox_PointerExited(object sender, PointerRoutedEventArgs e)
{
VisualStateManager.GoToState(Owner, TokenizingTextBox.NormalState, true);
}
private void AutoSuggestBox_LostFocus(object sender, RoutedEventArgs e)
{
VisualStateManager.GoToState(Owner, TokenizingTextBox.UnfocusedState, true);
}
private void AutoSuggestBox_GotFocus(object sender, RoutedEventArgs e)
{
// Verify if the usual behavior of clearing token selection is required
if (Owner.PauseTokenClearOnFocus == false && !TokenizingTextBox.IsShiftPressed)
{
// Clear any selected tokens
Owner.DeselectAll();
}
Owner.PauseTokenClearOnFocus = false;
VisualStateManager.GoToState(Owner, TokenizingTextBox.FocusedState, true);
}
private void OnASBLoaded(object sender, RoutedEventArgs e)
{
if (autoSuggestTextBox is not null)
{
autoSuggestTextBox.PreviewKeyDown -= AutoSuggestTextBox_PreviewKeyDown;
autoSuggestTextBox.TextChanging -= AutoSuggestTextBox_TextChangingAsync;
autoSuggestTextBox.SelectionChanged -= AutoSuggestTextBox_SelectionChanged;
autoSuggestTextBox.SelectionChanging -= AutoSuggestTextBox_SelectionChanging;
}
autoSuggestTextBox ??= autoSuggestBox.FindDescendant<TextBox>()!;
UpdateQueryIconVisibility();
UpdateTokensCounter(this);
// Local function for Selection changed
void AutoSuggestTextBox_SelectionChanged(object box, RoutedEventArgs args)
{
if (!(IsAllSelected || TokenizingTextBox.IsShiftPressed || Owner.IsClearingForClick))
{
Owner.DeselectAllTokensAndText(this);
}
// Ensure flag is always reset
Owner.IsClearingForClick = false;
}
// local function for clearing selection on interaction with text box
async void AutoSuggestTextBox_TextChangingAsync(TextBox o, TextBoxTextChangingEventArgs args)
{
// remove any selected tokens.
if (Owner.SelectedItems.Count > 1)
{
await Owner.RemoveAllSelectedTokens().ConfigureAwait(true);
}
}
if (autoSuggestTextBox is not null)
{
autoSuggestTextBox.PreviewKeyDown += AutoSuggestTextBox_PreviewKeyDown;
autoSuggestTextBox.TextChanging += AutoSuggestTextBox_TextChangingAsync;
autoSuggestTextBox.SelectionChanged += AutoSuggestTextBox_SelectionChanged;
autoSuggestTextBox.SelectionChanging += AutoSuggestTextBox_SelectionChanging;
AutoSuggestTextBoxLoaded?.Invoke(this, e);
}
}
private void AutoSuggestTextBox_SelectionChanging(TextBox sender, TextBoxSelectionChangingEventArgs args)
{
isSelectedFocusOnFirstCharacter = args.SelectionLength > 0 && args.SelectionStart is 0 && autoSuggestTextBox.SelectionStart > 0;
isSelectedFocusOnLastCharacter =
//// see if we are NOW on the last character.
//// test if the new selection includes the last character, and the current selection doesn't
(args.SelectionStart + args.SelectionLength == autoSuggestTextBox.Text.Length) &&
(autoSuggestTextBox.SelectionStart + autoSuggestTextBox.SelectionLength != autoSuggestTextBox.Text.Length);
}
private void AutoSuggestTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (IsCaretAtStart &&
(e.Key is VirtualKey.Back ||
e.Key is VirtualKey.Left))
{
// if the back key is pressed and there is any selection in the text box then the text box can handle it
if ((e.Key is VirtualKey.Left && isSelectedFocusOnFirstCharacter) ||
autoSuggestTextBox.SelectionLength is 0)
{
if (Owner.SelectPreviousItem(this))
{
if (!TokenizingTextBox.IsShiftPressed)
{
// Clear any text box selection
autoSuggestTextBox.SelectionLength = 0;
}
e.Handled = true;
}
}
}
else if (IsCaretAtEnd && e.Key is VirtualKey.Right)
{
// if the back key is pressed and there is any selection in the text box then the text box can handle it
if (isSelectedFocusOnLastCharacter || autoSuggestTextBox.SelectionLength is 0)
{
if (Owner.SelectNextItem(this))
{
if (!TokenizingTextBox.IsShiftPressed)
{
// Clear any text box selection
autoSuggestTextBox.SelectionLength = 0;
}
e.Handled = true;
}
}
}
else if (e.Key is VirtualKey.A && TokenizingTextBox.IsControlPressed)
{
// Need to provide this shortcut from the textbox only, as ListViewBase will do it for us on token.
Owner.SelectAllTokensAndText();
}
}
private void UpdateTokensCounter(TokenizingTextBoxItem ttbi)
{
if (autoSuggestBox?.FindDescendant(PART_TokensCounter) is TextBlock maxTokensCounter)
{
void OnTokenCountChanged(TokenizingTextBox ttb, object? value = default)
{
if (ttb.ItemsSource is InterspersedObservableCollection itemsSource)
{
int currentTokens = itemsSource.ItemsSource.Count;
int maxTokens = ttb.MaximumTokens;
maxTokensCounter.Text = $"{currentTokens}/{maxTokens}";
maxTokensCounter.Visibility = Visibility.Visible;
string targetState = (currentTokens >= maxTokens)
? TokenizingTextBox.MaxReachedState
: TokenizingTextBox.MaxUnreachedState;
VisualStateManager.GoToState(autoSuggestTextBox, targetState, true);
}
}
ttbi.Owner.TokenItemAdded -= OnTokenCountChanged;
ttbi.Owner.TokenItemRemoved -= OnTokenCountChanged;
if (Content is ITokenStringContainer { IsLast: true } str && ttbi is { Owner.MaximumTokens: >= 0 })
{
ttbi.Owner.TokenItemAdded += OnTokenCountChanged;
ttbi.Owner.TokenItemRemoved += OnTokenCountChanged;
OnTokenCountChanged(ttbi.Owner);
}
else
{
maxTokensCounter.Visibility = Visibility.Collapsed;
maxTokensCounter.Text = string.Empty;
}
}
}
private void UpdateQueryIconVisibility()
{
if (autoSuggestBox.FindDescendant(QueryButton) is Button queryButton)
{
if (Owner.QueryIcon is not null)
{
queryButton.Visibility = Visibility.Visible;
}
else
{
queryButton.Visibility = Visibility.Collapsed;
}
}
}
}

View File

@@ -1,837 +0,0 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
xmlns:shct="using:Snap.Hutao.Control.TokenizingTextBox"
xmlns:win="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<StaticResource x:Key="TokenItemBackground" ResourceKey="ControlFillColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBackgroundPointerOver" ResourceKey="ControlFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBackgroundPressed" ResourceKey="ControlFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBackgroundSelected" ResourceKey="AccentFillColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBackgroundPointerOverSelected" ResourceKey="AccentFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBackgroundPressedSelected" ResourceKey="AccentFillColorTertiaryBrush"/>
<StaticResource x:Key="TokenItemBorderBrush" ResourceKey="ControlStrokeColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPointerOver" ResourceKey="ControlStrokeColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPressed" ResourceKey="ControlStrokeColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushSelected" ResourceKey="AccentFillColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPointerOverSelected" ResourceKey="AccentFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPressedSelected" ResourceKey="AccentFillColorTertiaryBrush"/>
<StaticResource x:Key="TokenItemForeground" ResourceKey="TextFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPointerOver" ResourceKey="TextFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPressed" ResourceKey="TextFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemForegroundSelected" ResourceKey="TextOnAccentFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPointerOverSelected" ResourceKey="TextOnAccentFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPressedSelected" ResourceKey="TextOnAccentFillColorSecondaryBrush"/>
<Thickness x:Key="TokenItemBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<StaticResource x:Key="TokenItemBackground" ResourceKey="ControlFillColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBackgroundPointerOver" ResourceKey="ControlFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBackgroundPressed" ResourceKey="ControlFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBackgroundSelected" ResourceKey="AccentFillColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBackgroundPointerOverSelected" ResourceKey="AccentFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBackgroundPressedSelected" ResourceKey="AccentFillColorTertiaryBrush"/>
<StaticResource x:Key="TokenItemBorderBrush" ResourceKey="ControlStrokeColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPointerOver" ResourceKey="ControlStrokeColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPressed" ResourceKey="ControlStrokeColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushSelected" ResourceKey="AccentFillColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPointerOverSelected" ResourceKey="AccentFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPressedSelected" ResourceKey="AccentFillColorTertiaryBrush"/>
<StaticResource x:Key="TokenItemForeground" ResourceKey="TextFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPointerOver" ResourceKey="TextFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPressed" ResourceKey="TextFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemForegroundSelected" ResourceKey="TextOnAccentFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPointerOverSelected" ResourceKey="TextOnAccentFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPressedSelected" ResourceKey="TextOnAccentFillColorSecondaryBrush"/>
<Thickness x:Key="TokenItemBorderThickness">1</Thickness>
</ResourceDictionary>
<ResourceDictionary x:Key="HighContrast">
<StaticResource x:Key="TokenItemBackground" ResourceKey="ControlFillColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBackgroundPointerOver" ResourceKey="ControlFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBackgroundPressed" ResourceKey="ControlFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBackgroundSelected" ResourceKey="AccentFillColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBackgroundPointerOverSelected" ResourceKey="AccentFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBackgroundPressedSelected" ResourceKey="AccentFillColorTertiaryBrush"/>
<StaticResource x:Key="TokenItemBorderBrush" ResourceKey="ControlStrokeColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPointerOver" ResourceKey="ControlStrokeColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPressed" ResourceKey="ControlStrokeColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushSelected" ResourceKey="AccentFillColorDefaultBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPointerOverSelected" ResourceKey="AccentFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemBorderBrushPressedSelected" ResourceKey="AccentFillColorTertiaryBrush"/>
<StaticResource x:Key="TokenItemForeground" ResourceKey="TextFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPointerOver" ResourceKey="TextFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPressed" ResourceKey="TextFillColorSecondaryBrush"/>
<StaticResource x:Key="TokenItemForegroundSelected" ResourceKey="TextOnAccentFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPointerOverSelected" ResourceKey="TextOnAccentFillColorPrimaryBrush"/>
<StaticResource x:Key="TokenItemForegroundPressedSelected" ResourceKey="TextOnAccentFillColorSecondaryBrush"/>
<Thickness x:Key="TokenItemBorderThickness">1</Thickness>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!--<Thickness x:Key="ButtonPadding">8,4,8,5</Thickness> Not sure if we'll also need this later-->
<Thickness x:Key="TextControlThemePadding">10,3,6,6</Thickness>
<!-- Need local copy of this, as including WinUI overrides this to something that adds too much padding for our inner box -->
<x:Double x:Key="TokenizingTextBoxIconFontSize">10</x:Double>
<Thickness x:Key="TokenItemPadding">8,4,4,4</Thickness>
<Thickness x:Key="TokenizingTextBoxItemPresenterMargin">0,-2,0,1</Thickness>
<HorizontalAlignment x:Key="TokenizingTextBoxItemHorizontalContentAlignment">Center</HorizontalAlignment>
<VerticalAlignment x:Key="TokenizingTextBoxItemVerticalContentAlignment">Center</VerticalAlignment>
<CornerRadius x:Key="PopupOverlayCornerRadius">0,0,0,8</CornerRadius>
<Style x:Key="TokenizingTextBoxDeleteButtonStyle" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid
x:Name="ButtonLayoutGrid"
Margin="{StaticResource AutoSuggestBoxDeleteButtonMargin}"
Background="{ThemeResource TextControlButtonBackground}"
BorderBrush="{ThemeResource TextControlButtonBorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{ThemeResource ControlCornerRadius}">
<!-- FontSize is ignored here, see https://github.com/microsoft/microsoft-ui-xaml/issues/2568 -->
<!-- Set in code-behind link:TokenizingTextBoxItem.AutoSuggestBox.cs#L104 -->
<TextBlock
x:Name="GlyphElement"
HorizontalAlignment="Center"
VerticalAlignment="Center"
win:AutomationProperties.AccessibilityView="Raw"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="{ThemeResource TokenizingTextBoxIconFontSize}"
FontStyle="Normal"
Foreground="{ThemeResource TextControlButtonForeground}"
Text="&#xE894;"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonLayoutGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonLayoutGrid" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="GlyphElement" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonLayoutGrid" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonBackgroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ButtonLayoutGrid" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonBorderBrushPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="GlyphElement" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonForegroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ButtonLayoutGrid"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Name="TokenizingTextBoxQueryButtonStyle" TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter
x:Name="ContentPresenter"
Margin="{ThemeResource AutoSuggestBoxInnerButtonMargin}"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
muxc:AnimatedIcon.State="Normal"
win:AutomationProperties.AccessibilityView="Raw"
Background="{ThemeResource TextControlButtonBackground}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{ThemeResource TextControlButtonBorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
CornerRadius="{TemplateBinding CornerRadius}"
FontSize="{TemplateBinding FontSize}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonBackgroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonBorderBrushPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(muxc:AnimatedIcon.State)" Value="PointerOver"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonBackgroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="BorderBrush">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonBorderBrushPressed}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonForegroundPressed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(muxc:AnimatedIcon.State)" Value="Pressed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation
Storyboard.TargetName="ContentPresenter"
Storyboard.TargetProperty="Opacity"
To="0"
Duration="0"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Inner TextBox Style with removed borders to look like part of the outer textbox facade we've setup -->
<Style
x:Key="TokenizingTextBoxTextBoxStyle"
BasedOn="{StaticResource AutoSuggestBoxTextBoxStyle}"
TargetType="TextBox">
<Setter Property="MinWidth" Value="{ThemeResource TextControlThemeMinWidth}"/>
<Setter Property="Foreground" Value="{ThemeResource TextControlForeground}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="SelectionHighlightColor" Value="{ThemeResource TextControlSelectionHighlightColor}"/>
<Setter Property="BorderThickness" Value="0"/>
<!-- Override -->
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/>
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/>
<Setter Property="ScrollViewer.HorizontalScrollMode" Value="Auto"/>
<Setter Property="ScrollViewer.VerticalScrollMode" Value="Auto"/>
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden"/>
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden"/>
<win:Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="False"/>
<Setter Property="Padding" Value="8,5,6,6"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="MinHeight" Value="28"/>
<!-- Override -->
<win:Setter Property="SelectionHighlightColorWhenNotFocused" Value="{ThemeResource TextControlSelectionHighlightColor}"/>
<!-- Override -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Grid ColumnSpacing="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border
x:Name="BorderElement"
Grid.Row="1"
Grid.RowSpan="1"
Grid.ColumnSpan="3"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"/>
<ContentPresenter
x:Name="HeaderContentPresenter"
Grid.Row="0"
Grid.ColumnSpan="3"
Margin="{ThemeResource AutoSuggestBoxTopHeaderMargin}"
x:DeferLoadStrategy="Lazy"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
FontWeight="Normal"
Foreground="{ThemeResource TextControlHeaderForeground}"
TextWrapping="Wrap"
Visibility="Collapsed"/>
<ScrollViewer
x:Name="ContentElement"
Grid.Row="1"
Margin="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
win:AutomationProperties.AccessibilityView="Raw"
win:IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
win:IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
win:IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
IsTabStop="False"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
ZoomMode="Disabled"/>
<ContentControl
x:Name="PlaceholderTextContentPresenter"
Grid.Row="1"
Grid.ColumnSpan="2"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="Center"
Content="{TemplateBinding PlaceholderText}"
Foreground="{ThemeResource TextControlPlaceholderForeground}"
IsHitTestVisible="False"
IsTabStop="False"/>
<Button
x:Name="DeleteButton"
Grid.Row="1"
Grid.Column="1"
Width="32"
Height="28"
Padding="{ThemeResource HelperButtonThemePadding}"
VerticalAlignment="Stretch"
win:AutomationProperties.AccessibilityView="Raw"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
FontSize="{TemplateBinding FontSize}"
IsTabStop="False"
Style="{StaticResource TokenizingTextBoxDeleteButtonStyle}"
Visibility="Collapsed"/>
<TextBlock
Name="PART_TokensCounter"
Grid.Row="1"
Grid.Column="2"
Margin="2,0,0,2"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"/>
<Button
x:Name="QueryButton"
Grid.Row="1"
Grid.Column="3"
Width="32"
Height="28"
Margin="0,0,2,0"
Padding="0"
VerticalAlignment="Stretch"
win:AutomationProperties.AccessibilityView="Raw"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
FontSize="{TemplateBinding FontSize}"
IsTabStop="False"
Style="{StaticResource TokenizingTextBoxQueryButtonStyle}"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="HeaderContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlHeaderForegroundDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentElement" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlForegroundDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PlaceholderTextContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlPlaceholderForegroundDisabled}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PlaceholderTextContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlPlaceholderForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentElement" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlForegroundPointerOver}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Focused">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PlaceholderTextContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlPlaceholderForegroundFocused}"/>
<!-- WinUI override -->
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentElement" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlForegroundFocused}"/>
<!-- WinUI override -->
</ObjectAnimationUsingKeyFrames>
<!--<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentElement"
Storyboard.TargetProperty="RequestedTheme">
<DiscreteObjectKeyFrame KeyTime="0"
Value="Light" />
</ObjectAnimationUsingKeyFrames>-->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="QueryButton" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextControlButtonForeground}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ButtonStates">
<VisualState x:Name="ButtonVisible">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="DeleteButton" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ButtonCollapsed"/>
</VisualStateGroup>
<VisualStateGroup x:Name="TokensCounterStates">
<VisualState x:Name="MaxReachedState">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_TokensCounter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SystemFillColorCriticalBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="MaxUnreachedState"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TokenizingTextBoxItemTextStyle" TargetType="shct:TokenizingTextBoxItem">
<Setter Property="Background" Value="{ThemeResource SystemControlBackgroundChromeMediumLowBrush}"/>
<Setter Property="BorderBrush" Value="{ThemeResource SystemControlTransparentBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="UseSystemFocusVisuals" Value="False"/>
<Setter Property="MinWidth" Value="128"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="shct:TokenizingTextBoxItem">
<AutoSuggestBox
Name="PART_AutoSuggestBox"
DisplayMemberPath="{Binding Path=Owner.DisplayMemberPath, RelativeSource={RelativeSource Mode=TemplatedParent}}"
ItemTemplate="{Binding Path=Owner.SuggestedItemTemplate, RelativeSource={RelativeSource Mode=TemplatedParent}}"
ItemsSource="{Binding Path=Owner.SuggestedItemsSource, RelativeSource={RelativeSource Mode=TemplatedParent}}"
PlaceholderText="{Binding Path=Owner.PlaceholderText, RelativeSource={RelativeSource Mode=TemplatedParent}}"
Style="{StaticResource SystemAutoSuggestBoxStyle}"
Text="{Binding Text, Mode=TwoWay}"
TextBoxStyle="{StaticResource TokenizingTextBoxTextBoxStyle}"
TextMemberPath="{Binding Path=Owner.TextMemberPath, RelativeSource={RelativeSource Mode=TemplatedParent}}"
UpdateTextOnSelect="False"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Copy of System Style from 18362 to try and workaround WinUI Styles -->
<Style x:Key="SystemAutoSuggestBoxStyle" TargetType="AutoSuggestBox">
<Setter Property="VerticalAlignment" Value="Top"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="TextBoxStyle" Value="{StaticResource AutoSuggestBoxTextBoxStyle}"/>
<Setter Property="UseSystemFocusVisuals" Value="{ThemeResource IsApplicationFocusVisualKindReveal}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="AutoSuggestBox">
<Grid x:Name="LayoutRoot">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox
x:Name="TextBox"
Width="{TemplateBinding Width}"
Margin="0"
win:DesiredCandidateWindowAlignment="BottomEdge"
Canvas.ZIndex="0"
CornerRadius="4"
Header="{TemplateBinding Header}"
PlaceholderText="{TemplateBinding PlaceholderText}"
ScrollViewer.BringIntoViewOnFocusChange="False"
Style="{TemplateBinding TextBoxStyle}"
UseSystemFocusVisuals="{TemplateBinding UseSystemFocusVisuals}"/>
<Popup x:Name="SuggestionsPopup">
<Border
x:Name="SuggestionsContainer"
Background="{ThemeResource AutoSuggestBoxSuggestionsListBackground}"
CornerRadius="{StaticResource PopupOverlayCornerRadius}">
<ListView
x:Name="SuggestionsList"
MaxHeight="{ThemeResource AutoSuggestListMaxHeight}"
Margin="{ThemeResource AutoSuggestListPadding}"
Padding="{ThemeResource AutoSuggestListPadding}"
BorderBrush="{ThemeResource AutoSuggestBoxSuggestionsListBorderBrush}"
BorderThickness="{ThemeResource AutoSuggestListBorderThemeThickness}"
DisplayMemberPath="{TemplateBinding DisplayMemberPath}"
IsItemClickEnabled="True"
ItemContainerStyle="{TemplateBinding ItemContainerStyle}"
ItemTemplate="{TemplateBinding ItemTemplate}"
ItemTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
</Border>
</Popup>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="Orientation">
<VisualState x:Name="Landscape"/>
<VisualState x:Name="Portrait"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TokenizingTextBoxItemTokenStyle" TargetType="shct:TokenizingTextBoxItem">
<Setter Property="ClearButtonStyle" Value="{StaticResource SubtleButtonStyle}"/>
<Setter Property="HorizontalContentAlignment" Value="{StaticResource TokenizingTextBoxItemHorizontalContentAlignment}"/>
<Setter Property="VerticalContentAlignment" Value="{StaticResource TokenizingTextBoxItemVerticalContentAlignment}"/>
<!--<Setter Property="HorizontalAlignment" Value="Center" />-->
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/>
<Setter Property="Background" Value="{ThemeResource TokenItemBackground}"/>
<Setter Property="BorderBrush" Value="{ThemeResource TokenItemBorderBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource TokenItemBorderThickness}"/>
<Setter Property="CornerRadius" Value="2"/>
<Setter Property="Padding" Value="{ThemeResource TokenItemPadding}"/>
<Setter Property="IsTabStop" Value="True"/>
<Setter Property="TabNavigation" Value="Local"/>
<Setter Property="Foreground" Value="{ThemeResource TokenItemForeground}"/>
<Setter Property="FontWeight" Value="Normal"/>
<Setter Property="UseSystemFocusVisuals" Value="True"/>
<Setter Property="FocusVisualMargin" Value="-3"/>
<Setter Property="BackgroundSizing" Value="InnerBorderEdge"/>
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="shct:TokenizingTextBoxItem">
<Grid
x:Name="PART_RootGrid"
Height="{TemplateBinding Height}"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ColumnSpacing="8"
Control.IsTemplateFocusTarget="True"
CornerRadius="{TemplateBinding CornerRadius}">
<win:Grid.BackgroundTransition>
<win:BrushTransition Duration="0:0:0.083"/>
</win:Grid.BackgroundTransition>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Content -->
<ContentPresenter
x:Name="PART_ContentPresenter"
Margin="0,-1,0,0"
VerticalAlignment="Center"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
win:OpticalMarginAlignment="TrimSideBearings"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
Content="{TemplateBinding Content}"
ContentTransitions="{TemplateBinding ContentTransitions}"
FontWeight="{TemplateBinding FontWeight}"
Foreground="{TemplateBinding Foreground}"/>
<Button
x:Name="PART_RemoveButton"
Grid.Row="1"
Grid.Column="1"
Width="20"
Height="20"
MinWidth="0"
MinHeight="0"
Margin="0,0,0,-2"
Padding="2"
VerticalAlignment="Center"
CornerRadius="99"
IsTabStop="False"
Style="{TemplateBinding ClearButtonStyle}">
<FontIcon FontSize="12" Glyph="&#xE894;"/>
</Button>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource TokenItemBackgroundPointerOver}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource TokenItemBackgroundPressed}"/>
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource TokenItemBorderBrushPointerOver}"/>
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TokenItemForegroundPointerOver}"/>
<Setter Target="PART_RemoveButton.Foreground" Value="{ThemeResource TokenItemForegroundPointerOver}"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource TokenItemBackgroundSelected}"/>
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource TokenItemBorderBrushSelected}"/>
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TokenItemForegroundSelected}"/>
<Setter Target="PART_RemoveButton.Style" Value="{ThemeResource SubtleAccentButtonStyle}"/>
<Setter Target="PART_RootGrid.BackgroundSizing" Value="OuterBorderEdge"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOverSelected">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource TokenItemBackgroundPointerOverSelected}"/>
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource TokenItemBorderBrushPointerOverSelected}"/>
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TokenItemForegroundPointerOverSelected}"/>
<Setter Target="PART_RemoveButton.Style" Value="{ThemeResource SubtleAccentButtonStyle}"/>
<Setter Target="PART_RootGrid.BackgroundSizing" Value="OuterBorderEdge"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PressedSelected">
<VisualState.Setters>
<Setter Target="PART_RootGrid.Background" Value="{ThemeResource TokenItemBackgroundPressedSelected}"/>
<Setter Target="PART_RootGrid.BorderBrush" Value="{ThemeResource TokenItemBorderBrushPressedSelected}"/>
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TokenItemForegroundPressedSelected}"/>
<Setter Target="PART_RemoveButton.Style" Value="{ThemeResource SubtleAccentButtonStyle}"/>
<Setter Target="PART_RootGrid.BackgroundSizing" Value="OuterBorderEdge"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="DisabledStates">
<VisualState x:Name="Enabled"/>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="PART_ContentPresenter.Opacity" Value="{ThemeResource ListViewItemDisabledThemeOpacity}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Default style for TokenizingTextBoxItem -->
<Style BasedOn="{StaticResource TokenizingTextBoxItemTokenStyle}" TargetType="shct:TokenizingTextBoxItem"/>
<Style x:Key="SubtleButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparent}"/>
<Setter Property="BackgroundSizing" Value="InnerBorderEdge"/>
<Setter Property="Foreground" Value="{ThemeResource TextFillColorPrimary}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="{StaticResource ButtonPadding}"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/>
<Setter Property="FontWeight" Value="Normal"/>
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/>
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}"/>
<Setter Property="FocusVisualMargin" Value="-3"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter
x:Name="ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
muxc:AnimatedIcon.State="Normal"
win:AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
CornerRadius="{TemplateBinding CornerRadius}">
<win:ContentPresenter.BackgroundTransition>
<win:BrushTransition Duration="0:0:0.083"/>
</win:ContentPresenter.BackgroundTransition>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleFillColorSecondary}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextFillColorPrimary}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(muxc:AnimatedIcon.State)" Value="PointerOver"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleFillColorTertiary}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextFillColorSecondary}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(muxc:AnimatedIcon.State)" Value="Pressed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ControlFillColorDisabled}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextFillColorDisabled}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<!-- DisabledVisual Should be handled by the control, not the animated icon. -->
<Setter Target="ContentPresenter.(muxc:AnimatedIcon.State)" Value="Normal"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SubtleAccentButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource SubtleFillColorTransparent}"/>
<Setter Property="BackgroundSizing" Value="InnerBorderEdge"/>
<Setter Property="Foreground" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="{StaticResource ButtonPadding}"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}"/>
<Setter Property="FontWeight" Value="Normal"/>
<Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}"/>
<Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}"/>
<Setter Property="FocusVisualMargin" Value="-3"/>
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<ContentPresenter
x:Name="ContentPresenter"
Padding="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
muxc:AnimatedIcon.State="Normal"
win:AutomationProperties.AccessibilityView="Raw"
Background="{TemplateBinding Background}"
BackgroundSizing="{TemplateBinding BackgroundSizing}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
CornerRadius="{TemplateBinding CornerRadius}">
<win:ContentPresenter.BackgroundTransition>
<win:BrushTransition Duration="0:0:0.083"/>
</win:ContentPresenter.BackgroundTransition>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleFillColorSecondary}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextOnAccentFillColorPrimaryBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(muxc:AnimatedIcon.State)" Value="PointerOver"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource SubtleFillColorTertiary}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TextOnAccentFillColorSecondaryBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(muxc:AnimatedIcon.State)" Value="Pressed"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ControlFillColorTransparentBrush}"/>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="Foreground">
<!-- Should be TextOnAccentFillColorDisabledBrush, but doesn't seem to work? This is the same visual effect -->
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource ControlFillColorDefaultBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<VisualState.Setters>
<Setter Target="ContentPresenter.(muxc:AnimatedIcon.State)" Value="Normal"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.TokenizingTextBox;
internal class TokenizingTextBoxStyleSelector : StyleSelector
{
public Style TokenStyle { get; set; } = default!;
public Style TextStyle { get; set; } = default!;
/// <inheritdoc/>
protected override Style SelectStyleCore(object item, DependencyObject container)
{
if (item is ITokenStringContainer)
{
return TextStyle;
}
return TokenStyle;
}
}

View File

@@ -2,7 +2,6 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Data;
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Control;
@@ -40,6 +39,6 @@ internal abstract class ValueConverter<TFrom, TTo> : IValueConverter
/// <returns>源</returns>
public virtual TFrom ConvertBack(TTo to)
{
throw HutaoException.NotSupported();
throw Must.NeverHappen();
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
/// <summary>
/// 可克隆
/// </summary>
/// <typeparam name="TSelf">自身类型</typeparam>
[HighQuality]
internal interface ICloneable<out TSelf>
{
/// <summary>
/// 克隆
/// </summary>
/// <returns>新的克隆</returns>
TSelf Clone();
}

View File

@@ -1,19 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.ViewModel.Guide;
using Snap.Hutao.Web.Request.Builder;
using Snap.Hutao.Web.Request.Builder.Abstraction;
using System.Collections.Concurrent;
using System.Collections.Frozen;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core.Caching;
@@ -29,22 +25,19 @@ namespace Snap.Hutao.Core.Caching;
internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
private const string CacheFailedDownloadTasksName = $"{nameof(ImageCache)}.FailedDownloadTasks";
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = FrozenDictionary.ToFrozenDictionary(
[
KeyValuePair.Create(0, TimeSpan.FromSeconds(4)),
KeyValuePair.Create(1, TimeSpan.FromSeconds(16)),
KeyValuePair.Create(2, TimeSpan.FromSeconds(64)),
]);
private readonly FrozenDictionary<int, TimeSpan> retryCountToDelay = new Dictionary<int, TimeSpan>()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
[2] = TimeSpan.FromSeconds(64),
}.ToFrozenDictionary();
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
private readonly IHttpClientFactory httpClientFactory;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<ImageCache> logger;
private readonly IMemoryCache memoryCache;
private string? baseFolder;
private string? cacheFolder;
@@ -111,12 +104,12 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
{
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
{
logger.LogColorizedInformation("Begin to download file from '{Uri}' to '{File}'", (uri, ConsoleColor.Cyan), (filePath, ConsoleColor.Cyan));
logger.LogDebug("Begin downloading image file from '{Uri}' to '{File}'", uri, filePath);
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
}
else if (concurrentTasks.TryGetValue(fileName, out Task? task))
{
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", (uri, ConsoleColor.Cyan));
logger.LogDebug("Waiting for a queued image download task to complete for '{Uri}'", uri);
await task.ConfigureAwait(false);
}
@@ -139,7 +132,10 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
private static string GetCacheFileName(Uri uri)
{
return Hash.SHA1HexString(uri.ToString());
string url = uri.ToString();
byte[] chars = Encoding.UTF8.GetBytes(url);
byte[] hash = SHA1.HashData(chars);
return System.Convert.ToHexString(hash);
}
private static bool IsFileInvalid(string file, bool treatNullFileAsInvalid = true)
@@ -176,76 +172,40 @@ internal sealed partial class ImageCache : IImageCache, IImageCacheFilePathOpera
HttpClient httpClient = httpClientFactory.CreateClient(nameof(ImageCache));
while (retryCount < 3)
{
HttpRequestMessageBuilder requestMessageBuilder = httpRequestMessageBuilderFactory
.Create()
.SetRequestUri(uri)
// These headers are only available for our own api
.SetStaticResourceControlHeadersIf(uri.Host.Contains("api.snapgenshin.com", StringComparison.OrdinalIgnoreCase))
.Get();
using (HttpRequestMessage requestMessage = requestMessageBuilder.HttpRequestMessage)
using (HttpResponseMessage message = await httpClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
using (HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
if (message.RequestMessage is { RequestUri: { } target } && target != uri)
{
if (responseMessage.RequestMessage is { RequestUri: { } target } && target != uri)
{
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
logger.LogDebug("The Request '{Source}' has been redirected to '{Target}'", uri, target);
}
if (responseMessage.IsSuccessStatusCode)
if (message.IsSuccessStatusCode)
{
using (Stream httpStream = await message.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
if (responseMessage.Content.Headers.ContentType?.MediaType is "application/json")
using (FileStream fileStream = File.Create(baseFile))
{
#if DEBUG
DebugTrack(uri);
#endif
string raw = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
logger.LogColorizedCritical("Failed to download '{Uri}' with unexpected body '{Raw}'", (uri, ConsoleColor.Red), (raw, ConsoleColor.DarkYellow));
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
}
}
using (Stream httpStream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false))
switch (message.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
using (FileStream fileStream = File.Create(baseFile))
{
await httpStream.CopyToAsync(fileStream).ConfigureAwait(false);
return;
}
retryCount++;
TimeSpan delay = message.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
}
}
switch (responseMessage.StatusCode)
{
case HttpStatusCode.TooManyRequests:
{
retryCount++;
TimeSpan delay = responseMessage.Headers.RetryAfter?.Delta ?? retryCountToDelay[retryCount];
logger.LogInformation("Retry download '{Uri}' after {Delay}.", uri, delay);
await Task.Delay(delay).ConfigureAwait(false);
break;
}
default:
#if DEBUG
DebugTrack(uri);
#endif
logger.LogColorizedCritical("Failed to download '{Uri}' with status code '{StatusCode}'", (uri, ConsoleColor.Red), (responseMessage.StatusCode, ConsoleColor.DarkYellow));
return;
}
default:
return;
}
}
}
}
}
#if DEBUG
internal partial class ImageCache
{
private void DebugTrack(Uri uri)
{
HashSet<string>? set = memoryCache.GetOrCreate(CacheFailedDownloadTasksName, entry => entry.Value ??= new HashSet<string>()) as HashSet<string>;
set?.Add(uri.ToString());
}
}
#endif
}

View File

@@ -1,32 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Collection;
internal sealed class TwoEnumerbleEnumerator<TFirst, TSecond> : IDisposable
{
private readonly IEnumerator<TFirst> firstEnumerator;
private readonly IEnumerator<TSecond> secondEnumerator;
public TwoEnumerbleEnumerator(IEnumerable<TFirst> firstEnumerable, IEnumerable<TSecond> secondEnumerable)
{
firstEnumerator = firstEnumerable.GetEnumerator();
secondEnumerator = secondEnumerable.GetEnumerator();
}
public (TFirst First, TSecond Second) Current { get => (firstEnumerator.Current, secondEnumerator.Current); }
public bool MoveNext(ref bool moveFirst, ref bool moveSecond)
{
moveFirst = moveFirst && firstEnumerator.MoveNext();
moveSecond = moveSecond && secondEnumerator.MoveNext();
return moveFirst || moveSecond;
}
public void Dispose()
{
firstEnumerator.Dispose();
secondEnumerator.Dispose();
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Security.Cryptography;
using System.Text;
namespace Snap.Hutao.Core;
/// <summary>
/// 支持Md5转换
/// </summary>
[HighQuality]
internal static class Convert
{
/// <summary>
/// 获取字符串的MD5计算结果
/// </summary>
/// <param name="source">源字符串</param>
/// <returns>计算的结果</returns>
public static string ToMd5HexString(string source)
{
byte[] hash = MD5.HashData(Encoding.UTF8.GetBytes(source));
return System.Convert.ToHexString(hash);
}
}

View File

@@ -13,6 +13,13 @@ namespace Snap.Hutao.Core.Database;
[HighQuality]
internal static class DbSetExtension
{
/// <summary>
/// 添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int AddAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -20,13 +27,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
/// <summary>
/// 异步添加并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Add(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
/// <summary>
/// 添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static int AddRangeAndSave<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
@@ -34,13 +55,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities, CancellationToken token = default)
/// <summary>
/// 异步添加列表并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entities">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> AddRangeAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, IEnumerable<TEntity> entities)
where TEntity : class
{
dbSet.AddRange(entities);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
/// <summary>
/// 移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int RemoveAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -48,13 +83,27 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
/// <summary>
/// 异步移除并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> RemoveAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Remove(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
/// <summary>
/// 更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static int UpdateAndSave<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
@@ -62,11 +111,18 @@ internal static class DbSetExtension
return dbSet.SaveChangesAndClearChangeTracker();
}
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity, CancellationToken token = default)
/// <summary>
/// 异步更新并保存
/// </summary>
/// <typeparam name="TEntity">实体类型</typeparam>
/// <param name="dbSet">数据库集</param>
/// <param name="entity">实体</param>
/// <returns>影响条数</returns>
public static ValueTask<int> UpdateAndSaveAsync<TEntity>(this DbSet<TEntity> dbSet, TEntity entity)
where TEntity : class
{
dbSet.Update(entity);
return dbSet.SaveChangesAndClearChangeTrackerAsync(token);
return dbSet.SaveChangesAndClearChangeTrackerAsync();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -80,11 +136,11 @@ internal static class DbSetExtension
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet, CancellationToken token = default)
private static async ValueTask<int> SaveChangesAndClearChangeTrackerAsync<TEntity>(this DbSet<TEntity> dbSet)
where TEntity : class
{
DbContext dbContext = dbSet.Context();
int count = await dbContext.SaveChangesAsync(token).ConfigureAwait(false);
int count = await dbContext.SaveChangesAsync().ConfigureAwait(false);
dbContext.ChangeTracker.Clear();
return count;
}

View File

@@ -38,7 +38,7 @@ internal sealed partial class ScopedDbCurrent<TEntity, TMessage>
return;
}
if (serviceProvider.IsDisposed())
if (serviceProvider.IsDisposedSlow())
{
return;
}
@@ -96,7 +96,7 @@ internal sealed partial class ScopedDbCurrent<TEntityOnly, TEntity, TMessage>
return;
}
if (serviceProvider.IsDisposed())
if (serviceProvider.IsDisposedSlow())
{
return;
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Abstraction;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 对象扩展
/// </summary>
[HighQuality]
internal static class CastServiceExtension
{
/// <summary>
/// <see langword="as"/> 的链式调用扩展
/// </summary>
/// <typeparam name="T">目标转换类型</typeparam>
/// <param name="service">对象</param>
/// <returns>转换类型后的对象</returns>
[Obsolete("Not useful anymore")]
public static T? As<T>(this ICastService service)
where T : class
{
return service as T;
}
}

Some files were not shown because too many files have changed in this diff Show More