From 20f3df1c28168fba897a4c319e66156a04fd6b0f Mon Sep 17 00:00:00 2001 From: Zero <1270128439@qq.com> Date: Thu, 27 Aug 2026 00:07:02 +0800 Subject: [PATCH 1/4] Rework the update confirmation dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gives the dialog a soft warm mesh gradient behind its header and rebuilds its layout around it. The gradient is three stacked RadialGradientBrush layers, because one brush paints one blob and Background takes exactly one. Amber and rose are neighbours on the wheel so they read as one colour family, with lilac held to a whisper as the single accent; an earlier pass at violet/blue/mint in equal weight looked like a rainbow stripe. Warm was chosen because the primary button follows the user's Windows accent, which is blue for most people. The dark palette is not the light one at lower alpha — a saturated warm over a dark surface goes to brown mud — so it uses the same hues already lifted toward pale. The template's own Title and command buttons are given up to make room: Title renders inside the padded content grid and would sit above the wash, and the command area's two equal star columns stretch each button across half the dialog. Leaving all three button texts empty drives the NoneVisible state that collapses CommandSpace, and the buttons are laid out in the content instead. Enter is wired back up by focusing the accent button on Opened, with Pointer focus so it does not draw a second heavy border. Also reserves a scrollbar gutter in the notes list — WinUI scrollbars are overlays and were covering the last dozen pixels of every line — and gives the notes an explicit line height, since the default leading is tight for CJK once a line wraps. The plain notice dialogs now wrap their message in a width-capped TextBlock rather than handing ContentDialog a raw string, which let every short notice measure out to the 548px maximum. Co-Authored-By: Claude Opus 5 --- Services/DialogService.cs | 237 ++++++++++++++++++++++++++++++++++---- 1 file changed, 213 insertions(+), 24 deletions(-) diff --git a/Services/DialogService.cs b/Services/DialogService.cs index a62b060..78ca7a1 100644 --- a/Services/DialogService.cs +++ b/Services/DialogService.cs @@ -4,7 +4,10 @@ using System.ComponentModel; using System.Collections.Generic; using Windows.ApplicationModel.DataTransfer; +using Windows.Foundation; +using Windows.UI; using Microsoft.UI.Xaml.Automation; +using Microsoft.UI.Xaml.Media; using XrayUI.Controls; using XrayUI.Helpers; using XrayUI.Models; @@ -19,6 +22,13 @@ public class DialogService : IDialogService { private readonly Func _xamlRootFactory; + /// + /// Caps the wrapped message text of the plain notice dialogs. Handing ContentDialog a raw + /// string instead lets it measure out to its 548px maximum even for a one-liner, so every + /// short notice opens as wide as the longest one. + /// + private const double MessageMaxWidth = 280; + public DialogService(Func xamlRootFactory) { _xamlRootFactory = xamlRootFactory; @@ -661,7 +671,7 @@ public async Task ShowConfirmationAsync(string title, string message, stri { Text = message, TextWrapping = TextWrapping.Wrap, - MaxWidth = 280 + MaxWidth = MessageMaxWidth, }; var dialog = CreateDialog(); @@ -728,7 +738,12 @@ public async Task ShowErrorAsync(string title, string message, XamlRoot? xamlRoo { var dialog = CreateDialog(xamlRoot); dialog.Title = title; - dialog.Content = message; + dialog.Content = new TextBlock + { + Text = message, + TextWrapping = TextWrapping.Wrap, + MaxWidth = MessageMaxWidth, + }; dialog.CloseButtonText = L.Dialog_OK; await dialog.ShowAsync(); } @@ -1005,26 +1020,74 @@ public async Task ShowShareLinkDialogAsync(string serverName, string link) // ── App update confirm ──────────────────────────────────────────────── + /// + /// Content width of the update dialog, inside ContentDialog's own padding. Sized so a + /// typical changelog line still fits on one row; longer ones wrap onto the hanging + /// indent that sets up, which is why this can be tightened + /// well below what the longest line would need. + /// + private const double UpdateContentWidth = 352; + + /// Right gutter kept clear inside the notes list for the overlay scrollbar. + private const double ScrollBarGutter = 14; + + /// + /// How far down the dialog the gradient reaches, in DIPs. Pinned in absolute units + /// rather than a fraction of the surface so a three-note release and a ten-note one + /// get the same header wash instead of the gradient stretching with the note count. + /// Deliberately deeper than the coloured area looks: low amplitude over a large area + /// reads as atmosphere, while the same colour packed into a shallower band reads as a + /// stripe across the top. + /// + private const double UpdateGradientDepth = 260; + public async Task ShowUpdateConfirmDialogAsync( Version newVersion, IReadOnlyList notes) { var dialog = CreateDialog(); - dialog.Title = Loc.Format("Update_ConfirmTitle", newVersion); - dialog.PrimaryButtonText = L.Update_ConfirmNow; - dialog.CloseButtonText = L.Update_ConfirmLater; - dialog.DefaultButton = ContentDialogButton.Primary; + // Neither dialog.Title nor the Primary/Close button text is set, and both omissions + // are load-bearing. The template puts Title inside the padded content Grid, so a + // real Title would sit above the gradient instead of on it; and leaving all three + // button texts empty drives the template's NoneVisible state, which collapses + // CommandSpace entirely. That is the only way to escape the command area's two + // equal star columns, which stretch each button across half the dialog — the + // buttons below are ordinary right-aligned ones laid out in the content instead. + var pad = ContentDialogPadding(); + + // Grid root, not StackPanel: a StackPanel root breaks the measure chain and + // ContentDialog clips tall content instead of letting the notes list scroll. + // Fixed width keeps the dialog compact — without it the longest note line + // stretches it toward ContentDialog's max width. + var root = new Grid { Width = UpdateContentWidth }; + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // heading + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // notes header + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // notes list + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // buttons + + // Added first so everything else draws over it, and spanning every row so its own + // height decides how far the colour reaches instead of the heading row clipping it. + // The negative margin cancels the dialog's padding, so it bleeds to the left, right + // and top edges — the rounded DialogSpace clips its corners. + var wash = BuildGradientWash(); + Grid.SetRowSpan(wash, root.RowDefinitions.Count); + wash.Margin = new Thickness(-pad.Left, -pad.Top, -pad.Right, 0); + root.Children.Add(wash); + + var heading = new TextBlock + { + // 20 is what ContentDialog's own Title uses; going larger made the dialog read + // top-heavy and, with it, bigger than it is. + Text = Loc.Format("Update_ConfirmTitle", newVersion), + FontSize = 20, + FontWeight = Microsoft.UI.Text.FontWeights.SemiBold, + TextWrapping = TextWrapping.Wrap, + Margin = new Thickness(0, 2, 0, 18), + }; + Grid.SetRow(heading, 0); + root.Children.Add(heading); - // No notes → no Content at all: the dialog stays a compact title + buttons. if (notes.Count > 0) { - // Grid root, not StackPanel: a StackPanel root breaks the measure chain and - // ContentDialog clips tall content instead of letting the notes list scroll. - // Fixed width keeps the dialog compact — without it the longest note line - // stretches it toward ContentDialog's max width. - var root = new Grid { Width = 380 }; - root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // notes header - root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // notes list - // Opacity instead of TextFillColorSecondaryBrush: Application.Current.Resources // resolves theme brushes against the app-level theme (never set here), which goes // stale under the Personalize theme override — see Views/LogWindow.xaml. @@ -1035,30 +1098,152 @@ public async Task ShowUpdateConfirmDialogAsync( Opacity = 0.65, Margin = new Thickness(0, 0, 0, 6), }; - Grid.SetRow(notesHeader, 0); + Grid.SetRow(notesHeader, 1); root.Children.Add(notesHeader); - var list = new StackPanel { Spacing = 4 }; + var list = new StackPanel { Spacing = 6 }; foreach (var line in notes) list.Children.Add(BuildNoteLine(line)); var scroller = new ScrollViewer { Content = list, - MaxHeight = 220, + MaxHeight = 240, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollMode = ScrollMode.Disabled, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, + // WinUI scrollbars are overlays: they paint on top of the content rather + // than taking layout space, so without this gutter the expanded mouse + // scrollbar covers the last dozen pixels of every note line. Whether it + // overlays at all depends on the "always show scrollbars" accessibility + // setting, so reserving the space is the only way to get the same result + // under both. The gutter is unconditional because whether the list will + // overflow is not known until after layout. + Padding = new Thickness(0, 0, ScrollBarGutter, 0), }; - Grid.SetRow(scroller, 1); + Grid.SetRow(scroller, 2); root.Children.Add(scroller); - - dialog.Content = root; } - return await dialog.ShowAsync() == ContentDialogResult.Primary; + var confirmed = false; + + var updateNow = new Button + { + Content = L.Update_ConfirmNow, + MinWidth = 104, + Style = Application.Current.Resources["AccentButtonStyle"] as Style, + }; + updateNow.Click += (_, _) => { confirmed = true; dialog.Hide(); }; + + var later = new Button { Content = L.Update_ConfirmLater, MinWidth = 88 }; + later.Click += (_, _) => dialog.Hide(); + + var buttons = new StackPanel + { + Orientation = Orientation.Horizontal, + HorizontalAlignment = HorizontalAlignment.Right, + Spacing = 8, + Margin = new Thickness(0, 20, 0, 0), + Children = { updateNow, later }, + }; + Grid.SetRow(buttons, 3); + root.Children.Add(buttons); + + // ContentDialog.DefaultButton only drives the template's own buttons, so Enter has + // nothing to activate here without this. Escape still dismisses the dialog on its + // own and leaves confirmed false, which is the right answer for a cancel. + // Pointer rather than Programmatic focus: both make Enter work, but Programmatic + // draws the keyboard focus ring, which reads as a heavy double border around the + // accent button. Tabbing brings the ring back, as it should. + dialog.Opened += (_, _) => updateNow.Focus(FocusState.Pointer); + + dialog.Content = root; + await dialog.ShowAsync(); + return confirmed; + } + + /// + /// The soft mesh gradient behind the whole dialog. Three stacked + /// layers are what make it read as a mesh — one brush + /// can only paint one blob, and Background takes exactly one. Every layer is centred on + /// the top edge and falls off to zero alpha within , + /// so the plain dialog background takes over well above the buttons with no seam. + /// + private static Grid BuildGradientWash() + { + var wash = new Grid { VerticalAlignment = VerticalAlignment.Top, Height = UpdateGradientDepth }; + var width = UpdateContentWidth + ContentDialogPadding().Left * 2; + + // Amber and rose are neighbours on the wheel, so together they read as one colour + // family rather than as competing hues; the lilac is held down to a whisper as the + // single accent. An earlier pass used violet, blue and mint at equal weight and it + // looked like a rainbow stripe — hue count, not gradient quality, was the problem. + // Warm was picked over cool on purpose: the primary button follows the user's + // Windows accent colour, which is blue for most people, and a warm field is what + // keeps that button reading as the one thing to click. + // Centres are staggered rather than all sitting on the top edge, which is what + // leaves a pale valley in the middle instead of one continuous band of colour. + // + // Dark is not the light set at lower alpha: a saturated warm composited over a dark + // grey surface goes to brown mud (verified by rendering it). The dark hues are the + // same colours already lifted toward pale, which keeps them reading as warm light. + var dark = ThemeHelper.ActualTheme == ElementTheme.Dark; + var (amber, amberAlpha) = dark ? (0xF7D9A6u, 0.26) : (0xF0B860u, 0.34); + var (rose, roseAlpha) = dark ? (0xF5C0B8u, 0.24) : (0xEE9B92u, 0.32); + var (lilac, lilacAlpha) = dark ? (0xD8CBEEu, 0.08) : (0xC0A8E0u, 0.10); + + wash.Children.Add(GradientBlob(amber, amberAlpha, + centerX: 0.08 * width, centerY: -40, radiusX: 300, radiusY: 260)); + wash.Children.Add(GradientBlob(rose, roseAlpha, + centerX: 1.02 * width, centerY: 0, radiusX: 290, radiusY: 250)); + wash.Children.Add(GradientBlob(lilac, lilacAlpha, + centerX: 1.00 * width, centerY: 170, radiusX: 240, radiusY: 210)); + return wash; + } + + /// + /// One colour blob of the mesh: an empty Border stretched over the wash, painted with a + /// radial gradient that falls off to the same colour at zero alpha (fading to a named + /// transparent would drift through grey on the way out). Absolute mapping mode, so the + /// blob keeps its shape no matter how tall the dialog grows. + /// + private static Border GradientBlob( + uint rgb, double alpha, + double centerX, double centerY, double radiusX, double radiusY) + { + byte r = (byte)(rgb >> 16), g = (byte)(rgb >> 8), b = (byte)rgb; + return new Border + { + Background = new RadialGradientBrush + { + MappingMode = BrushMappingMode.Absolute, + Center = new Point(centerX, centerY), + GradientOrigin = new Point(centerX, centerY), + RadiusX = radiusX, + RadiusY = radiusY, + GradientStops = + { + // Slightly faster than linear, so the colour stays gathered around its + // centre and releases cleanly instead of holding a flat plateau. + new GradientStop { Color = Color.FromArgb((byte)(alpha * 255), r, g, b), Offset = 0 }, + new GradientStop { Color = Color.FromArgb((byte)(alpha * 0.42 * 255), r, g, b), Offset = 0.5 }, + new GradientStop { Color = Color.FromArgb(0, r, g, b), Offset = 1 }, + }, + }, + }; } + /// + /// ContentDialog's content padding, read from the theme resource so the banner's bleed + /// margin follows if WinUI retunes it. A Thickness is theme-invariant, so this app-level + /// lookup is safe in a way the brush lookups called out above are not. + /// + private static Thickness ContentDialogPadding() => + Application.Current.Resources.TryGetValue("ContentDialogPadding", out var value) + && value is Thickness padding + ? padding + : new Thickness(24); + /// /// One bullet as a two-column Grid rather than a "• "-prefixed string, so wrapped /// lines keep a hanging indent instead of running back under the bullet. @@ -1072,7 +1257,8 @@ private static Grid BuildNoteLine(string text) var bullet = new TextBlock { Text = "•", - FontSize = 13, + FontSize = 14, + LineHeight = 20, Opacity = 0.65, VerticalAlignment = VerticalAlignment.Top, }; @@ -1080,7 +1266,10 @@ private static Grid BuildNoteLine(string text) var body = new TextBlock { Text = text, - FontSize = 13, + FontSize = 14, + // Default leading is tight for CJK once a line wraps; 20 keeps a wrapped note + // readable without spacing the list out. + LineHeight = 20, TextWrapping = TextWrapping.Wrap, }; Grid.SetColumn(body, 1); From ddae0896bf054e4a0b189accaeac0246250d0366 Mon Sep 17 00:00:00 2001 From: Zero <1270128439@qq.com> Date: Thu, 27 Aug 2026 09:12:01 +0800 Subject: [PATCH 2/4] Give the update dialog back its accessible name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dropping dialog.Title to get the heading onto the gradient also dropped the dialog's accessible name — ContentDialog's automation peer takes the name from Title, and the heading TextBlock that replaced it is content, so it names nothing. Narrator announced an unnamed dialog. Sets AutomationProperties.Name on the dialog from the same formatted string the heading shows. Co-Authored-By: Claude Opus 5 --- Services/DialogService.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Services/DialogService.cs b/Services/DialogService.cs index 78ca7a1..632d273 100644 --- a/Services/DialogService.cs +++ b/Services/DialogService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using System.ComponentModel; @@ -1052,6 +1052,13 @@ public async Task ShowUpdateConfirmDialogAsync( // CommandSpace entirely. That is the only way to escape the command area's two // equal star columns, which stretch each button across half the dialog — the // buttons below are ordinary right-aligned ones laid out in the content instead. + // + // Dropping Title costs the dialog its accessible name: ContentDialog's automation + // peer takes the name from Title, and the heading TextBlock below is content, so it + // names nothing. Narrator would announce an unnamed dialog. Set it explicitly. + var title = Loc.Format("Update_ConfirmTitle", newVersion); + AutomationProperties.SetName(dialog, title); + var pad = ContentDialogPadding(); // Grid root, not StackPanel: a StackPanel root breaks the measure chain and @@ -1077,7 +1084,7 @@ public async Task ShowUpdateConfirmDialogAsync( { // 20 is what ContentDialog's own Title uses; going larger made the dialog read // top-heavy and, with it, bigger than it is. - Text = Loc.Format("Update_ConfirmTitle", newVersion), + Text = title, FontSize = 20, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold, TextWrapping = TextWrapping.Wrap, From c673d3783f5edf710d712c8b47e334bd869ab60b Mon Sep 17 00:00:00 2001 From: Zero <1270128439@qq.com> Date: Thu, 27 Aug 2026 12:38:40 +0800 Subject: [PATCH 3/4] Tighten the update dialog's changelog layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reserves a scrollbar gutter in the notes list — WinUI scrollbars are overlays, so the expanded mouse scrollbar was covering the last dozen pixels of every note line — and gives the notes an explicit line height, since the default leading is tight for CJK once a line wraps. Font goes 13 to 14, with the content width, list spacing and scroll height retuned around it. Drops the gradient wash and hand-laid-out buttons this branch added earlier. It was decoration on a dialog that shows for a few seconds a few times a year, and giving up the template's Title and command area had already cost the dialog its accessible name once. Standard ContentDialog chrome is back, so Enter, Escape, the focus ring and the automation name are the template's job again. Co-Authored-By: Claude Opus 5 --- Services/DialogService.cs | 211 +++++--------------------------------- 1 file changed, 26 insertions(+), 185 deletions(-) diff --git a/Services/DialogService.cs b/Services/DialogService.cs index 632d273..401ac2e 100644 --- a/Services/DialogService.cs +++ b/Services/DialogService.cs @@ -1,13 +1,10 @@ -using System; +using System; using System.Threading; using System.Threading.Tasks; using System.ComponentModel; using System.Collections.Generic; using Windows.ApplicationModel.DataTransfer; -using Windows.Foundation; -using Windows.UI; using Microsoft.UI.Xaml.Automation; -using Microsoft.UI.Xaml.Media; using XrayUI.Controls; using XrayUI.Helpers; using XrayUI.Models; @@ -1021,80 +1018,38 @@ public async Task ShowShareLinkDialogAsync(string serverName, string link) // ── App update confirm ──────────────────────────────────────────────── /// - /// Content width of the update dialog, inside ContentDialog's own padding. Sized so a - /// typical changelog line still fits on one row; longer ones wrap onto the hanging - /// indent that sets up, which is why this can be tightened - /// well below what the longest line would need. + /// Content width of the update dialog's notes list, inside ContentDialog's own padding. + /// Sized so a typical changelog line still fits on one row; longer ones wrap onto the + /// hanging indent that sets up, which is why it can stay + /// well below what the longest line would need. It also has to cover the bullet column + /// and , neither of which holds text, so trimming it back + /// toward the bare text width costs a good dozen characters a line. /// - private const double UpdateContentWidth = 352; + private const double UpdateContentWidth = 392; /// Right gutter kept clear inside the notes list for the overlay scrollbar. private const double ScrollBarGutter = 14; - /// - /// How far down the dialog the gradient reaches, in DIPs. Pinned in absolute units - /// rather than a fraction of the surface so a three-note release and a ten-note one - /// get the same header wash instead of the gradient stretching with the note count. - /// Deliberately deeper than the coloured area looks: low amplitude over a large area - /// reads as atmosphere, while the same colour packed into a shallower band reads as a - /// stripe across the top. - /// - private const double UpdateGradientDepth = 260; - public async Task ShowUpdateConfirmDialogAsync( Version newVersion, IReadOnlyList notes) { var dialog = CreateDialog(); - // Neither dialog.Title nor the Primary/Close button text is set, and both omissions - // are load-bearing. The template puts Title inside the padded content Grid, so a - // real Title would sit above the gradient instead of on it; and leaving all three - // button texts empty drives the template's NoneVisible state, which collapses - // CommandSpace entirely. That is the only way to escape the command area's two - // equal star columns, which stretch each button across half the dialog — the - // buttons below are ordinary right-aligned ones laid out in the content instead. - // - // Dropping Title costs the dialog its accessible name: ContentDialog's automation - // peer takes the name from Title, and the heading TextBlock below is content, so it - // names nothing. Narrator would announce an unnamed dialog. Set it explicitly. - var title = Loc.Format("Update_ConfirmTitle", newVersion); - AutomationProperties.SetName(dialog, title); - - var pad = ContentDialogPadding(); - - // Grid root, not StackPanel: a StackPanel root breaks the measure chain and - // ContentDialog clips tall content instead of letting the notes list scroll. - // Fixed width keeps the dialog compact — without it the longest note line - // stretches it toward ContentDialog's max width. - var root = new Grid { Width = UpdateContentWidth }; - root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // heading - root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // notes header - root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // notes list - root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // buttons - - // Added first so everything else draws over it, and spanning every row so its own - // height decides how far the colour reaches instead of the heading row clipping it. - // The negative margin cancels the dialog's padding, so it bleeds to the left, right - // and top edges — the rounded DialogSpace clips its corners. - var wash = BuildGradientWash(); - Grid.SetRowSpan(wash, root.RowDefinitions.Count); - wash.Margin = new Thickness(-pad.Left, -pad.Top, -pad.Right, 0); - root.Children.Add(wash); - - var heading = new TextBlock - { - // 20 is what ContentDialog's own Title uses; going larger made the dialog read - // top-heavy and, with it, bigger than it is. - Text = title, - FontSize = 20, - FontWeight = Microsoft.UI.Text.FontWeights.SemiBold, - TextWrapping = TextWrapping.Wrap, - Margin = new Thickness(0, 2, 0, 18), - }; - Grid.SetRow(heading, 0); - root.Children.Add(heading); + dialog.Title = Loc.Format("Update_ConfirmTitle", newVersion); + dialog.PrimaryButtonText = L.Update_ConfirmNow; + dialog.CloseButtonText = L.Update_ConfirmLater; + dialog.DefaultButton = ContentDialogButton.Primary; + // No notes → no Content at all: the dialog stays a compact title + buttons. if (notes.Count > 0) { + // Grid root, not StackPanel: a StackPanel root breaks the measure chain and + // ContentDialog clips tall content instead of letting the notes list scroll. + // Fixed width keeps the dialog compact — without it the longest note line + // stretches it toward ContentDialog's max width. + var root = new Grid { Width = UpdateContentWidth }; + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // notes header + root.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // notes list + // Opacity instead of TextFillColorSecondaryBrush: Application.Current.Resources // resolves theme brushes against the app-level theme (never set here), which goes // stale under the Personalize theme override — see Views/LogWindow.xaml. @@ -1105,7 +1060,7 @@ public async Task ShowUpdateConfirmDialogAsync( Opacity = 0.65, Margin = new Thickness(0, 0, 0, 6), }; - Grid.SetRow(notesHeader, 1); + Grid.SetRow(notesHeader, 0); root.Children.Add(notesHeader); var list = new StackPanel { Spacing = 6 }; @@ -1128,129 +1083,15 @@ public async Task ShowUpdateConfirmDialogAsync( // overflow is not known until after layout. Padding = new Thickness(0, 0, ScrollBarGutter, 0), }; - Grid.SetRow(scroller, 2); + Grid.SetRow(scroller, 1); root.Children.Add(scroller); - } - var confirmed = false; - - var updateNow = new Button - { - Content = L.Update_ConfirmNow, - MinWidth = 104, - Style = Application.Current.Resources["AccentButtonStyle"] as Style, - }; - updateNow.Click += (_, _) => { confirmed = true; dialog.Hide(); }; - - var later = new Button { Content = L.Update_ConfirmLater, MinWidth = 88 }; - later.Click += (_, _) => dialog.Hide(); - - var buttons = new StackPanel - { - Orientation = Orientation.Horizontal, - HorizontalAlignment = HorizontalAlignment.Right, - Spacing = 8, - Margin = new Thickness(0, 20, 0, 0), - Children = { updateNow, later }, - }; - Grid.SetRow(buttons, 3); - root.Children.Add(buttons); - - // ContentDialog.DefaultButton only drives the template's own buttons, so Enter has - // nothing to activate here without this. Escape still dismisses the dialog on its - // own and leaves confirmed false, which is the right answer for a cancel. - // Pointer rather than Programmatic focus: both make Enter work, but Programmatic - // draws the keyboard focus ring, which reads as a heavy double border around the - // accent button. Tabbing brings the ring back, as it should. - dialog.Opened += (_, _) => updateNow.Focus(FocusState.Pointer); - - dialog.Content = root; - await dialog.ShowAsync(); - return confirmed; - } + dialog.Content = root; + } - /// - /// The soft mesh gradient behind the whole dialog. Three stacked - /// layers are what make it read as a mesh — one brush - /// can only paint one blob, and Background takes exactly one. Every layer is centred on - /// the top edge and falls off to zero alpha within , - /// so the plain dialog background takes over well above the buttons with no seam. - /// - private static Grid BuildGradientWash() - { - var wash = new Grid { VerticalAlignment = VerticalAlignment.Top, Height = UpdateGradientDepth }; - var width = UpdateContentWidth + ContentDialogPadding().Left * 2; - - // Amber and rose are neighbours on the wheel, so together they read as one colour - // family rather than as competing hues; the lilac is held down to a whisper as the - // single accent. An earlier pass used violet, blue and mint at equal weight and it - // looked like a rainbow stripe — hue count, not gradient quality, was the problem. - // Warm was picked over cool on purpose: the primary button follows the user's - // Windows accent colour, which is blue for most people, and a warm field is what - // keeps that button reading as the one thing to click. - // Centres are staggered rather than all sitting on the top edge, which is what - // leaves a pale valley in the middle instead of one continuous band of colour. - // - // Dark is not the light set at lower alpha: a saturated warm composited over a dark - // grey surface goes to brown mud (verified by rendering it). The dark hues are the - // same colours already lifted toward pale, which keeps them reading as warm light. - var dark = ThemeHelper.ActualTheme == ElementTheme.Dark; - var (amber, amberAlpha) = dark ? (0xF7D9A6u, 0.26) : (0xF0B860u, 0.34); - var (rose, roseAlpha) = dark ? (0xF5C0B8u, 0.24) : (0xEE9B92u, 0.32); - var (lilac, lilacAlpha) = dark ? (0xD8CBEEu, 0.08) : (0xC0A8E0u, 0.10); - - wash.Children.Add(GradientBlob(amber, amberAlpha, - centerX: 0.08 * width, centerY: -40, radiusX: 300, radiusY: 260)); - wash.Children.Add(GradientBlob(rose, roseAlpha, - centerX: 1.02 * width, centerY: 0, radiusX: 290, radiusY: 250)); - wash.Children.Add(GradientBlob(lilac, lilacAlpha, - centerX: 1.00 * width, centerY: 170, radiusX: 240, radiusY: 210)); - return wash; + return await dialog.ShowAsync() == ContentDialogResult.Primary; } - /// - /// One colour blob of the mesh: an empty Border stretched over the wash, painted with a - /// radial gradient that falls off to the same colour at zero alpha (fading to a named - /// transparent would drift through grey on the way out). Absolute mapping mode, so the - /// blob keeps its shape no matter how tall the dialog grows. - /// - private static Border GradientBlob( - uint rgb, double alpha, - double centerX, double centerY, double radiusX, double radiusY) - { - byte r = (byte)(rgb >> 16), g = (byte)(rgb >> 8), b = (byte)rgb; - return new Border - { - Background = new RadialGradientBrush - { - MappingMode = BrushMappingMode.Absolute, - Center = new Point(centerX, centerY), - GradientOrigin = new Point(centerX, centerY), - RadiusX = radiusX, - RadiusY = radiusY, - GradientStops = - { - // Slightly faster than linear, so the colour stays gathered around its - // centre and releases cleanly instead of holding a flat plateau. - new GradientStop { Color = Color.FromArgb((byte)(alpha * 255), r, g, b), Offset = 0 }, - new GradientStop { Color = Color.FromArgb((byte)(alpha * 0.42 * 255), r, g, b), Offset = 0.5 }, - new GradientStop { Color = Color.FromArgb(0, r, g, b), Offset = 1 }, - }, - }, - }; - } - - /// - /// ContentDialog's content padding, read from the theme resource so the banner's bleed - /// margin follows if WinUI retunes it. A Thickness is theme-invariant, so this app-level - /// lookup is safe in a way the brush lookups called out above are not. - /// - private static Thickness ContentDialogPadding() => - Application.Current.Resources.TryGetValue("ContentDialogPadding", out var value) - && value is Thickness padding - ? padding - : new Thickness(24); - /// /// One bullet as a two-column Grid rather than a "• "-prefixed string, so wrapped /// lines keep a hanging indent instead of running back under the bullet. From 18df2f38076cd4e0477c16105863da912a551efa Mon Sep 17 00:00:00 2001 From: Zero <1270128439@qq.com> Date: Tue, 15 Sep 2026 00:44:01 +0800 Subject: [PATCH 4/4] Reuse wrapped dialog message content --- Services/DialogService.cs | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/Services/DialogService.cs b/Services/DialogService.cs index 401ac2e..7fb7719 100644 --- a/Services/DialogService.cs +++ b/Services/DialogService.cs @@ -19,12 +19,13 @@ public class DialogService : IDialogService { private readonly Func _xamlRootFactory; - /// - /// Caps the wrapped message text of the plain notice dialogs. Handing ContentDialog a raw - /// string instead lets it measure out to its 548px maximum even for a one-liner, so every - /// short notice opens as wide as the longest one. - /// - private const double MessageMaxWidth = 280; + // Wrap and cap plain messages so short notices do not expand to the dialog's max width. + private static TextBlock CreateMessageText(string message) => new() + { + Text = message, + TextWrapping = TextWrapping.Wrap, + MaxWidth = 280, + }; public DialogService(Func xamlRootFactory) { @@ -662,20 +663,11 @@ void UpdateLanAddressText() public async Task ShowConfirmationAsync(string title, string message, string? confirmText = null, string? cancelText = null, bool isDanger = false) { - confirmText ??= L.Dialog_OK; - cancelText ??= L.Dialog_Cancel; - var content = new TextBlock - { - Text = message, - TextWrapping = TextWrapping.Wrap, - MaxWidth = MessageMaxWidth, - }; - var dialog = CreateDialog(); dialog.Title = title; - dialog.Content = content; - dialog.PrimaryButtonText = confirmText; - dialog.CloseButtonText = cancelText; + dialog.Content = CreateMessageText(message); + dialog.PrimaryButtonText = confirmText ?? L.Dialog_OK; + dialog.CloseButtonText = cancelText ?? L.Dialog_Cancel; dialog.DefaultButton = isDanger ? ContentDialogButton.None : ContentDialogButton.Primary; if (isDanger && Application.Current.Resources.TryGetValue("DangerAccentButtonStyle", out var style) && @@ -735,12 +727,7 @@ public async Task ShowErrorAsync(string title, string message, XamlRoot? xamlRoo { var dialog = CreateDialog(xamlRoot); dialog.Title = title; - dialog.Content = new TextBlock - { - Text = message, - TextWrapping = TextWrapping.Wrap, - MaxWidth = MessageMaxWidth, - }; + dialog.Content = CreateMessageText(message); dialog.CloseButtonText = L.Dialog_OK; await dialog.ShowAsync(); }