Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a69960f
feat(debug-toast): 添加测试弹窗功能及相关本地化字符串
sysfox Aug 7, 2026
96e6798
feat(toast): 限制弹窗宽高,点击展示完整内容
sysfox Aug 7, 2026
b715dd0
feat(toast): 新弹窗叠置于旧弹窗上方
sysfox Aug 7, 2026
699c30d
feat(setup): 调试选项支持连续触发多条弹窗
sysfox Aug 7, 2026
783588a
fix(toast): 内容限 3 行省略,收窄最大宽度
sysfox Aug 7, 2026
e2fdd64
feat(toast): 弹窗按 Z 轴叠置,露出上沿可数数量
sysfox Aug 7, 2026
d25a1bf
feat(toast): 入场回弹落位与图标按压,叠置栈补位改为整体层动
sysfox Aug 7, 2026
0fd1193
feat(toast): 工具箱质感——楔形色标、下沉进度轨道、信息类型降噪
sysfox Aug 7, 2026
0a0fa31
fix(toast): 修复短文本弹窗初始过宽并逐步收窄的宽度回归
sysfox Aug 7, 2026
c7b021e
feat(toast): 文本上限提至 6 行,悬停暂停加载条,加强回弹动效
sysfox Aug 7, 2026
34031c9
feat(toast): 信息类弹窗用主题色,详情弹窗图标与类型联动
sysfox Aug 7, 2026
c4ebabc
fix(toast): 消失提速,隐藏过程中悬停不再卡半透明
sysfox Aug 8, 2026
59f22f9
feat(toast): 详情弹窗标题与图标同色,新增复制按钮
sysfox Aug 8, 2026
6e7204f
feat(toast): 叠置时旧弹窗淡化处理,最新全亮
sysfox Aug 8, 2026
2a6c8c7
feat(toast): 详情弹窗复制按钮移至底部左侧,文本限高改 2 行,放大关闭按钮
sysfox Aug 8, 2026
3118088
feat(toast): 渐隐旧弹窗加毛玻璃模糊,越旧越模糊越淡
sysfox Aug 8, 2026
3529af8
feat(toast): 详情弹窗复制按钮移回右侧按钮组
sysfox Aug 8, 2026
3594d85
fix(toast): 隐藏倒计时标志在取消路径下复位,避免 IsHiding 误判阻塞淡化/悬停
sysfox Aug 8, 2026
c1bd81c
refactor(toast): 移除模糊效果,简化淡化动画逻辑
sysfox Aug 9, 2026
b6686fc
feat(toast): 优化弹窗透明度管理,确保旧弹窗逐级淡化,提升视觉层次感
sysfox Aug 9, 2026
d2b244e
refactor(toast): 调整弹窗布局逻辑,优化弹窗间距和可见性管理
sysfox Aug 9, 2026
2ab35e1
revert(toast): 撤销 MyMsg 改动,已迁移至 feat/toast-detail-upstream
sysfox Aug 9, 2026
67174cf
feat(toast): 添加调试日志以跟踪弹窗显示和移除事件
sysfox Aug 11, 2026
2bef282
refactor(toast): 移除调试弹窗相关代码,简化界面和逻辑
sysfox Aug 12, 2026
91da4ac
Merge branch 'dev' into dev
sysfox Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions PCL.Core/App/Localization/Languages/en-US.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
<sys:String x:Key="Common.Action.Cancel">Cancel</sys:String>
<sys:String x:Key="Common.Action.Close">Close</sys:String>
<sys:String x:Key="Common.Action.Confirm">OK</sys:String>
<sys:String x:Key="Main.Toast.Detail.Title">Toast details</sys:String>
<sys:String x:Key="Main.Toast.Detail.ToolTip">Click to view full content</sys:String>
<sys:String x:Key="Common.Action.Continue">Continue</sys:String>
<sys:String x:Key="Common.Action.Copy">Copy</sys:String>
<sys:String x:Key="Common.Action.Decline">Decline</sys:String>
Expand Down
2 changes: 2 additions & 0 deletions PCL.Core/App/Localization/Languages/zh-CN.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
<sys:String x:Key="Common.Action.Cancel">取消</sys:String>
<sys:String x:Key="Common.Action.Close">关闭</sys:String>
<sys:String x:Key="Common.Action.Confirm">确定</sys:String>
<sys:String x:Key="Main.Toast.Detail.Title">提示详情</sys:String>
<sys:String x:Key="Main.Toast.Detail.ToolTip">点击查看完整内容</sys:String>
<sys:String x:Key="Common.Action.Continue">继续</sys:String>
<sys:String x:Key="Common.Action.Copy">复制</sys:String>
<sys:String x:Key="Common.Action.Decline">拒绝</sys:String>
Expand Down
23 changes: 14 additions & 9 deletions Plain Craft Launcher 2/Controls/MyToast.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
CornerRadius="8" BorderThickness="0"
UseLayoutRounding="True" SnapsToDevicePixels="True">
<Border.Effect>
<DropShadowEffect BlurRadius="16" Opacity="0.2" ShadowDepth="4" Direction="270" Color="#000000" />
<DropShadowEffect BlurRadius="12" Opacity="0.16" ShadowDepth="2" Direction="270" Color="#000000" />
</Border.Effect>
<Grid x:Name="RootGrid" SizeChanged="RootGrid_SizeChanged">
<Grid.RowDefinitions>
Expand All @@ -28,18 +28,23 @@

<TextBlock x:Name="TitleText" Grid.Column="1" VerticalAlignment="Center"
FontSize="13" FontWeight="Bold"
TextWrapping="Wrap" LineHeight="16" MaxHeight="32"
TextTrimming="CharacterEllipsis" />

<local:MyIconButton Grid.Column="2" x:Name="BtnClose" Width="20" Height="20"
<local:MyIconButton Grid.Column="2" x:Name="BtnClose" Width="26" Height="26"
Margin="8,0,0,0" SvgIcon="lucide/x" Theme="Custom" />
</Grid>

<Rectangle x:Name="ProgressBar" Height="3" Grid.Row="1"
HorizontalAlignment="Stretch" VerticalAlignment="Bottom"
RenderTransformOrigin="0,0.5">
<Rectangle.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1" />
</Rectangle.RenderTransform>
</Rectangle>
<!-- 下沉轨道 + 时间线填充:一条彩色时间线骑在暗槽上 -->
<Grid Grid.Row="1">
<Rectangle x:Name="ProgressTrack" Height="3" VerticalAlignment="Bottom" />
<Rectangle x:Name="ProgressBar" Height="3" VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
RenderTransformOrigin="0,0.5">
<Rectangle.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="1" />
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
</Grid>
</Border>
150 changes: 127 additions & 23 deletions Plain Craft Launcher 2/Controls/MyToast.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System.Windows.Media;
using System.Windows.Media.Animation;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.UI.Theme;
using PCL.Core.Utils;

namespace PCL;

Expand Down Expand Up @@ -40,6 +42,14 @@ public partial class MyToast
// 进度条状态
private double _progressStartWidth;
private double _progressTotalMs;
private bool _pausedByHover;
private double _hoverRemainingMs;

/// <summary>悬停暂停开始时刻(毫秒),配合 _hoverRemainingMs 判断暂停期间倒计时是否已走完。</summary>
private long _pauseStartedAtTick;

/// <summary>当前隐藏动画倒计时结束、实际开始滑出/淡出的起始 tick(0 表示无待滑出的隐藏)。</summary>
private long _hideStartsAtTick;

public MyToast()
{
Expand All @@ -49,6 +59,8 @@ public MyToast()
PreviewMouseMove += Toast_PreviewMouseMove;
PreviewMouseLeftButtonUp += Toast_PreviewMouseLeftButtonUp;
LostMouseCapture += Toast_LostMouseCapture;
Root.MouseEnter += Root_MouseEnter;
Root.MouseLeave += Root_MouseLeave;
Loaded += (_, _) =>
{
UpdateColors();
Expand All @@ -64,7 +76,9 @@ public MyToast()
ModAnimation.AniStop($"Toast Dismiss {Uuid}");
ModAnimation.AniStop($"Toast Emphasize {Uuid}");
ModAnimation.AniStop($"Toast Drag Return {Uuid}");
ModAnimation.AniStop($"Toast StackSettle {Uuid}");
ProgressBar.BeginAnimation(WidthProperty, null);
ResetHoverPause();
};
}

Expand All @@ -77,6 +91,13 @@ public string Context
set => TitleText.Text = value;
}

// 点击展开,展示完整内容
private void ShowDetail()
{
if (IsDismissing) return;
ModMain.MyMsgBox(Context, Lang.Text("Main.Toast.Detail.Title"), Lang.Text("Common.Action.Confirm"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add a copy path to toast details

When a long or truncated toast is clicked, this path opens the standard MyMsgBox with only an OK button. That dialog renders the caption in MyMsgText as a non-selectable TextBlock and has no clipboard action, so users still cannot copy the full error/warning text from the new detail view; include a copy button/action or selectable text for this detail dialog.

Useful? React with 👍 / 👎.

}

public string Icon { get; set; } = "lucide/info";

public HintType ToastType { get; set; } = HintType.Info;
Expand All @@ -85,14 +106,15 @@ public string Context

public bool IsDismissing { get; private set; }

private double _targetHeight;
/// <summary>弹窗完全展开后的目标高度,用于纵向排布的定位计算。</summary>
internal double _targetHeight;

public void Show()
{
if (Parent is not Panel)
return;
if (System.Windows.Application.Current.MainWindow is not null)
MaxWidth = System.Windows.Application.Current.MainWindow.ActualWidth * 0.9;
MaxWidth = Math.Min(System.Windows.Application.Current.MainWindow.ActualWidth * 0.9, 360d);
Margin = new Thickness(0, 0, 16, 4);
Opacity = 0;

Expand All @@ -103,12 +125,18 @@ public void Show()

RenderTransform = new TranslateTransform(60, 0);

// 图标“按入”微动效:0.8 → 1,略带过头回弹,呼应启动器按钮的按压语言
ToastIcon.RenderTransformOrigin = new Point(0.5, 0.5);
ToastIcon.RenderTransform = new ScaleTransform(0.8, 0.8);

ModAnimation.AniStop($"Toast Drag Return {Uuid}");
var enterAnimations = new List<ModAnimation.AniData>
{
ModAnimation.AaTranslateX(this, -60, 400, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaTranslateX(this, -60, 380, ease: new ModAnimation.AniEaseOutCar()),
ModAnimation.AaHeight(this, _targetHeight, 150, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaOpacity(this, 1, 100)
ModAnimation.AaOpacity(this, 1, 100),
ModAnimation.AaScaleTransform(ToastIcon, 0.2, 260, 120,
ease: new ModAnimation.AniEaseOutBack(ModAnimation.AniEasePower.Weak))
};
ModAnimation.AniStart(enterAnimations, $"Toast Show {Uuid}");

Expand All @@ -121,16 +149,19 @@ public void Emphasize()
ModAnimation.AniStop($"Toast Hide {Uuid}");
ModAnimation.AniStop($"Toast Emphasize {Uuid}");
ModAnimation.AniStop($"Toast Drag Return {Uuid}");
_hideStartsAtTick = 0; // 隐藏倒计时被取消,复位标志避免 IsHiding 误判
ProgressBar.BeginAnimation(WidthProperty, null);
ResetHoverPause();
if (RenderTransform is TranslateTransform tt) tt.X = 0;
Opacity = 1;
Height = _targetHeight;
ToastIcon.RenderTransform = new ScaleTransform(1, 1); // 若入场动效尚未结束,先复位图标缩放
ModAnimation.AniStart(new List<ModAnimation.AniData>
{
ModAnimation.AaTranslateX(this, -8, 70, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaTranslateX(this, 16, 70, after: true),
ModAnimation.AaTranslateX(this, -8, 60, after: true, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaCode(RestartHideAnimation, after: true),
ModAnimation.AaTranslateX(this, -14, 90, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaTranslateX(this, 18, 100, after: true, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaTranslateX(this, -4, 110, after: true, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaCode(RestartHideAnimation, after: true)
}, $"Toast Emphasize {Uuid}");
}

Expand All @@ -143,15 +174,19 @@ private void RestartHideAnimation()
private void StartHideAnimation(double delayMs)
{
var delay = (int)Math.Round(delayMs);
_hideStartsAtTick = TimeUtils.GetTimeTick() + delay;
ModAnimation.AniStart(new List<ModAnimation.AniData>
{
ModAnimation.AaTranslateX(this, 60, 200, delay, new ModAnimation.AniEaseInFluent()),
ModAnimation.AaOpacity(this, -1, 150, delay),
ModAnimation.AaTranslateX(this, 60, 150, delay, new ModAnimation.AniEaseInFluent()),
ModAnimation.AaOpacity(this, -1, 110, delay),
ModAnimation.AaHeight(this, -_targetHeight, 100, ease: new ModAnimation.AniEaseOutFluent(), after: true),
ModAnimation.AaCode(() =>
{
if (Parent is Panel p)
{
p.Children.Remove(this);
HintService.OnToastRemoved(this);
}
}, after: true)
}, $"Toast Hide {Uuid}");
}
Expand All @@ -167,15 +202,21 @@ public void Dismiss()
ModAnimation.AniStop($"Toast Hide {Uuid}");
ModAnimation.AniStop($"Toast Emphasize {Uuid}");
ModAnimation.AniStop($"Toast Drag Return {Uuid}");
ModAnimation.AniStop($"Toast StackSettle {Uuid}");
_hideStartsAtTick = 0; // 隐藏动画被取消(改为滑动关闭),复位标志避免 IsHiding 误判
ProgressBar.BeginAnimation(WidthProperty, null);
ResetHoverPause();
ModAnimation.AniStart(new List<ModAnimation.AniData>
{
ModAnimation.AaTranslateX(this, 60, 150, ease: new ModAnimation.AniEaseInFluent()),
ModAnimation.AaOpacity(this, -1, 100),
ModAnimation.AaCode(() =>
{
if (Parent is Panel p)
{
p.Children.Remove(this);
HintService.OnToastRemoved(this);
}
}, after: true)
}, $"Toast Dismiss {Uuid}");
}
Expand All @@ -202,25 +243,31 @@ private void RootGrid_SizeChanged(object sender, SizeChangedEventArgs e)

private void UpdateColors()
{
var baseHue = ToastType switch
{
HintType.Success => 145d,
HintType.Error => 355d,
HintType.Warning => 40d,
_ => 210d
};
var isInfo = ToastType == HintType.Info;
var res = System.Windows.Application.Current.Resources;
var accent = new ModBase.MyColor().FromHSL2(baseHue, 75, 60);
// 信息类型整体用主题色,其余类型用饱和状态色
var accentBrush = isInfo
? (Brush)res["ColorBrush2"]
: new SolidColorBrush(ToastType switch
{
HintType.Success => new ModBase.MyColor().FromHSL2(145d, 75d, 60d),
HintType.Error => new ModBase.MyColor().FromHSL2(355d, 75d, 60d),
HintType.Warning => new ModBase.MyColor().FromHSL2(40d, 75d, 60d),
_ => new ModBase.MyColor().FromHSL2(210d, 75d, 60d)
});
var bg = ThemeService.IsDarkMode
? new SolidColorBrush(LabColor.FromLch(0.35))
: (Brush)res["ColorBrushBackground"];
var text = (SolidColorBrush)res["ColorBrushGray1"];
var accentBrush = new SolidColorBrush(accent);
var track = new SolidColorBrush(ThemeService.IsDarkMode
? Color.FromArgb(70, 255, 255, 255) // 暗色卡片上略亮的内嵌暗槽
: Color.FromArgb(60, 0, 0, 0)); // 亮色卡片上略暗的内嵌暗槽

Root.Background = bg;
Root.BorderBrush = bg;
// 卡片背景移到 RootGrid:模糊 RootGrid 时背景一起模糊,并被 8px 圆角裁剪;阴影仍在 Root 上不受影响
RootGrid.Background = bg;
TitleText.Foreground = text;
ProgressBar.Fill = accentBrush;
ProgressTrack.Fill = track;
ProgressBar.Fill = accentBrush; // Info 为主题色,其余类型为状态色
BtnClose.Foreground = text;
ToastIcon.Icon = Icon;
ToastIcon.IconBrush = accentBrush;
Expand Down Expand Up @@ -284,6 +331,7 @@ private void Toast_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs
if (_dragPending && !_isDragging)
{
_dragPending = false;
ShowDetail();
return;
}

Expand Down Expand Up @@ -325,6 +373,7 @@ private void BeginDrag(double initialDelta)
ModAnimation.AniStop($"Toast Hide {Uuid}");
ModAnimation.AniStop($"Toast Emphasize {Uuid}");
ModAnimation.AniStop($"Toast Drag Return {Uuid}");
_hideStartsAtTick = 0; // 拖拽取消隐藏倒计时,复位标志避免 IsHiding 误判

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear hover pause state when starting drag

When a toast is already hover-paused and the user drags it far enough to enter BeginDrag but releases without dismissing, this path cancels the hide timer and ReturnFromDrag() starts a fresh timer, but _pausedByHover remains true with the old _hoverRemainingMs. The next Root_MouseLeave then treats the stale hover pause as active and replaces the resumed timer with the old remaining time, or hides immediately if the original pause interval elapsed; clear the hover pause state here when drag takes over the progress/timer.

Useful? React with 👍 / 👎.


PauseProgress();

Expand Down Expand Up @@ -364,7 +413,11 @@ private void ReturnFromDrag()
{
ModAnimation.AaTranslateX(this, -currentX, ReturnAnimationMs, ease: new ModAnimation.AniEaseOutFluent()),
ModAnimation.AaOpacity(this, 1d - currentOpacity, ReturnAnimationMs),
ModAnimation.AaCode(() => StartHideAnimation(GetProgressRemainingMs()), after: true)
ModAnimation.AaCode(() =>
{
HintService.RearrangeToasts(); // 复位后重排,让位/补位的旧弹窗回到正确位置
StartHideAnimation(GetProgressRemainingMs());
}, after: true)
}, $"Toast Drag Return {Uuid}");
}

Expand Down Expand Up @@ -427,5 +480,56 @@ private double GetProgressRemainingMs()
return _progressTotalMs * (currentWidth / _progressStartWidth);
}

private void Root_MouseEnter(object sender, MouseEventArgs e)
{
if (_isDragging || _pausedByHover || IsDismissing)
return;
// 隐藏动画已进入滑出/淡出阶段:不悬停暂停,避免停掉半途的隐藏导致卡半透明、永不消失
if (_hideStartsAtTick > 0 && TimeUtils.GetTimeTick() >= _hideStartsAtTick)
return;
// 悬停暂停:停住隐藏倒计时并冻结进度条,移出后按剩余时间恢复
ModAnimation.AniStop($"Toast Hide {Uuid}");
var currentWidth = ProgressBar.Width;
ProgressBar.BeginAnimation(WidthProperty, null);
ProgressBar.Width = currentWidth;
_hoverRemainingMs = GetProgressRemainingMs();
_pauseStartedAtTick = TimeUtils.GetTimeTick();
_hideStartsAtTick = 0; // 悬停暂停即取消隐藏倒计时,结束判定改由 _pauseStartedAtTick + 剩余时长得出
_pausedByHover = true;
ProgressBar.Opacity = 0.4;
}

private void Root_MouseLeave(object sender, MouseEventArgs e)
{
if (_isDragging)
return; // 拖拽接管进度条,悬停恢复交由 ReturnFromDrag 处理
if (!_pausedByHover)
return;
_pausedByHover = false;
ProgressBar.Opacity = 1;
if (_hoverRemainingMs <= 0 || ProgressBar.Width <= 0)
return; // 已结束或未启动,无需恢复动画
// 异常路径:暂停期间倒计时已走完,不再恢复倒计时,直接滑出消失
if (_pauseStartedAtTick > 0 && TimeUtils.GetTimeTick() - _pauseStartedAtTick >= _hoverRemainingMs)
{
StartHideAnimation(0);
return;
Comment on lines +513 to +516

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resume the paused timeout after hover

If a user keeps the pointer over a toast longer than _hoverRemainingMs, this branch hides it immediately on MouseLeave, even though MouseEnter stopped the hide animation and froze the progress bar at the saved remaining width. That makes hover fail to actually pause long enough for users to read a toast—the toast vanishes as soon as they move away instead of resuming the saved remaining time shown by the bar.

Useful? React with 👍 / 👎.

}
StartHideAnimation(_hoverRemainingMs);
var currentWidth = ProgressBar.Width;
var anim = new DoubleAnimation(currentWidth, 0d, TimeSpan.FromMilliseconds(_hoverRemainingMs));
ProgressBar.BeginAnimation(WidthProperty, anim);
}

private void ResetHoverPause()
{
if (!_pausedByHover)
return;
_pausedByHover = false;
_hoverRemainingMs = 0;
_pauseStartedAtTick = 0;
ProgressBar.Opacity = 1;
}

#endregion
}
8 changes: 4 additions & 4 deletions Plain Craft Launcher 2/FormMain.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -219,10 +219,10 @@
UseLayoutRounding="False" SnapsToDevicePixels="False" />
</Border>
</Grid>
<StackPanel x:Name="PanHint" UseLayoutRounding="True"
SnapsToDevicePixels="True" HorizontalAlignment="Left" VerticalAlignment="Bottom"
Margin="0,0,0,20"
Grid.Row="0" Grid.RowSpan="2" Panel.ZIndex="1" />
<Grid x:Name="PanHint" UseLayoutRounding="True"
SnapsToDevicePixels="True" HorizontalAlignment="Left" VerticalAlignment="Stretch"
Margin="0,0,0,20"
Grid.Row="0" Grid.RowSpan="2" Panel.ZIndex="1" />
<StackPanel x:Name="PanExtraButtons" HorizontalAlignment="Right" VerticalAlignment="Bottom" Grid.Row="1" Margin="15">
<local:MyExtraButton x:Name="BtnExtraUpdateRestart" HorizontalAlignment="Right"
Click="BtnExtraUpdateRestart_Click"
Expand Down
Loading