完善了日历组件
This commit is contained in:
lincube
2026-03-02 20:02:14 +08:00
parent 87f47e1887
commit 2436e43f65
25 changed files with 3660 additions and 609 deletions

View File

@@ -5,4 +5,6 @@ public static class BuiltInComponentIds
public const string Clock = "Clock";
public const string Blank2x4 = "Blank2x4";
public const string Date = "Date";
public const string MonthCalendar = "MonthCalendar";
public const string LunarCalendar = "LunarCalendar";
}

View File

@@ -26,18 +26,36 @@ public sealed class ComponentRegistry
"Clock",
"Clock",
"Status",
MinWidthCells: 1,
MinWidthCells: 3,
MinHeightCells: 1,
AllowStatusBarPlacement: true,
AllowDesktopPlacement: false),
new DesktopComponentDefinition(
BuiltInComponentIds.Date,
"Date",
"Calendar",
"Calendar",
"Date",
MinWidthCells: 4,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.MonthCalendar,
"Month Calendar",
"CalendarMonth",
"Date",
MinWidthCells: 2,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.LunarCalendar,
"Lunar Calendar",
"Calendar",
"Date",
MinWidthCells: 2,
MinHeightCells: 2,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true)
};

View File

@@ -1,4 +1,4 @@
{
{
"app.title": "LanMontainDesktop",
"button.back_to_windows": "Back to Windows",
"tooltip.back_to_windows": "Back to Windows",
@@ -15,6 +15,7 @@
"settings.wallpaper.description": "Pick an image or video to apply as the app window wallpaper immediately.",
"settings.wallpaper.current_label": "Current Wallpaper",
"settings.wallpaper.placement_label": "Placement",
"settings.wallpaper.placement_desc": "Adjust how the image fills the desktop.",
"settings.wallpaper.pick_button": "Browse Files",
"settings.wallpaper.clear_button": "Reset to Solid Color",
"settings.wallpaper.no_selection": "No wallpaper selected.",
@@ -38,6 +39,11 @@
"settings.grid.title": "Grid Layout",
"settings.grid.description": "Every component must occupy at least one cell (minimum 1x1).",
"settings.grid.short_side_label": "Short Side Cells",
"settings.grid.spacing_label": "Grid Spacing",
"settings.grid.spacing_relaxed": "Relaxed (iOS)",
"settings.grid.spacing_compact": "Compact (Android)",
"settings.grid.edge_inset_label": "Screen Inset",
"settings.grid.edge_inset_px_format": "≈ {0:F1}px",
"settings.grid.apply_button": "Apply",
"settings.grid.info_format": "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)",
"settings.color.title": "Color",
@@ -61,6 +67,13 @@
"settings.status_bar.description": "Choose which components appear on the top status bar.",
"settings.status_bar.clock_header": "Clock Component",
"settings.status_bar.clock_description": "Display a clock on the top status bar.",
"settings.status_bar.spacing_header": "Component Spacing",
"settings.status_bar.spacing_desc": "Adjust spacing between status bar components.",
"settings.status_bar.spacing_mode_compact": "Compact",
"settings.status_bar.spacing_mode_relaxed": "Relaxed",
"settings.status_bar.spacing_mode_custom": "Custom",
"settings.status_bar.spacing_custom_label": "Custom spacing (%)",
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
"settings.region.title": "Region",
"settings.region.description": "Choose language and apply immediately to settings and key UI.",
"settings.region.language_header": "Language",
@@ -86,12 +99,17 @@
"launcher.folder_items_format": "{0} apps",
"button.component_library": "Edit Desktop",
"tooltip.component_library": "Edit Desktop",
"component_library.title": "Edit Desktop",
"component_library.title": "Widgets",
"component_library.empty": "Swipe to pick a category, tap to open, then drag a widget onto the desktop.",
"component_library.drag_hint": "Drag to place",
"component_category.date": "Date",
"component.date": "Date",
"component.delete": "Delete",
"component.edit": "Edit",
"component_category.date": "Calendar",
"component.date": "Calendar",
"component.month_calendar": "Month Calendar",
"component.lunar_calendar": "Lunar Calendar",
"desktop.add_page": "Add page",
"desktop.delete_page": "Delete page",
"placement.fill": "Fill",
"placement.fit": "Fit",
"placement.stretch": "Stretch",

View File

@@ -1,4 +1,4 @@
{
{
"app.title": "LanMontainDesktop",
"button.back_to_windows": "回到Windows",
"tooltip.back_to_windows": "回到Windows",
@@ -15,6 +15,7 @@
"settings.wallpaper.description": "选择图片或视频后可立即设为应用窗口壁纸。",
"settings.wallpaper.current_label": "当前壁纸",
"settings.wallpaper.placement_label": "显示方式",
"settings.wallpaper.placement_desc": "调整图像在桌面上的填充方式。",
"settings.wallpaper.pick_button": "浏览文件",
"settings.wallpaper.clear_button": "恢复纯色",
"settings.wallpaper.no_selection": "未选择壁纸。",
@@ -38,6 +39,11 @@
"settings.grid.title": "网格布局",
"settings.grid.description": "每个组件至少占用一个格子(最小 1x1。",
"settings.grid.short_side_label": "短边格数",
"settings.grid.spacing_label": "网格间距",
"settings.grid.spacing_relaxed": "宽松iOS",
"settings.grid.spacing_compact": "紧凑Android",
"settings.grid.edge_inset_label": "屏幕边距",
"settings.grid.edge_inset_px_format": "≈ {0:F1}px",
"settings.grid.apply_button": "应用",
"settings.grid.info_format": "网格:{0} 列 x {1} 行 | 单元格 {2:F1}px1:1",
"settings.color.title": "颜色",
@@ -61,6 +67,13 @@
"settings.status_bar.description": "选择顶部状态栏显示的组件。",
"settings.status_bar.clock_header": "时间组件",
"settings.status_bar.clock_description": "在顶部状态栏显示时钟。",
"settings.status_bar.spacing_header": "组件间距",
"settings.status_bar.spacing_desc": "调整状态栏组件之间的间距。",
"settings.status_bar.spacing_mode_compact": "紧凑",
"settings.status_bar.spacing_mode_relaxed": "宽松",
"settings.status_bar.spacing_mode_custom": "自定义",
"settings.status_bar.spacing_custom_label": "自定义间距(%",
"settings.status_bar.spacing_custom_px_format": "≈ {0:F1}px",
"settings.region.title": "地区",
"settings.region.description": "选择语言并立即应用到设置与主要界面。",
"settings.region.language_header": "语言",
@@ -89,9 +102,14 @@
"component_library.title": "桌面编辑",
"component_library.empty": "左右滑动选择类别,点击进入,然后拖动组件到桌面放置。",
"component_library.drag_hint": "拖动放置",
"component_category.date": "日期",
"component.delete": "删除",
"component.edit": "编辑",
"component_category.date": "日历",
"component.date": "日历",
"component.month_calendar": "月历",
"component.lunar_calendar": "农历",
"desktop.add_page": "新增页面",
"desktop.delete_page": "删除页面",
"placement.fill": "填充",
"placement.fit": "适应",
"placement.stretch": "拉伸",

View File

@@ -6,6 +6,10 @@ public sealed class AppSettingsSnapshot
{
public int GridShortSideCells { get; set; } = 12;
public string GridSpacingPreset { get; set; } = "Relaxed";
public int DesktopEdgeInsetPercent { get; set; } = 18;
public bool? IsNightMode { get; set; }
public string? ThemeColor { get; set; }
@@ -28,10 +32,16 @@ public sealed class AppSettingsSnapshot
TaskbarActionId.OpenSettings.ToString()
];
public bool EnableDynamicTaskbarActions { get; set; } = false;
public bool EnableDynamicTaskbarActions { get; set; } = true;
public string TaskbarLayoutMode { get; set; } = "BottomFullRowMacStyle";
public string ClockDisplayFormat { get; set; } = "HourMinuteSecond";
public string StatusBarSpacingMode { get; set; } = "Relaxed";
public int StatusBarCustomSpacingPercent { get; set; } = 12;
public int DesktopPageCount { get; set; } = 1;
public int CurrentDesktopSurfaceIndex { get; set; } = 0;

View File

@@ -4,5 +4,8 @@ public enum TaskbarActionId
{
MinimizeToWindows,
OpenSettings,
AddDesktopPage
AddDesktopPage,
DeleteDesktopPage,
DeleteComponent,
EditComponent
}

View File

@@ -0,0 +1,227 @@
using System;
using System.Globalization;
namespace LanMontainDesktop.Services;
public sealed class LunarCalendarService
{
private static readonly ChineseLunisolarCalendar Calendar = new();
private static readonly string[] HeavenlyStemsZh =
[
"\u7532",
"\u4e59",
"\u4e19",
"\u4e01",
"\u620a",
"\u5df1",
"\u5e9a",
"\u8f9b",
"\u58ec",
"\u7678"
];
private static readonly string[] EarthlyBranchesZh =
[
"\u5b50",
"\u4e11",
"\u5bc5",
"\u536f",
"\u8fb0",
"\u5df3",
"\u5348",
"\u672a",
"\u7533",
"\u9149",
"\u620c",
"\u4ea5"
];
private static readonly string[] HeavenlyStemsEn =
["Jia", "Yi", "Bing", "Ding", "Wu", "Ji", "Geng", "Xin", "Ren", "Gui"];
private static readonly string[] EarthlyBranchesEn =
["Zi", "Chou", "Yin", "Mao", "Chen", "Si", "Wu", "Wei", "Shen", "You", "Xu", "Hai"];
private static readonly string[] ZodiacsZh =
[
"\u9f20",
"\u725b",
"\u864e",
"\u5154",
"\u9f99",
"\u86c7",
"\u9a6c",
"\u7f8a",
"\u7334",
"\u9e21",
"\u72d7",
"\u732a"
];
private static readonly string[] ZodiacsEn =
["Rat", "Ox", "Tiger", "Rabbit", "Dragon", "Snake", "Horse", "Goat", "Monkey", "Rooster", "Dog", "Pig"];
private static readonly string[] LunarMonthsZh =
[
"\u6b63",
"\u4e8c",
"\u4e09",
"\u56db",
"\u4e94",
"\u516d",
"\u4e03",
"\u516b",
"\u4e5d",
"\u5341",
"\u51ac",
"\u814a"
];
private static readonly string[] LunarDayDigitsZh =
[
"\u4e00",
"\u4e8c",
"\u4e09",
"\u56db",
"\u4e94",
"\u516d",
"\u4e03",
"\u516b",
"\u4e5d",
"\u5341"
];
public LunarCalendarInfo GetLunarInfo(DateTime dateTime)
{
var date = dateTime.Date;
try
{
var lunarYear = Calendar.GetYear(date);
var rawLunarMonth = Calendar.GetMonth(date);
var lunarDay = Calendar.GetDayOfMonth(date);
var (lunarMonth, isLeapMonth) = NormalizeLunarMonth(lunarYear, rawLunarMonth);
var sexagenaryYear = Calendar.GetSexagenaryYear(date);
var stemIndex = Calendar.GetCelestialStem(sexagenaryYear) - 1;
var branchIndex = Calendar.GetTerrestrialBranch(sexagenaryYear) - 1;
var ganzhiYearZh = $"{HeavenlyStemsZh[stemIndex]}{EarthlyBranchesZh[branchIndex]}";
var ganzhiYearEn = $"{HeavenlyStemsEn[stemIndex]}-{EarthlyBranchesEn[branchIndex]}";
var zodiacZh = ZodiacsZh[branchIndex];
var zodiacEn = ZodiacsEn[branchIndex];
return new LunarCalendarInfo(
LunarYear: lunarYear,
LunarMonth: lunarMonth,
LunarDay: lunarDay,
IsLeapMonth: isLeapMonth,
LunarDateZh: BuildLunarDateZh(lunarMonth, lunarDay, isLeapMonth),
LunarDateEn: BuildLunarDateEn(lunarMonth, lunarDay, isLeapMonth),
GanzhiYearZh: ganzhiYearZh,
GanzhiYearEn: ganzhiYearEn,
ZodiacZh: zodiacZh,
ZodiacEn: zodiacEn);
}
catch (ArgumentOutOfRangeException)
{
// ChineseLunisolarCalendar has a limited date range.
return new LunarCalendarInfo(
LunarYear: date.Year,
LunarMonth: date.Month,
LunarDay: date.Day,
IsLeapMonth: false,
LunarDateZh: "\u65e5\u671f\u8d85\u51fa\u519c\u5386\u652f\u6301\u8303\u56f4",
LunarDateEn: date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
GanzhiYearZh: "-",
GanzhiYearEn: "-",
ZodiacZh: "-",
ZodiacEn: "-");
}
}
private static (int Month, bool IsLeapMonth) NormalizeLunarMonth(int lunarYear, int rawMonth)
{
var leapMonth = Calendar.GetLeapMonth(lunarYear);
if (leapMonth == 0)
{
return (rawMonth, false);
}
if (rawMonth == leapMonth)
{
return (rawMonth - 1, true);
}
if (rawMonth > leapMonth)
{
return (rawMonth - 1, false);
}
return (rawMonth, false);
}
private static string BuildLunarDateZh(int lunarMonth, int lunarDay, bool isLeapMonth)
{
var monthName = lunarMonth is >= 1 and <= 12
? LunarMonthsZh[lunarMonth - 1]
: lunarMonth.ToString(CultureInfo.InvariantCulture);
var leapPrefix = isLeapMonth ? "\u95f0" : string.Empty;
return $"{leapPrefix}{monthName}\u6708{BuildLunarDayZh(lunarDay)}";
}
private static string BuildLunarDateEn(int lunarMonth, int lunarDay, bool isLeapMonth)
{
var leapPrefix = isLeapMonth ? "Leap " : string.Empty;
return $"{leapPrefix}M{lunarMonth} D{lunarDay}";
}
private static string BuildLunarDayZh(int day)
{
if (day <= 0)
{
return day.ToString(CultureInfo.InvariantCulture);
}
if (day <= 10)
{
return day == 10 ? "\u521d\u5341" : $"\u521d{LunarDayDigitsZh[day - 1]}";
}
if (day < 20)
{
return $"\u5341{LunarDayDigitsZh[day - 11]}";
}
if (day == 20)
{
return "\u4e8c\u5341";
}
if (day < 30)
{
return $"\u5eff{LunarDayDigitsZh[day - 21]}";
}
if (day == 30)
{
return "\u4e09\u5341";
}
return day.ToString(CultureInfo.InvariantCulture);
}
}
public sealed record LunarCalendarInfo(
int LunarYear,
int LunarMonth,
int LunarDay,
bool IsLeapMonth,
string LunarDateZh,
string LunarDateEn,
string GanzhiYearZh,
string GanzhiYearEn,
string ZodiacZh,
string ZodiacEn);

View File

@@ -10,7 +10,7 @@
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="CornerRadius" Value="20" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Padding" Value="16,10" />
@@ -57,7 +57,7 @@
<Style Selector="Button.swatch-button">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="CornerRadius" Value="16" />
<Setter Property="Opacity" Value="0.88" />
</Style>
@@ -70,25 +70,39 @@
<Style Selector="Border.glass-panel">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BorderThickness" Value="1.2" />
<Setter Property="CornerRadius" Value="28" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassPanelOpacity}" />
<Setter Property="BoxShadow" Value="0 1 2 #1A000000" />
<Setter Property="BoxShadow" Value="0 4 12 #1A000000" />
</Style>
<Style Selector="Border.glass-strong">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassStrongBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="CornerRadius" Value="32" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
<Setter Property="BoxShadow" Value="0 2 4 #26000000" />
<Setter Property="BoxShadow" Value="0 8 24 #26000000" />
</Style>
<Style Selector="Border.glass-island">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassStrongBorderBrush}" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="CornerRadius" Value="36" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
<Setter Property="BoxShadow" Value="0 12 32 #33000000" />
<Setter Property="Transitions">
<Transitions>
<ThicknessTransition Property="Padding" Duration="0:0:0.2" Easing="QuarticEaseOut" />
</Transitions>
</Setter>
</Style>
<Style Selector="Border.mica-strong">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="CornerRadius" Value="36" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
<Setter Property="BoxShadow" Value="0 8 22 #2A000000" />
</Style>

View File

@@ -3,21 +3,31 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="220"
d:DesignHeight="70"
d:DesignWidth="180"
d:DesignHeight="48"
x:Class="LanMontainDesktop.Views.Components.ClockWidget">
<Border x:Name="RootBorder"
Classes="glass-panel"
Padding="8"
CornerRadius="8">
<TextBlock x:Name="TimeTextBlock"
Padding="0"
CornerRadius="24">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextAlignment="Center"
FontSize="26"
VerticalAlignment="Center">
<TextBlock x:Name="MainTimeTextBlock"
FontFeatures="tnum"
FontSize="20"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="SecondsTextBlock"
FontFeatures="tnum"
FontSize="14"
FontWeight="Normal"
Opacity="0.75"
Margin="4,2,0,0"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
</StackPanel>
</Border>
</UserControl>

View File

@@ -8,6 +8,12 @@ using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views.Components;
public enum ClockDisplayFormat
{
HourMinuteSecond, // HH:mm:ss
HourMinute // HH:mm
}
public partial class ClockWidget : UserControl
{
private readonly DispatcherTimer _timer = new()
@@ -16,6 +22,7 @@ public partial class ClockWidget : UserControl
};
private TimeZoneService? _timeZoneService;
private ClockDisplayFormat _displayFormat = ClockDisplayFormat.HourMinuteSecond;
public ClockWidget()
{
@@ -27,9 +34,21 @@ public partial class ClockWidget : UserControl
UpdateClock();
}
/// <summary>
/// 设置时区服务
/// </summary>
public ClockDisplayFormat DisplayFormat
{
get => _displayFormat;
set
{
_displayFormat = value;
UpdateClock();
}
}
public void SetDisplayFormat(ClockDisplayFormat format)
{
DisplayFormat = format;
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService != null)
@@ -66,17 +85,45 @@ public partial class ClockWidget : UserControl
private void UpdateClock()
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
TimeTextBlock.Text = now.ToString("HH:mm:ss", CultureInfo.CurrentCulture);
MainTimeTextBlock.Text = now.ToString("HH:mm", CultureInfo.CurrentCulture);
SecondsTextBlock.Text = now.ToString("ss", CultureInfo.CurrentCulture);
SecondsTextBlock.IsVisible = _displayFormat == ClockDisplayFormat.HourMinuteSecond;
}
public void ApplyCellSize(double cellSize)
{
var padding = Math.Clamp(cellSize * 0.12, 2, 14);
RootBorder.Padding = new Thickness(padding);
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.16, 4, 18));
// --- Class Island “满盈”风格算法 ---
// Keep the time legible across dense and sparse grid layouts.
TimeTextBlock.FontSize = Math.Clamp(cellSize * 0.42, 10, 56);
TimeTextBlock.FontWeight = FontWeight.SemiBold;
// 1. 计算组件高度:保持与任务栏核心比例一致 (0.74x)
var targetHeight = Math.Clamp(cellSize * 0.74, 34, 74);
RootBorder.Height = targetHeight;
// 2. 动态圆角:确保始终是完美的胶囊半圆
RootBorder.CornerRadius = new CornerRadius(targetHeight / 2);
RootBorder.VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center;
// 3. 核心:满盈字阶 (Filled Typography)
// 使主时间文字占据容器高度的 ~68%,产生饱满的视觉张力
var mainFontSize = targetHeight * 0.68;
MainTimeTextBlock.FontSize = mainFontSize;
MainTimeTextBlock.FontWeight = FontWeight.SemiBold;
// 4. 次级信息:秒数维持 0.7x 比例,并增强透明度呼吸感
SecondsTextBlock.FontSize = mainFontSize * 0.7;
SecondsTextBlock.Opacity = 0.55;
// 5. 视觉占比:占据约 2.2 个单元格的感官宽度 (cellSize * 2 + gaps)
RootBorder.MinWidth = cellSize * 2.2;
// 6. 间距微调
if (MainTimeTextBlock.Parent is StackPanel panel)
{
panel.Spacing = Math.Clamp(cellSize * 0.06, 2, 8);
}
// 确保清除可能存在的固定 Padding由代码控制“紧密感”
RootBorder.Padding = new Thickness(Math.Clamp(cellSize * 0.15, 12, 24), 0);
}
}

View File

@@ -3,81 +3,111 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="400"
d:DesignHeight="200"
d:DesignWidth="460"
d:DesignHeight="220"
x:Class="LanMontainDesktop.Views.Components.DateWidget">
<Border x:Name="RootBorder"
Background="Transparent"
CornerRadius="16"
ClipToBounds="True">
<Grid ColumnDefinitions="*,*">
<!-- 左侧:月历 -->
<Border x:Name="CalendarBackgroundBorder"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
CornerRadius="28"
ClipToBounds="True"
Padding="12">
<Viewbox Stretch="Uniform">
<Grid Width="460"
Height="220"
ColumnDefinitions="1.2*,1*"
ColumnSpacing="12">
<Grid x:Name="LeftPanelGrid"
Grid.Column="0"
Padding="12"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
<Grid RowDefinitions="Auto,*">
<!-- 月份年份 -->
<TextBlock x:Name="CalendarMonthYearTextBlock"
RowDefinitions="Auto,Auto,*"
RowSpacing="8">
<TextBlock x:Name="GregorianHeadlineTextBlock"
Grid.Row="0"
HorizontalAlignment="Center"
FontSize="12"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Margin="0,0,0,8" />
FontSize="22"
FontWeight="Bold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<UniformGrid Grid.Row="1"
Columns="7">
<TextBlock x:Name="WeekdayText0" Text="日" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText1" Text="一" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText2" Text="二" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText3" Text="三" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText4" Text="四" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText5" Text="五" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText6" Text="六" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="13" FontWeight="SemiBold" />
</UniformGrid>
<!-- 月历网格 -->
<Grid x:Name="CalendarGrid"
Grid.Row="1"
RowDefinitions="Auto,*,*,*,*,*"
ColumnDefinitions="*,*,*,*,*,*,*">
<!-- 星期标题 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="日" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="一" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="2" Text="二" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="3" Text="三" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="4" Text="四" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="5" Text="五" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Grid.Row="0" Grid.Column="6" Text="六" HorizontalAlignment="Center" FontSize="10" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
</Grid>
</Grid>
</Border>
<!-- 右侧:今日详情 -->
<Border x:Name="TodayBackgroundBorder"
Grid.Column="1"
Background="{DynamicResource AdaptiveAccentBrush}"
Padding="16">
<Grid RowDefinitions="Auto,*,Auto">
<!-- 今日标签 -->
<TextBlock Grid.Row="0"
Text="今天"
HorizontalAlignment="Center"
FontSize="11"
FontWeight="Medium"
Opacity="0.8"
Foreground="{DynamicResource AdaptiveOnAccentBrush}" />
<!-- 日期数字 -->
<TextBlock x:Name="TodayDayTextBlock"
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="56"
FontWeight="Light"
Foreground="{DynamicResource AdaptiveOnAccentBrush}" />
<!-- 星期 -->
<TextBlock x:Name="TodayWeekdayTextBlock"
Grid.Row="2"
HorizontalAlignment="Center"
FontSize="13"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveOnAccentBrush}" />
RowDefinitions="*,*,*,*,*"
ColumnDefinitions="*,*,*,*,*,*,*" />
</Grid>
<Border x:Name="LunarCardBorder"
Grid.Column="1"
Background="{DynamicResource AdaptiveLayer2Brush}"
CornerRadius="24"
Padding="14">
<Grid x:Name="RightPanelGrid"
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
RowSpacing="10">
<TextBlock x:Name="LunarDateTextBlock"
Grid.Row="0"
FontSize="28"
FontWeight="Bold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="LunarMetaTextBlock"
Grid.Row="1"
FontSize="14"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Opacity="0.88"
TextWrapping="Wrap" />
<Border Grid.Row="2"
Height="1"
Margin="0,2,0,2"
Background="{DynamicResource AdaptiveStrokeBrush}" />
<Grid Grid.Row="3"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<TextBlock x:Name="YiLabelTextBlock"
Grid.Column="0"
Text="宜"
FontSize="18"
FontWeight="Bold"
Foreground="#4E7D3A" />
<TextBlock x:Name="YiItemsTextBlock"
Grid.Column="1"
FontSize="16"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis" />
</Grid>
<Grid Grid.Row="4"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<TextBlock x:Name="JiLabelTextBlock"
Grid.Column="0"
Text="忌"
FontSize="18"
FontWeight="Bold"
Foreground="#A1473E" />
<TextBlock x:Name="JiItemsTextBlock"
Grid.Column="1"
FontSize="16"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis" />
</Grid>
</Grid>
</Border>
</Grid>
</Viewbox>
</Border>
</UserControl>

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
@@ -15,8 +15,15 @@ public partial class DateWidget : UserControl
{
Interval = TimeSpan.FromMinutes(1)
};
private static readonly LunarCalendarService LunarCalendarService = new();
private static readonly string[] ZhWeekdayHeaders = ["日", "一", "二", "三", "四", "五", "六"];
private static readonly string[] EnWeekdayHeaders = ["S", "M", "T", "W", "T", "F", "S"];
private TimeZoneService? _timeZoneService;
private double _currentCellSize = 64;
private double _calendarDayFontSize = 14;
private double _calendarTodayDotSize = 28;
public DateWidget()
{
@@ -25,12 +32,10 @@ public partial class DateWidget : UserControl
_timer.Tick += OnTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
UpdateDate();
}
/// <summary>
/// 设置时区服务
/// </summary>
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService != null)
@@ -54,6 +59,11 @@ public partial class DateWidget : UserControl
_timer.Stop();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyCellSize(_currentCellSize);
}
private void OnTimerTick(object? sender, EventArgs e)
{
UpdateDate();
@@ -68,30 +78,75 @@ public partial class DateWidget : UserControl
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
var culture = CultureInfo.CurrentCulture;
var isZh = culture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
var lunar = LunarCalendarService.GetLunarInfo(now);
// 右侧:今日详情
TodayDayTextBlock.Text = now.Day.ToString();
TodayWeekdayTextBlock.Text = now.ToString("dddd", culture);
GregorianHeadlineTextBlock.Text = isZh
? $"{now.Month}月{now.Day}日 {ToChineseWeekday(now.DayOfWeek)}"
: now.ToString("MMM d ddd", culture);
// 左侧:月历
CalendarMonthYearTextBlock.Text = now.ToString("yyyy年M月", culture);
if (isZh)
{
LunarDateTextBlock.Text = $"农历 {lunar.LunarDateZh}";
LunarMetaTextBlock.Text = $"{lunar.GanzhiYearZh}年({lunar.ZodiacZh}年)";
YiLabelTextBlock.Text = "宜";
JiLabelTextBlock.Text = "忌";
YiItemsTextBlock.Text = "祭祀 祈福 出行 会友";
JiItemsTextBlock.Text = "动土 诉讼 远航 争执";
}
else
{
LunarDateTextBlock.Text = $"Lunar {lunar.LunarDateEn}";
LunarMetaTextBlock.Text = $"Ganzhi year: {lunar.GanzhiYearEn} ({lunar.ZodiacEn})";
YiLabelTextBlock.Text = "Do";
JiLabelTextBlock.Text = "Avoid";
YiItemsTextBlock.Text = "Worship Blessing Travel Meet";
JiItemsTextBlock.Text = "Groundwork Lawsuit Voyage Dispute";
}
// 生成月历
UpdateWeekdayHeaders(isZh);
GenerateCalendar(now);
}
private static string ToChineseWeekday(DayOfWeek dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeek.Sunday => "周日",
DayOfWeek.Monday => "周一",
DayOfWeek.Tuesday => "周二",
DayOfWeek.Wednesday => "周三",
DayOfWeek.Thursday => "周四",
DayOfWeek.Friday => "周五",
_ => "周六"
};
}
private void UpdateWeekdayHeaders(bool isZh)
{
var headers = isZh ? ZhWeekdayHeaders : EnWeekdayHeaders;
WeekdayText0.Text = headers[0];
WeekdayText1.Text = headers[1];
WeekdayText2.Text = headers[2];
WeekdayText3.Text = headers[3];
WeekdayText4.Text = headers[4];
WeekdayText5.Text = headers[5];
WeekdayText6.Text = headers[6];
}
private void GenerateCalendar(DateTime currentDate)
{
// 清空之前的日期(保留星期标题)
var childrenToRemove = new List<Control>();
var removeList = new List<Control>();
foreach (var child in CalendarGrid.Children)
{
if (child is TextBlock tb && tb.Tag?.ToString() == "day")
if (child is Control control && control.Tag is string tag &&
(tag == "day" || tag == "today-dot"))
{
childrenToRemove.Add(tb);
removeList.Add(control);
}
}
foreach (var child in childrenToRemove)
foreach (var child in removeList)
{
CalendarGrid.Children.Remove(child);
}
@@ -100,32 +155,31 @@ public partial class DateWidget : UserControl
var month = currentDate.Month;
var today = currentDate.Day;
// 获取该月第一天
var firstDayOfMonth = new DateTime(year, month, 1);
var daysInMonth = DateTime.DaysInMonth(year, month);
var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek; // 0 = Sunday
var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek;
// 生成日期
for (int day = 1; day <= daysInMonth; day++)
for (var day = 1; day <= daysInMonth; day++)
{
var row = ((day + startDayOfWeek - 1) / 7) + 1; // +1 because row 0 is weekday headers
var row = (day + startDayOfWeek - 1) / 7;
var col = (day + startDayOfWeek - 1) % 7;
if (row > 5) continue; // 最多显示6行
if (row > 4)
{
continue;
}
var dayText = new TextBlock
{
Text = day.ToString(),
Text = day.ToString(CultureInfo.CurrentCulture),
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
FontSize = 10,
FontSize = _calendarDayFontSize,
FontWeight = FontWeight.SemiBold,
Tag = "day"
};
// 今天高亮
if (day == today)
{
// 使用主题色高亮今天
var accentBrush = this.TryFindResource("AdaptiveAccentBrush", out var accent)
? accent as IBrush
: Brushes.Blue;
@@ -134,31 +188,28 @@ public partial class DateWidget : UserControl
: Brushes.White;
dayText.Foreground = onAccentBrush;
dayText.FontWeight = FontWeight.Bold;
dayText.Background = new SolidColorBrush(Colors.Transparent);
// 添加背景圆
var highlight = new Border
var dot = new Border
{
Width = _calendarTodayDotSize,
Height = _calendarTodayDotSize,
CornerRadius = new CornerRadius(_calendarTodayDotSize * 0.5),
Background = accentBrush,
CornerRadius = new CornerRadius(10),
Width = 20,
Height = 20,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
Child = dayText
Child = dayText,
Tag = "today-dot"
};
Grid.SetRow(highlight, row);
Grid.SetColumn(highlight, col);
CalendarGrid.Children.Add(highlight);
Grid.SetRow(dot, row);
Grid.SetColumn(dot, col);
CalendarGrid.Children.Add(dot);
}
else
{
// 使用主题次要文本颜色
var secondaryBrush = this.TryFindResource("AdaptiveTextSecondaryBrush", out var secondary)
? secondary as IBrush
: Brushes.Gray;
dayText.Foreground = secondaryBrush;
var isWeekend = col is 0 or 6;
dayText.Foreground = isWeekend
? GetThemeBrush("AdaptiveTextSecondaryBrush", 0.82)
: GetThemeBrush("AdaptiveTextPrimaryBrush", 0.92);
Grid.SetRow(dayText, row);
Grid.SetColumn(dayText, col);
CalendarGrid.Children.Add(dayText);
@@ -168,13 +219,59 @@ public partial class DateWidget : UserControl
public void ApplyCellSize(double cellSize)
{
// 根据格子大小调整圆角
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.12, 8, 20));
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
// 调整字体大小
var baseFontSize = cellSize * 0.25;
TodayDayTextBlock.FontSize = Math.Clamp(baseFontSize * 2.8, 28, 72);
TodayWeekdayTextBlock.FontSize = Math.Clamp(baseFontSize * 0.6, 10, 16);
CalendarMonthYearTextBlock.FontSize = Math.Clamp(baseFontSize * 0.55, 9, 14);
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(28 * scale, 16, 40));
RootBorder.Padding = new Thickness(Math.Clamp(12 * scale, 8, 18));
LeftPanelGrid.RowSpacing = Math.Clamp(8 * scale, 5, 14);
RightPanelGrid.RowSpacing = Math.Clamp(10 * scale, 6, 16);
LunarCardBorder.CornerRadius = new CornerRadius(Math.Clamp(24 * scale, 14, 34));
LunarCardBorder.Padding = new Thickness(Math.Clamp(14 * scale, 9, 20));
GregorianHeadlineTextBlock.FontSize = Math.Clamp(22 * scale, 14, 34);
WeekdayText0.FontSize = Math.Clamp(13 * scale, 9, 18);
WeekdayText1.FontSize = WeekdayText0.FontSize;
WeekdayText2.FontSize = WeekdayText0.FontSize;
WeekdayText3.FontSize = WeekdayText0.FontSize;
WeekdayText4.FontSize = WeekdayText0.FontSize;
WeekdayText5.FontSize = WeekdayText0.FontSize;
WeekdayText6.FontSize = WeekdayText0.FontSize;
LunarDateTextBlock.FontSize = Math.Clamp(28 * scale, 17, 44);
LunarMetaTextBlock.FontSize = Math.Clamp(14 * scale, 10, 22);
YiLabelTextBlock.FontSize = Math.Clamp(18 * scale, 12, 28);
JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize;
YiItemsTextBlock.FontSize = Math.Clamp(16 * scale, 11, 24);
JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize;
_calendarDayFontSize = Math.Clamp(14 * scale, 9, 22);
_calendarTodayDotSize = Math.Clamp(28 * scale, 17, 38);
UpdateDate();
}
private double ResolveScale()
{
var cellScale = Math.Clamp(_currentCellSize / 48d, 0.72, 1.55);
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 220d, 0.65, 1.65) : 1;
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 460d, 0.65, 1.65) : 1;
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.08), 0.65, 1.6);
}
private IBrush GetThemeBrush(string key, double opacity)
{
if (this.TryFindResource(key, out var value) && value is IBrush brush)
{
if (brush is ISolidColorBrush solid)
{
return new SolidColorBrush(solid.Color, opacity);
}
return brush;
}
return new SolidColorBrush(Colors.Gray, opacity);
}
}

View File

@@ -0,0 +1,20 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="400"
d:DesignHeight="300"
x:Class="LanMontainDesktop.Views.Components.DateWidgetSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="24">
<StackPanel Spacing="16">
<TextBlock Text="日历组件设置"
FontSize="20"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
</StackPanel>
</Border>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace LanMontainDesktop.Views.Components;
public partial class DateWidgetSettingsWindow : UserControl
{
public DateWidgetSettingsWindow()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,87 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="300"
d:DesignHeight="300"
x:Class="LanMontainDesktop.Views.Components.LunarCalendarWidget">
<Border x:Name="RootBorder"
Background="#EFE6D9"
CornerRadius="30"
ClipToBounds="True"
Padding="16">
<Viewbox Stretch="Uniform">
<Grid x:Name="LayoutRoot"
Width="300"
Height="300"
RowDefinitions="Auto,Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="GregorianLineTextBlock"
Grid.Row="0"
Text="10/9 Thu"
FontSize="24"
FontWeight="SemiBold"
Foreground="#7A5A47"
HorizontalAlignment="Center" />
<TextBlock x:Name="LunarDateTextBlock"
Grid.Row="1"
Text="Lunar"
FontSize="88"
FontWeight="Bold"
Foreground="#6B4936"
HorizontalAlignment="Center" />
<Border x:Name="DividerBorder"
Grid.Row="2"
Height="1"
Margin="8,8,8,2"
Background="#D2C6B7" />
<Grid x:Name="AuspiciousGrid"
Grid.Row="3"
RowDefinitions="Auto,Auto"
RowSpacing="12">
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<TextBlock x:Name="YiLabelTextBlock"
Grid.Column="0"
Text="Yi"
FontSize="30"
FontWeight="Bold"
Foreground="#5F6D2E" />
<TextBlock x:Name="YiItemsTextBlock"
Grid.Column="1"
FontSize="24"
FontWeight="SemiBold"
Foreground="#6B4936"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="Auto,*"
ColumnSpacing="8">
<TextBlock x:Name="JiLabelTextBlock"
Grid.Column="0"
Text="Ji"
FontSize="30"
FontWeight="Bold"
Foreground="#8A4A3A" />
<TextBlock x:Name="JiItemsTextBlock"
Grid.Column="1"
FontSize="24"
FontWeight="SemiBold"
Foreground="#6B4936"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
</Grid>
</Grid>
</Grid>
</Viewbox>
</Border>
</UserControl>

View File

@@ -0,0 +1,239 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views.Components;
public partial class LunarCalendarWidget : UserControl
{
private readonly DispatcherTimer _timer = new()
{
Interval = TimeSpan.FromMinutes(1)
};
private static readonly LunarCalendarService LunarCalendarService = new();
private static readonly string[] ZhYiCandidates =
[
"\u796d\u7940",
"\u7948\u798f",
"\u4f1a\u53cb",
"\u51fa\u884c",
"\u6c42\u8d22",
"\u5f00\u5e02",
"\u4ea4\u6613",
"\u5ac1\u5a36",
"\u6c42\u5b66",
"\u4fee\u9020",
"\u5b89\u5e8a",
"\u7eb3\u91c7"
];
private static readonly string[] ZhJiCandidates =
[
"\u52a8\u571f",
"\u8bc9\u8bbc",
"\u8fdc\u822a",
"\u4e89\u6267",
"\u7834\u571f",
"\u5b89\u846c",
"\u4f10\u6728",
"\u6398\u4e95",
"\u8fc1\u5f99",
"\u5f00\u4ed3",
"\u7f6e\u4ea7",
"\u5f00\u6e20"
];
private static readonly string[] EnYiCandidates =
[
"Worship",
"Blessing",
"Travel",
"Meetings",
"Trade",
"Business",
"Study",
"Build",
"Gathering",
"Planning"
];
private static readonly string[] EnJiCandidates =
[
"Dispute",
"Lawsuit",
"Major move",
"Groundwork",
"Burial",
"Long voyage",
"Contract rush",
"Risky purchase",
"Heavy repair",
"Conflict"
];
private TimeZoneService? _timeZoneService;
private double _currentCellSize = 48;
public LunarCalendarWidget()
{
InitializeComponent();
_timer.Tick += OnTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
UpdateContent();
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService is not null)
{
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
}
_timeZoneService = timeZoneService;
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
UpdateContent();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
UpdateContent();
_timer.Start();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_timer.Stop();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyCellSize(_currentCellSize);
}
private void OnTimerTick(object? sender, EventArgs e)
{
UpdateContent();
}
private void OnTimeZoneChanged(object? sender, EventArgs e)
{
UpdateContent();
}
private void UpdateContent()
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
var culture = CultureInfo.CurrentCulture;
var isZh = culture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
var lunar = LunarCalendarService.GetLunarInfo(now);
GregorianLineTextBlock.Text = isZh
? $"{now.Month}\u6708{now.Day}\u65e5 {ToChineseWeekday(now.DayOfWeek)}"
: now.ToString("MMM d ddd", culture);
LunarDateTextBlock.Text = isZh ? lunar.LunarDateZh : lunar.LunarDateEn;
YiLabelTextBlock.Text = isZh ? "\u5b9c" : "Do";
JiLabelTextBlock.Text = isZh ? "\u5fcc" : "Avoid";
YiItemsTextBlock.Text = BuildDailySelection(
now.Date,
isZh ? ZhYiCandidates : EnYiCandidates,
count: 4,
salt: 17,
useChineseSpacing: isZh);
JiItemsTextBlock.Text = BuildDailySelection(
now.Date,
isZh ? ZhJiCandidates : EnJiCandidates,
count: 4,
salt: 29,
useChineseSpacing: isZh);
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(30 * scale, 16, 44));
RootBorder.Padding = new Thickness(Math.Clamp(16 * scale, 8, 24));
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 18);
DividerBorder.Margin = new Thickness(
Math.Clamp(8 * scale, 3, 14),
Math.Clamp(8 * scale, 3, 14),
Math.Clamp(8 * scale, 3, 14),
Math.Clamp(2 * scale, 1, 6));
AuspiciousGrid.RowSpacing = Math.Clamp(12 * scale, 6, 20);
GregorianLineTextBlock.FontSize = Math.Clamp(24 * scale, 11, 36);
LunarDateTextBlock.FontSize = Math.Clamp(88 * scale, 30, 130);
YiLabelTextBlock.FontSize = Math.Clamp(30 * scale, 13, 44);
JiLabelTextBlock.FontSize = YiLabelTextBlock.FontSize;
YiItemsTextBlock.FontSize = Math.Clamp(24 * scale, 11, 36);
JiItemsTextBlock.FontSize = YiItemsTextBlock.FontSize;
}
private double ResolveScale()
{
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.62, 1.95);
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 300d, 0.58, 2.0) : 1;
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 300d, 0.58, 2.0) : 1;
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.05), 0.58, 1.95);
}
private static string ToChineseWeekday(DayOfWeek dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeek.Sunday => "\u5468\u65e5",
DayOfWeek.Monday => "\u5468\u4e00",
DayOfWeek.Tuesday => "\u5468\u4e8c",
DayOfWeek.Wednesday => "\u5468\u4e09",
DayOfWeek.Thursday => "\u5468\u56db",
DayOfWeek.Friday => "\u5468\u4e94",
_ => "\u5468\u516d"
};
}
private static string BuildDailySelection(
DateTime date,
string[] pool,
int count,
int salt,
bool useChineseSpacing)
{
if (pool.Length == 0 || count <= 0)
{
return string.Empty;
}
var target = Math.Min(count, pool.Length);
var selected = new List<string>(target);
var usedIndices = new HashSet<int>();
var cursor = Math.Abs(date.Year * 1009 + date.DayOfYear * 37 + salt * 211);
var step = (salt % Math.Max(1, pool.Length - 1)) + 1;
for (var i = 0; i < pool.Length * 3 && selected.Count < target; i++)
{
var index = (cursor + i * step) % pool.Length;
if (usedIndices.Add(index))
{
selected.Add(pool[index]);
}
}
if (selected.Count == 0)
{
return string.Empty;
}
return string.Join(useChineseSpacing ? " " : ", ", selected);
}
}

View File

@@ -0,0 +1,49 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="280"
d:DesignHeight="280"
x:Class="LanMontainDesktop.Views.Components.MonthCalendarWidget">
<Border x:Name="RootBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="28"
ClipToBounds="True"
Padding="14">
<Viewbox Stretch="Uniform">
<Grid x:Name="LayoutRoot"
Width="280"
Height="280"
RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="HeaderTextBlock"
Grid.Row="0"
FontSize="42"
FontWeight="Bold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<UniformGrid Grid.Row="1"
Columns="7">
<TextBlock x:Name="WeekdayText0" Text="S" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText1" Text="M" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText2" Text="T" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText3" Text="W" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText4" Text="T" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText5" Text="F" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="WeekdayText6" Text="S" HorizontalAlignment="Center" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" FontSize="20" FontWeight="SemiBold" />
</UniformGrid>
<Grid x:Name="CalendarGrid"
Grid.Row="2"
RowDefinitions="*,*,*,*,*,*"
ColumnDefinitions="*,*,*,*,*,*,*" />
</Grid>
</Viewbox>
</Border>
</UserControl>

View File

@@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views.Components;
public partial class MonthCalendarWidget : UserControl
{
private readonly DispatcherTimer _timer = new()
{
Interval = TimeSpan.FromMinutes(1)
};
private static readonly string[] ZhWeekdayHeaders = ["\u65e5", "\u4e00", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d"];
private static readonly string[] EnWeekdayHeaders = ["S", "M", "T", "W", "T", "F", "S"];
private TimeZoneService? _timeZoneService;
private double _currentCellSize = 48;
private double _calendarDayFontSize = 22;
private double _calendarTodayDotSize = 44;
public MonthCalendarWidget()
{
InitializeComponent();
_timer.Tick += OnTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
UpdateCalendar();
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
{
if (_timeZoneService is not null)
{
_timeZoneService.TimeZoneChanged -= OnTimeZoneChanged;
}
_timeZoneService = timeZoneService;
_timeZoneService.TimeZoneChanged += OnTimeZoneChanged;
UpdateCalendar();
}
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
UpdateCalendar();
_timer.Start();
}
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_timer.Stop();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyCellSize(_currentCellSize);
}
private void OnTimerTick(object? sender, EventArgs e)
{
UpdateCalendar();
}
private void OnTimeZoneChanged(object? sender, EventArgs e)
{
UpdateCalendar();
}
private void UpdateCalendar()
{
var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now;
var culture = CultureInfo.CurrentCulture;
var isZh = culture.TwoLetterISOLanguageName.Equals("zh", StringComparison.OrdinalIgnoreCase);
HeaderTextBlock.Text = isZh
? $"{now.Month}\u6708{now.Day}\u65e5"
: now.ToString("MMM d", culture);
UpdateWeekdayHeaders(isZh);
GenerateCalendar(now);
}
private void UpdateWeekdayHeaders(bool isZh)
{
var headers = isZh ? ZhWeekdayHeaders : EnWeekdayHeaders;
var blocks = GetWeekdayHeaderBlocks();
for (var i = 0; i < blocks.Count; i++)
{
blocks[i].Text = headers[i];
}
}
private IReadOnlyList<TextBlock> GetWeekdayHeaderBlocks()
{
return
[
WeekdayText0,
WeekdayText1,
WeekdayText2,
WeekdayText3,
WeekdayText4,
WeekdayText5,
WeekdayText6
];
}
private void GenerateCalendar(DateTime currentDate)
{
var removeList = new List<Control>();
foreach (var child in CalendarGrid.Children)
{
if (child is Control control &&
control.Tag is string tag &&
(tag == "day" || tag == "today-dot"))
{
removeList.Add(control);
}
}
foreach (var child in removeList)
{
CalendarGrid.Children.Remove(child);
}
var year = currentDate.Year;
var month = currentDate.Month;
var today = currentDate.Day;
var firstDayOfMonth = new DateTime(year, month, 1);
var daysInMonth = DateTime.DaysInMonth(year, month);
var startDayOfWeek = (int)firstDayOfMonth.DayOfWeek;
for (var day = 1; day <= daysInMonth; day++)
{
var row = (day + startDayOfWeek - 1) / 7;
var col = (day + startDayOfWeek - 1) % 7;
if (row > 5)
{
continue;
}
var dayText = new TextBlock
{
Text = day.ToString(CultureInfo.CurrentCulture),
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
FontSize = _calendarDayFontSize,
FontWeight = FontWeight.SemiBold,
Tag = "day"
};
if (day == today)
{
var accentBrush = this.TryFindResource("AdaptiveAccentBrush", out var accent)
? accent as IBrush
: Brushes.Blue;
var onAccentBrush = this.TryFindResource("AdaptiveOnAccentBrush", out var onAccent)
? onAccent as IBrush
: Brushes.White;
dayText.Foreground = onAccentBrush;
var dot = new Border
{
Width = _calendarTodayDotSize,
Height = _calendarTodayDotSize,
CornerRadius = new CornerRadius(_calendarTodayDotSize * 0.5),
Background = accentBrush,
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
Child = dayText,
Tag = "today-dot"
};
Grid.SetRow(dot, row);
Grid.SetColumn(dot, col);
CalendarGrid.Children.Add(dot);
}
else
{
var isWeekend = col is 0 or 6;
dayText.Foreground = isWeekend
? GetThemeBrush("AdaptiveTextSecondaryBrush", 0.78)
: GetThemeBrush("AdaptiveTextPrimaryBrush", 0.94);
Grid.SetRow(dayText, row);
Grid.SetColumn(dayText, col);
CalendarGrid.Children.Add(dayText);
}
}
}
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
var scale = ResolveScale();
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(28 * scale, 14, 40));
RootBorder.Padding = new Thickness(Math.Clamp(14 * scale, 8, 22));
LayoutRoot.RowSpacing = Math.Clamp(10 * scale, 5, 16);
HeaderTextBlock.FontSize = Math.Clamp(42 * scale, 14, 58);
var weekdayFontSize = Math.Clamp(20 * scale, 8, 26);
foreach (var block in GetWeekdayHeaderBlocks())
{
block.FontSize = weekdayFontSize;
}
_calendarDayFontSize = Math.Clamp(22 * scale, 8, 30);
_calendarTodayDotSize = Math.Clamp(44 * scale, 16, 58);
UpdateCalendar();
}
private double ResolveScale()
{
var cellScale = Math.Clamp(_currentCellSize / 44d, 0.65, 1.85);
var heightScale = Bounds.Height > 1 ? Math.Clamp(Bounds.Height / 280d, 0.60, 1.90) : 1;
var widthScale = Bounds.Width > 1 ? Math.Clamp(Bounds.Width / 280d, 0.60, 1.90) : 1;
return Math.Clamp(Math.Min(cellScale, Math.Min(heightScale, widthScale) * 1.06), 0.60, 1.85);
}
private IBrush GetThemeBrush(string key, double opacity)
{
if (this.TryFindResource(key, out var value) && value is IBrush brush)
{
if (brush is ISolidColorBrush solid)
{
return new SolidColorBrush(solid.Color, opacity);
}
return brush;
}
return new SolidColorBrush(Colors.Gray, opacity);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -98,8 +98,10 @@ public partial class MainWindow
var viewportRow = gridMetrics.RowCount > 2 ? 1 : 0;
var viewportRowSpan = gridMetrics.RowCount > 2 ? gridMetrics.RowCount - 2 : 1;
var pageWidth = Math.Max(1, gridMetrics.ColumnCount * gridMetrics.CellSize);
var pageHeight = Math.Max(1, viewportRowSpan * gridMetrics.CellSize);
var pageWidth = Math.Max(1, gridMetrics.GridWidthPx);
var pageHeight = Math.Max(
1,
viewportRowSpan * gridMetrics.CellSize + Math.Max(0, viewportRowSpan - 1) * gridMetrics.GapPx);
Grid.SetRow(DesktopPagesViewport, viewportRow);
Grid.SetColumn(DesktopPagesViewport, 0);
@@ -137,6 +139,8 @@ public partial class MainWindow
{
Width = pageWidth,
Height = pageHeight,
RowSpacing = gridMetrics.GapPx,
ColumnSpacing = gridMetrics.GapPx,
Background = Brushes.Transparent,
ShowGridLines = false
};
@@ -309,6 +313,16 @@ public partial class MainWindow
return;
}
// 如果在组件编辑模式下点击空白区域,取消组件选中
if (_isComponentLibraryOpen && _selectedDesktopComponentHost is not null)
{
if (!IsInteractivePointerSource(e.Source))
{
ClearDesktopComponentSelection();
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
}
if (!CanSwipeDesktopSurface())
{
return;
@@ -519,7 +533,7 @@ public partial class MainWindow
Classes = { "glass-panel" },
BorderThickness = new Thickness(0),
Margin = new Thickness(0, 0, 12, 12),
CornerRadius = new CornerRadius(12),
CornerRadius = new CornerRadius(20),
Child = panel
// 不设置固定 Width 和 Height由 UpdateLauncherTileLayout 动态设置
};
@@ -598,7 +612,7 @@ public partial class MainWindow
Classes = { "glass-panel" },
Margin = new Thickness(0, 0, 12, 12),
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(12),
CornerRadius = new CornerRadius(20),
Padding = new Thickness(10),
Content = content
// 不设置固定 Width 和 Height由 UpdateLauncherTileLayout 动态设置

View File

@@ -1,6 +1,9 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Layout;
using FluentIcons.Avalonia;
using FluentIcons.Common;
namespace LanMontainDesktop.Views;
@@ -59,11 +62,11 @@ public partial class MainWindow
WallpaperPreviewBackButtonTextBlock.Text = L("button.back_to_windows", "Back to Windows");
ToolTip.SetTip(BackToWindowsButton, L("tooltip.back_to_windows", "Back to Windows"));
OpenComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
GridPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop");
ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "Edit Desktop"));
ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Edit Desktop");
OpenComponentLibraryTextBlock.Text = L("button.component_library", "编辑桌面");
WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "编辑桌面");
GridPreviewComponentLibraryTextBlock.Text = L("button.component_library", "编辑桌面");
ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "编辑桌面"));
ComponentLibraryTitleTextBlock.Text = L("component_library.title", "小组件");
ToolTip.SetTip(CloseComponentLibraryButton, L("common.close", "Close"));
ComponentLibraryEmptyTextBlock.Text = L(
"component_library.empty",
@@ -88,17 +91,55 @@ public partial class MainWindow
ClearWallpaperButton.Content = L("settings.wallpaper.clear_button", "重置");
GridPanelTitleTextBlock.Text = L("settings.grid.title", "Grid Layout");
GridSpacingPresetLabelTextBlock.Text = L("settings.grid.spacing_label", "Grid Spacing");
GridSpacingRelaxedComboBoxItem.Content = L("settings.grid.spacing_relaxed", "Relaxed");
GridSpacingCompactComboBoxItem.Content = L("settings.grid.spacing_compact", "Compact");
GridEdgeInsetLabelTextBlock.Text = L("settings.grid.edge_inset_label", "Screen Inset");
ApplyGridButton.Content = L("settings.grid.apply_button", "Apply");
UpdateGridEdgeInsetComputedPxText(_currentDesktopCellSize);
ColorPanelTitleTextBlock.Text = L("settings.color.title", "Color");
ThemeModeSettingsExpander.Header = L("settings.color.day_night_label", "Day/Night");
NightModeToggleSwitch.OnContent = L("settings.color.day_night_on", "Night");
NightModeToggleSwitch.OffContent = L("settings.color.day_night_off", "Day");
NightModeToggleSwitch.OffContent = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 6,
Children =
{
new SymbolIcon { Symbol = Symbol.WeatherSunny, IconVariant = IconVariant.Regular, FontSize = 14 },
new TextBlock
{
Text = L("settings.color.day_night_off", "Day"),
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
}
}
};
NightModeToggleSwitch.OnContent = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 6,
Children =
{
new SymbolIcon { Symbol = Symbol.WeatherMoon, IconVariant = IconVariant.Regular, FontSize = 14 },
new TextBlock
{
Text = L("settings.color.day_night_on", "Night"),
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
}
}
};
RecommendedColorsLabelTextBlock.Text = L("settings.color.recommended_label", "Recommended Colors");
SystemMonetColorsLabelTextBlock.Text = L("settings.color.system_monet_label", "System Monet Colors");
RefreshMonetColorsButton.Content = L("settings.color.refresh_button", "Refresh");
StatusBarPanelTitleTextBlock.Text = L("settings.status_bar.title", "Status Bar");
StatusBarClockSettingsExpander.Header = L("settings.status_bar.clock_header", "Clock");
StatusBarSpacingSettingsExpander.Header = L("settings.status_bar.spacing_header", "Component Spacing");
StatusBarSpacingSettingsExpander.Description = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
StatusBarSpacingModeCompactItem.Content = L("settings.status_bar.spacing_mode_compact", "Compact");
StatusBarSpacingModeRelaxedItem.Content = L("settings.status_bar.spacing_mode_relaxed", "Relaxed");
StatusBarSpacingModeCustomItem.Content = L("settings.status_bar.spacing_mode_custom", "Custom");
StatusBarSpacingCustomLabelTextBlock.Text = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
RegionPanelTitleTextBlock.Text = L("settings.region.title", "Region");
LanguageSettingsExpander.Header = L("settings.region.language_header", "Language");

View File

@@ -1,6 +1,7 @@
using System;
using FluentIcons.Avalonia;
using FluentIcons.Common;
using LanMontainDesktop.Views.Components;
using System.Collections.Generic;
using System.IO;
@@ -60,7 +61,8 @@ public partial class MainWindow
WallpaperSettingsPanel is null ||
ColorSettingsPanel is null ||
StatusBarSettingsPanel is null ||
RegionSettingsPanel is null)
RegionSettingsPanel is null ||
AboutSettingsPanel is null)
{
return;
}
@@ -71,6 +73,7 @@ public partial class MainWindow
ColorSettingsPanel.IsVisible = selectedIndex == 2;
StatusBarSettingsPanel.IsVisible = selectedIndex == 3;
RegionSettingsPanel.IsVisible = selectedIndex == 4;
AboutSettingsPanel.IsVisible = selectedIndex == 5;
if (selectedIndex == 1)
{
@@ -633,6 +636,8 @@ public partial class MainWindow
var snapshot = new AppSettingsSnapshot
{
GridShortSideCells = _targetShortSideCells,
GridSpacingPreset = _gridSpacingPreset,
DesktopEdgeInsetPercent = _desktopEdgeInsetPercent,
IsNightMode = _isNightMode,
ThemeColor = _selectedThemeColor.ToString(),
WallpaperPath = _wallpaperPath,
@@ -644,6 +649,9 @@ public partial class MainWindow
PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(),
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
TaskbarLayoutMode = _taskbarLayoutMode,
ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond",
StatusBarSpacingMode = _statusBarSpacingMode,
StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent,
DesktopPageCount = _desktopPageCount,
CurrentDesktopSurfaceIndex = _currentDesktopSurfaceIndex,
DesktopComponentPlacements = _desktopComponentPlacements.ToList()
@@ -652,6 +660,23 @@ public partial class MainWindow
_appSettingsService.Save(snapshot);
}
private IDisposable? _persistSettingsDebounceTimer;
private void SchedulePersistSettings(int delayMs = 200)
{
if (_suppressSettingsPersistence)
{
return;
}
_persistSettingsDebounceTimer?.Dispose();
_persistSettingsDebounceTimer = DispatcherTimer.RunOnce(() =>
{
_persistSettingsDebounceTimer = null;
PersistSettings();
}, TimeSpan.FromMilliseconds(Math.Max(0, delayMs)));
}
private void UpdateAdaptiveTextSystem()
{
var isLightBackground = _isSettingsOpen
@@ -980,8 +1005,13 @@ public partial class MainWindow
UpdateAdaptiveTextSystem();
ApplyWallpaperBrush();
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
if (_settingsContentPanelTransform is not null)
{
_settingsContentPanelTransform.Y = 30;
}
SettingsPage.IsVisible = true;
SettingsPage.Opacity = 0;
UpdateSettingsViewportInsets(Math.Max(1, _currentDesktopCellSize));
UpdateWallpaperPreviewLayout();
@@ -992,6 +1022,10 @@ public partial class MainWindow
return;
}
if (_settingsContentPanelTransform is not null)
{
_settingsContentPanelTransform.Y = 0;
}
SettingsPage.Opacity = 1;
}, DispatcherPriority.Background);
}
@@ -1011,10 +1045,18 @@ public partial class MainWindow
if (immediate)
{
SettingsPage.Opacity = 0;
if (_settingsContentPanelTransform is not null)
{
_settingsContentPanelTransform.Y = 30;
}
SettingsPage.IsVisible = false;
return;
}
if (_settingsContentPanelTransform is not null)
{
_settingsContentPanelTransform.Y = 30;
}
SettingsPage.Opacity = 0;
DispatcherTimer.RunOnce(() =>
@@ -1059,6 +1101,15 @@ public partial class MainWindow
};
}
if (StatusBarSpacingSettingsExpander is not null)
{
StatusBarSpacingSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = Symbol.TextLineSpacing,
IconVariant = variant
};
}
if (LanguageSettingsExpander is not null)
{
LanguageSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource

View File

@@ -126,7 +126,7 @@
Grid.Column="1"
Classes="glass-panel"
ClipToBounds="False"
CornerRadius="18"
CornerRadius="36"
Padding="18">
<Grid RowDefinitions="Auto,*">
<StackPanel Spacing="4">
@@ -160,7 +160,7 @@
Margin="52"
MaxWidth="760"
MaxHeight="520"
CornerRadius="18"
CornerRadius="36"
Padding="14">
<Border.RenderTransform>
<TranslateTransform Y="42" />
@@ -232,12 +232,13 @@
</Border>
<Border x:Name="BottomTaskbarContainer"
Classes="glass-strong"
Classes="glass-island"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="1"
Margin="4"
CornerRadius="18"
HorizontalAlignment="Stretch"
Margin="0"
CornerRadius="36"
Padding="6">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="8">
@@ -374,14 +375,14 @@
</Border.RenderTransform>
<Border Classes="mica-strong"
CornerRadius="14"
CornerRadius="32"
Padding="18">
<Grid ColumnDefinitions="220,*"
ColumnSpacing="16">
<Border x:Name="SettingsNavPanelBorder"
Classes="glass-panel"
Grid.Column="0"
CornerRadius="10"
CornerRadius="28"
Padding="10">
<Border.Styles>
<Style Selector="ListBox#SettingsNavListBox">
@@ -394,7 +395,7 @@
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="10,8" />
<Setter Property="Margin" Value="0,2" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="CornerRadius" Value="12" />
</Style>
<Style Selector="ListBox#SettingsNavListBox ListBoxItem:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemHoverBackgroundBrush}" />
@@ -443,13 +444,19 @@
<TextBlock x:Name="SettingsNavRegionTextBlock" Text="&#22320;&#21306;" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
<ListBoxItem x:Name="SettingsNavAboutItem" ToolTip.Tip="&#20851;&#20110;">
<StackPanel Orientation="Horizontal" Spacing="12">
<fi:SymbolIcon x:Name="SettingsNavAboutIcon" Symbol="Info" IconVariant="Regular" />
<TextBlock x:Name="SettingsNavAboutTextBlock" Text="&#20851;&#20110;" VerticalAlignment="Center" />
</StackPanel>
</ListBoxItem>
</ListBox>
</StackPanel>
</Border>
<Border Grid.Column="1"
Classes="glass-panel"
CornerRadius="10"
CornerRadius="20"
Padding="14">
<Grid>
<Grid x:Name="WallpaperSettingsPanel"
@@ -468,17 +475,17 @@
<Border x:Name="WallpaperPreviewHost"
Grid.Row="1" Grid.Column="0"
Margin="0,0,16,0"
VerticalAlignment="Top"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
<!-- Monitor Frame (Bezel) -->
<Border x:Name="WallpaperPreviewFrame"
HorizontalAlignment="Stretch"
CornerRadius="14"
CornerRadius="28"
Background="#FF1A1A1A"
Padding="12">
<Border x:Name="WallpaperPreviewViewport"
ClipToBounds="True"
CornerRadius="4"
CornerRadius="12"
Background="#30111827">
<Grid>
<vlc:VideoView x:Name="WallpaperPreviewVideoView"
@@ -497,13 +504,8 @@
<StackPanel x:Name="WallpaperPreviewTopStatusComponentsPanel"
Orientation="Horizontal"
Spacing="3">
<Border x:Name="WallpaperPreviewClockContainer"
IsVisible="False">
<TextBlock x:Name="WallpaperPreviewClockTextBlock"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="12:34" />
</Border>
<comp:ClockWidget x:Name="WallpaperPreviewClockWidget"
IsVisible="False" />
</StackPanel>
</Border>
@@ -511,7 +513,7 @@
Classes="glass-strong"
Grid.Row="1"
Margin="3"
CornerRadius="8"
CornerRadius="16"
Padding="2">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="3">
@@ -619,15 +621,18 @@
HorizontalAlignment="Stretch">
<Border x:Name="GridPreviewFrame"
HorizontalAlignment="Stretch"
CornerRadius="14"
CornerRadius="28"
Background="#FF1A1A1A"
Padding="12">
<Border x:Name="GridPreviewViewport"
ClipToBounds="True"
CornerRadius="4"
CornerRadius="16"
Background="#30111827">
<Panel>
<Canvas x:Name="GridPreviewLinesCanvas" />
<Canvas x:Name="GridPreviewLinesCanvas"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsHitTestVisible="False" />
<Grid x:Name="GridPreviewGrid"
HorizontalAlignment="Center"
VerticalAlignment="Center">
@@ -645,7 +650,7 @@
Classes="glass-strong"
Grid.Row="1"
Margin="3"
CornerRadius="8"
CornerRadius="16"
Padding="2">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="3">
@@ -701,6 +706,47 @@
Value="12" />
</Grid>
<TextBlock x:Name="GridSpacingPresetLabelTextBlock"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="Spacing" />
<ComboBox x:Name="GridSpacingPresetComboBox"
Width="220"
SelectionChanged="OnGridSpacingPresetSelectionChanged">
<ComboBoxItem x:Name="GridSpacingRelaxedComboBoxItem" Tag="Relaxed" Content="Relaxed" />
<ComboBoxItem x:Name="GridSpacingCompactComboBoxItem" Tag="Compact" Content="Compact" />
</ComboBox>
<TextBlock x:Name="GridEdgeInsetLabelTextBlock"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="Screen Inset" />
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
<Slider x:Name="GridEdgeInsetSlider"
Grid.Column="0"
Minimum="0"
Maximum="30"
TickFrequency="1"
TickPlacement="None"
Value="18"
ValueChanged="OnGridEdgeInsetSliderChanged" />
<ui:NumberBox x:Name="GridEdgeInsetNumberBox"
Grid.Column="1"
Width="80"
Minimum="0"
Maximum="30"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Value="18" />
</Grid>
<TextBlock x:Name="GridEdgeInsetComputedPxTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="≈ 0 px" />
<Button x:Name="ApplyGridButton"
HorizontalAlignment="Stretch"
Padding="0,10"
@@ -762,7 +808,7 @@
<Border x:Name="RecommendedColorSwatch1"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton2"
@@ -773,7 +819,7 @@
<Border x:Name="RecommendedColorSwatch2"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton3"
@@ -784,7 +830,7 @@
<Border x:Name="RecommendedColorSwatch3"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton4"
@@ -795,7 +841,7 @@
<Border x:Name="RecommendedColorSwatch4"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton5"
@@ -806,7 +852,7 @@
<Border x:Name="RecommendedColorSwatch5"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="RecommendedColorButton6"
@@ -817,7 +863,7 @@
<Border x:Name="RecommendedColorSwatch6"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
</WrapPanel>
@@ -852,7 +898,7 @@
<Border x:Name="MonetColorSwatch1"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton2"
@@ -863,7 +909,7 @@
<Border x:Name="MonetColorSwatch2"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton3"
@@ -874,7 +920,7 @@
<Border x:Name="MonetColorSwatch3"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton4"
@@ -885,7 +931,7 @@
<Border x:Name="MonetColorSwatch4"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton5"
@@ -896,7 +942,7 @@
<Border x:Name="MonetColorSwatch5"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
<Button x:Name="MonetColorButton6"
@@ -907,7 +953,7 @@
<Border x:Name="MonetColorSwatch6"
Width="26"
Height="26"
CornerRadius="6"
CornerRadius="12"
BorderThickness="0" />
</Button>
</WrapPanel>
@@ -932,7 +978,8 @@
<ui:SettingsExpander x:Name="StatusBarClockSettingsExpander"
Header="&#26102;&#38388;&#32452;&#20214;"
Description="&#22312;&#39030;&#37096;&#29366;&#24577;&#26639;&#26174;&#31034;&#26102;&#38047;&#12290;">
Description="&#22312;&#39030;&#37096;&#29366;&#24577;&#26639;&#26174;&#31034;&#26102;&#38047;&#12290;"
IsExpanded="False">
<ui:SettingsExpander.IconSource>
</ui:SettingsExpander.IconSource>
@@ -943,6 +990,72 @@
Checked="OnStatusBarClockChecked"
Unchecked="OnStatusBarClockUnchecked" />
</ui:SettingsExpander.Footer>
<StackPanel Margin="0,8,0,0" Spacing="12">
<TextBlock Text="显示格式" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<StackPanel Orientation="Horizontal" Spacing="8">
<RadioButton x:Name="ClockFormatHMSSRadio"
Content="时分秒 (HH:mm:ss)"
GroupName="ClockFormat"
Checked="OnClockFormatChanged"
Tag="Hms" />
<RadioButton x:Name="ClockFormatHMRadio"
Content="时分 (HH:mm)"
GroupName="ClockFormat"
Checked="OnClockFormatChanged"
Tag="Hm" />
</StackPanel>
</StackPanel>
</ui:SettingsExpander>
<ui:SettingsExpander x:Name="StatusBarSpacingSettingsExpander"
Header="Component spacing"
Description="Adjust spacing between status bar components."
IsExpanded="False">
<ui:SettingsExpander.IconSource>
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox x:Name="StatusBarSpacingModeComboBox"
Width="220"
SelectionChanged="OnStatusBarSpacingModeChanged">
<ComboBoxItem x:Name="StatusBarSpacingModeCompactItem" Tag="Compact" Content="Compact" />
<ComboBoxItem x:Name="StatusBarSpacingModeRelaxedItem" Tag="Relaxed" Content="Relaxed" />
<ComboBoxItem x:Name="StatusBarSpacingModeCustomItem" Tag="Custom" Content="Custom" />
</ComboBox>
</ui:SettingsExpander.Footer>
<StackPanel x:Name="StatusBarSpacingCustomPanel"
Margin="0,8,0,0"
Spacing="12"
IsVisible="False">
<TextBlock x:Name="StatusBarSpacingCustomLabelTextBlock"
FontSize="14"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="Custom spacing" />
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
<Slider x:Name="StatusBarSpacingSlider"
Grid.Column="0"
Minimum="0"
Maximum="30"
TickFrequency="1"
TickPlacement="None"
Value="12"
ValueChanged="OnStatusBarSpacingSliderChanged" />
<ui:NumberBox x:Name="StatusBarSpacingNumberBox"
Grid.Column="1"
Width="80"
Minimum="0"
Maximum="30"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Value="12" />
</Grid>
<TextBlock x:Name="StatusBarSpacingComputedPxTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="≈ 0 px" />
</StackPanel>
</ui:SettingsExpander>
</StackPanel>
@@ -983,6 +1096,19 @@
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
<StackPanel x:Name="AboutSettingsPanel" IsVisible="False" Spacing="20">
<TextBlock x:Name="AboutPanelTitleTextBlock" FontSize="24" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Text="关于" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" CornerRadius="20" Padding="20">
<StackPanel Spacing="12">
<TextBlock Text="LanMontainDesktop" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock Text="现代化桌面壳层应用" FontSize="13" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<Separator Background="{DynamicResource AdaptiveButtonBorderBrush}" Margin="0,8" />
<TextBlock x:Name="VersionTextBlock" Text="版本号: 1.0.0" FontSize="13" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="CodeNameTextBlock" Text="版本代号: Administrate" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveAccentBrush}" />
</StackPanel>
</Border>
</StackPanel>
</Grid>
</Border>
</Grid>
@@ -990,6 +1116,47 @@
</Border>
</Grid>
<Border x:Name="ComponentSettingsWindow"
IsVisible="False"
Opacity="0"
Classes="glass-strong"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Width="400"
MinWidth="300"
MaxWidth="500"
Height="300"
MinHeight="200"
Margin="24,24,24,100"
CornerRadius="36"
Padding="0">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0"
Background="{DynamicResource AdaptiveAccentBrush}"
CornerRadius="36,36,0,0"
Padding="16,12">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="组件设置"
FontSize="16"
FontWeight="SemiBold"
Foreground="White"
VerticalAlignment="Center" />
<Button Grid.Column="1"
Padding="8"
Background="Transparent"
BorderThickness="0"
Click="OnCloseComponentSettingsClick">
<fi:FluentIcon Icon="Dismiss"
FontSize="14"
Foreground="White" />
</Button>
</Grid>
</Border>
<ContentControl x:Name="ComponentSettingsContentHost"
Grid.Row="1" />
</Grid>
</Border>
<Border x:Name="ComponentLibraryWindow"
IsVisible="False"
Opacity="0"
@@ -1002,8 +1169,11 @@
Height="260"
MinHeight="220"
Margin="24,24,24,100"
CornerRadius="18"
Padding="14">
CornerRadius="36"
Padding="14"
PointerPressed="OnComponentLibraryWindowPointerPressed"
PointerMoved="OnComponentLibraryWindowPointerMoved"
PointerReleased="OnComponentLibraryWindowPointerReleased">
<Border.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.2" />
@@ -1018,7 +1188,7 @@
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Text="&#32452;&#20214;&#24211;" />
Text="小组件" />
<Button x:Name="CloseComponentLibraryButton"
Grid.Column="1"
Padding="8"

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using FluentAvalonia.UI.Controls;
using Avalonia.Layout;
@@ -22,6 +23,7 @@ using LanMontainDesktop.ComponentSystem.Extensions;
using LanMontainDesktop.Models;
using LanMontainDesktop.Services;
using LanMontainDesktop.Theme;
using LanMontainDesktop.Views.Components;
using LibVLCSharp.Shared;
namespace LanMontainDesktop.Views;
@@ -47,6 +49,9 @@ public partial class MainWindow : Window
private const int StatusBarRowIndex = 0;
private const int MinShortSideCells = 6;
private const int MaxShortSideCells = 96;
private const int MinEdgeInsetPercent = 0;
private const int MaxEdgeInsetPercent = 30;
private const int DefaultEdgeInsetPercent = 18;
private const int SettingsTransitionDurationMs = 240;
private const double WallpaperPreviewMaxWidth = 520;
private const double LightBackgroundLuminanceThreshold = 0.57;
@@ -64,7 +69,17 @@ public partial class MainWindow : Window
TaskbarActionId.MinimizeToWindows,
TaskbarActionId.OpenSettings
];
private readonly record struct GridMetrics(int ColumnCount, int RowCount, double CellSize);
private readonly record struct GridMetrics(
int ColumnCount,
int RowCount,
double CellSize,
double GapPx,
double EdgeInsetPx,
double GridWidthPx,
double GridHeightPx)
{
public double Pitch => CellSize + GapPx;
}
private readonly MonetColorService _monetColorService = new();
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
@@ -87,6 +102,7 @@ public partial class MainWindow : Window
private bool _suppressSettingsPersistence;
private bool _isUpdatingWallpaperPreviewLayout;
private bool _isComponentLibraryOpen;
private Border? _selectedDesktopComponentHost;
private bool _reopenSettingsAfterComponentLibraryClose;
private TranslateTransform? _settingsContentPanelTransform;
private IBrush? _defaultDesktopBackground;
@@ -104,8 +120,20 @@ public partial class MainWindow : Window
private IReadOnlyList<Color> _monetColors = Array.Empty<Color>();
private Color _selectedThemeColor = Color.Parse("#FF3B82F6");
private double _currentDesktopCellSize;
private double _currentDesktopCellGap;
private double _currentDesktopEdgeInset;
private string _gridSpacingPreset = "Relaxed";
private string _statusBarSpacingMode = "Relaxed";
private int _statusBarCustomSpacingPercent = 12;
private bool _suppressGridSpacingEvents;
private bool _suppressGridInsetEvents;
private bool _suppressStatusBarSpacingEvents;
private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent;
private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle;
private string _languageCode = "zh-CN";
private ClockDisplayFormat _clockDisplayFormat = ClockDisplayFormat.HourMinuteSecond;
private double CurrentDesktopPitch => _currentDesktopCellSize + _currentDesktopCellGap;
public MainWindow()
{
@@ -131,12 +159,40 @@ public partial class MainWindow : Window
snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
MinShortSideCells,
MaxShortSideCells);
_gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
_suppressGridSpacingEvents = true;
GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
_suppressGridSpacingEvents = false;
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
_suppressGridInsetEvents = true;
GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
_suppressGridInsetEvents = false;
GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged;
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
_statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
_suppressStatusBarSpacingEvents = true;
StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch
{
"Compact" => 0,
"Custom" => 2,
_ => 1
};
StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
_suppressStatusBarSpacingEvents = false;
StatusBarSpacingNumberBox.ValueChanged += OnStatusBarSpacingNumberBoxChanged;
GridSizeNumberBox.Value = _targetShortSideCells;
GridSizeSlider.Value = _targetShortSideCells;
GridSizeSlider.ValueChanged += OnGridSizeSliderChanged;
GridSizeNumberBox.ValueChanged += OnGridSizeNumberBoxChanged;
SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 4);
SettingsNavListBox.SelectedIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, 5);
UpdateSettingsTabContent();
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
@@ -199,6 +255,8 @@ public partial class MainWindow : Window
GridPreviewHost.SizeChanged -= OnGridPreviewHostSizeChanged;
GridSizeSlider.ValueChanged -= OnGridSizeSliderChanged;
GridSizeNumberBox.ValueChanged -= OnGridSizeNumberBoxChanged;
GridEdgeInsetNumberBox.ValueChanged -= OnGridEdgeInsetNumberBoxChanged;
StatusBarSpacingNumberBox.ValueChanged -= OnStatusBarSpacingNumberBoxChanged;
base.OnClosed(e);
}
@@ -245,12 +303,152 @@ public partial class MainWindow : Window
UpdateGridPreviewLayout();
}
private void OnGridEdgeInsetSliderChanged(object? sender, RoutedEventArgs e)
{
if (_suppressGridInsetEvents)
{
return;
}
var value = (int)Math.Round(GridEdgeInsetSlider.Value);
SetPendingGridEdgeInsetPercent(value, updateSlider: false, updateNumberBox: true);
UpdateGridPreviewLayout();
}
private void OnGridEdgeInsetNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
{
if (_suppressGridInsetEvents)
{
return;
}
var value = (int)Math.Round(GridEdgeInsetNumberBox.Value);
SetPendingGridEdgeInsetPercent(value, updateSlider: true, updateNumberBox: false);
UpdateGridPreviewLayout();
}
private void SetPendingGridEdgeInsetPercent(int percent, bool updateSlider, bool updateNumberBox)
{
var clamped = Math.Clamp(percent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
_suppressGridInsetEvents = true;
try
{
if (updateSlider && Math.Abs(GridEdgeInsetSlider.Value - clamped) > double.Epsilon)
{
GridEdgeInsetSlider.Value = clamped;
}
if (updateNumberBox && Math.Abs(GridEdgeInsetNumberBox.Value - clamped) > double.Epsilon)
{
GridEdgeInsetNumberBox.Value = clamped;
}
}
finally
{
_suppressGridInsetEvents = false;
}
}
private void OnGridSpacingPresetSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressGridSpacingEvents)
{
return;
}
UpdateGridPreviewLayout();
}
private void OnStatusBarSpacingModeChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressStatusBarSpacingEvents)
{
return;
}
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(
TryGetSelectedComboBoxTag(StatusBarSpacingModeComboBox) ?? _statusBarSpacingMode);
StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
ApplyDesktopStatusBarComponentSpacing();
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
SchedulePersistSettings();
}
private void OnStatusBarSpacingSliderChanged(object? sender, RangeBaseValueChangedEventArgs e)
{
if (_suppressStatusBarSpacingEvents)
{
return;
}
var percent = (int)Math.Round(StatusBarSpacingSlider.Value);
SetStatusBarCustomSpacingPercent(percent, updateSlider: false, updateNumberBox: true);
if (string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase))
{
ApplyDesktopStatusBarComponentSpacing();
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
}
SchedulePersistSettings();
}
private void OnStatusBarSpacingNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e)
{
if (_suppressStatusBarSpacingEvents)
{
return;
}
var percent = (int)Math.Round(StatusBarSpacingNumberBox.Value);
SetStatusBarCustomSpacingPercent(percent, updateSlider: true, updateNumberBox: false);
if (string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase))
{
ApplyDesktopStatusBarComponentSpacing();
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
}
SchedulePersistSettings();
}
private void SetStatusBarCustomSpacingPercent(int percent, bool updateSlider, bool updateNumberBox)
{
percent = Math.Clamp(percent, 0, 30);
_statusBarCustomSpacingPercent = percent;
_suppressStatusBarSpacingEvents = true;
try
{
if (updateSlider && Math.Abs(StatusBarSpacingSlider.Value - percent) > double.Epsilon)
{
StatusBarSpacingSlider.Value = percent;
}
if (updateNumberBox && Math.Abs(StatusBarSpacingNumberBox.Value - percent) > double.Epsilon)
{
StatusBarSpacingNumberBox.Value = percent;
}
}
finally
{
_suppressStatusBarSpacingEvents = false;
}
}
private void UpdateGridPreviewLayout()
{
if (GridPreviewFrame is null ||
GridPreviewHost is null ||
GridPreviewViewport is null ||
GridPreviewGrid is null)
GridPreviewGrid is null ||
GridPreviewLinesCanvas is null)
{
return;
}
@@ -279,14 +477,24 @@ public partial class MainWindow : Window
var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding);
var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding);
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells);
var preset = NormalizeGridSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
var gapRatio = ResolveGridGapRatio(preset);
var pendingEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
var edgeInset = CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent);
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0)
{
return;
}
GridPreviewGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize;
GridPreviewGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize;
var inset = new Thickness(gridMetrics.EdgeInsetPx);
GridPreviewGrid.Margin = inset;
GridPreviewGrid.RowSpacing = gridMetrics.GapPx;
GridPreviewGrid.ColumnSpacing = gridMetrics.GapPx;
GridPreviewGrid.Width = gridMetrics.GridWidthPx;
GridPreviewGrid.Height = gridMetrics.GridHeightPx;
GridPreviewLinesCanvas.Margin = inset;
GridPreviewGrid.RowDefinitions.Clear();
GridPreviewGrid.ColumnDefinitions.Clear();
@@ -316,6 +524,8 @@ public partial class MainWindow : Window
Grid.SetColumnSpan(GridPreviewBottomTaskbarContainer, gridMetrics.ColumnCount);
ApplyGridPreviewWidgetSizing(gridMetrics.CellSize);
ApplyStatusBarComponentSpacingForPanel(GridPreviewTopStatusComponentsPanel, gridMetrics.CellSize);
UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize);
GridInfoTextBlock.Text = Lf(
"settings.grid.info_format",
@@ -344,21 +554,19 @@ public partial class MainWindow : Window
GridPreviewLinesCanvas.Children.Clear();
var cellSize = gridMetrics.CellSize;
var gridWidth = gridMetrics.ColumnCount * cellSize;
var gridHeight = gridMetrics.RowCount * cellSize;
var pitch = gridMetrics.Pitch;
var gridWidth = gridMetrics.GridWidthPx;
var gridHeight = gridMetrics.GridHeightPx;
GridPreviewLinesCanvas.Width = gridWidth;
GridPreviewLinesCanvas.Height = gridHeight;
Canvas.SetLeft(GridPreviewLinesCanvas, 0);
Canvas.SetTop(GridPreviewLinesCanvas, 0);
var dashLength = cellSize * 0.3;
var gapLength = cellSize * 0.2;
for (var row = 0; row <= gridMetrics.RowCount; row++)
{
var y = row * cellSize;
var y = row == gridMetrics.RowCount ? gridHeight : row * pitch;
var line = new Line
{
StartPoint = new Point(0, y),
@@ -373,7 +581,7 @@ public partial class MainWindow : Window
for (var col = 0; col <= gridMetrics.ColumnCount; col++)
{
var x = col * cellSize;
var x = col == gridMetrics.ColumnCount ? gridWidth : col * pitch;
var line = new Line
{
StartPoint = new Point(x, 0),
@@ -389,13 +597,12 @@ public partial class MainWindow : Window
private void ApplyGridPreviewWidgetSizing(double cellSize)
{
var margin = Math.Clamp(cellSize * 0.08, 1, 6);
var previewTaskbarCell = Math.Clamp(cellSize, 10, 36);
var previewTaskbarCell = Math.Clamp(cellSize * 0.74, 10, 30);
var iconSize = Math.Clamp(cellSize * 0.35, 8, 16);
GridPreviewTopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1, 4));
GridPreviewBottomTaskbarContainer.Margin = new Thickness(margin);
GridPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.22, 4, 10));
GridPreviewTopStatusBarHost.Padding = new Thickness(0);
GridPreviewBottomTaskbarContainer.Margin = new Thickness(0);
GridPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 16, 32));
GridPreviewBottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 1, 4));
GridPreviewBackButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13);
@@ -411,6 +618,10 @@ public partial class MainWindow : Window
private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e)
{
_gridSpacingPreset = NormalizeGridSpacingPreset(
TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset);
_desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent();
var requested = (int)Math.Round(GridSizeNumberBox.Value);
if (requested <= 0)
{
@@ -429,26 +640,56 @@ public partial class MainWindow : Window
GridSizeSlider.Value = _targetShortSideCells;
}
SetPendingGridEdgeInsetPercent(_desktopEdgeInsetPercent, updateSlider: true, updateNumberBox: true);
RebuildDesktopGrid();
PersistSettings();
}
private void OnClockFormatChanged(object? sender, RoutedEventArgs e)
{
if (sender is not RadioButton radioButton || radioButton.Tag is not string formatTag)
{
return;
}
_clockDisplayFormat = formatTag == "Hm"
? ClockDisplayFormat.HourMinute
: ClockDisplayFormat.HourMinuteSecond;
if (ClockWidget is ClockWidget clock)
{
clock.SetDisplayFormat(_clockDisplayFormat);
}
ApplyTopStatusComponentVisibility();
UpdateWallpaperPreviewLayout();
PersistSettings();
}
private void RebuildDesktopGrid()
{
var gridMetrics = CalculateGridMetrics(
DesktopHost.Bounds.Width,
DesktopHost.Bounds.Height,
_targetShortSideCells);
var hostWidth = DesktopHost.Bounds.Width;
var hostHeight = DesktopHost.Bounds.Height;
var gapRatio = ResolveGridGapRatio(_gridSpacingPreset);
var edgeInset = CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
var gridMetrics = CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0)
{
return;
}
_currentDesktopCellSize = gridMetrics.CellSize;
_currentDesktopCellGap = gridMetrics.GapPx;
_currentDesktopEdgeInset = gridMetrics.EdgeInsetPx;
UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize);
DesktopGrid.RowDefinitions.Clear();
DesktopGrid.ColumnDefinitions.Clear();
DesktopGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize;
DesktopGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize;
DesktopGrid.Margin = new Thickness(gridMetrics.EdgeInsetPx);
DesktopGrid.RowSpacing = gridMetrics.GapPx;
DesktopGrid.ColumnSpacing = gridMetrics.GapPx;
DesktopGrid.Width = gridMetrics.GridWidthPx;
DesktopGrid.Height = gridMetrics.GridHeightPx;
for (var row = 0; row < gridMetrics.RowCount; row++)
{
@@ -476,6 +717,7 @@ public partial class MainWindow : Window
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
ApplyWidgetSizing(gridMetrics.CellSize);
ApplyDesktopStatusBarComponentSpacing();
UpdateDesktopSurfaceLayout(gridMetrics);
UpdateSettingsViewportInsets(gridMetrics.CellSize);
@@ -489,26 +731,190 @@ public partial class MainWindow : Window
UpdateWallpaperPreviewLayout();
}
private static GridMetrics CalculateGridMetrics(double hostWidth, double hostHeight, int targetShortSideCells)
private void ApplyDesktopStatusBarComponentSpacing()
{
ApplyStatusBarComponentSpacingForPanel(TopStatusComponentsPanel, _currentDesktopCellSize);
UpdateStatusBarSpacingComputedPxText(_currentDesktopCellSize);
}
private int ResolveStatusBarSpacingPercent()
{
return _statusBarSpacingMode switch
{
"Compact" => 6,
"Custom" => Math.Clamp(_statusBarCustomSpacingPercent, 0, 30),
_ => 12
};
}
private void ApplyStatusBarComponentSpacingForPanel(StackPanel? panel, double cellSize)
{
if (panel is null)
{
return;
}
var percent = ResolveStatusBarSpacingPercent();
var spacingPx = Math.Max(0, cellSize) * (percent / 100d);
panel.Spacing = spacingPx;
}
private void UpdateStatusBarSpacingComputedPxText(double cellSize)
{
if (StatusBarSpacingComputedPxTextBlock is null)
{
return;
}
var percent = ResolveStatusBarSpacingPercent();
var spacingPx = Math.Max(0, cellSize) * (percent / 100d);
StatusBarSpacingComputedPxTextBlock.Text = Lf(
"settings.status_bar.spacing_custom_px_format",
"鈮?{0:F1}px",
spacingPx);
}
private int ResolvePendingGridEdgeInsetPercent()
{
var pending = (int)Math.Round(GridEdgeInsetNumberBox.Value);
return Math.Clamp(pending, MinEdgeInsetPercent, MaxEdgeInsetPercent);
}
private void UpdateGridEdgeInsetComputedPxText(double cellSize)
{
if (GridEdgeInsetComputedPxTextBlock is null)
{
return;
}
var percent = ResolvePendingGridEdgeInsetPercent();
var insetPx = Math.Clamp(Math.Max(0, cellSize) * (percent / 100d), 0, 80);
GridEdgeInsetComputedPxTextBlock.Text = Lf(
"settings.grid.edge_inset_px_format",
"{0:F1}px",
insetPx);
}
private static string NormalizeGridSpacingPreset(string? value)
{
return string.Equals(value, "Compact", StringComparison.OrdinalIgnoreCase)
? "Compact"
: "Relaxed";
}
private static string NormalizeStatusBarSpacingMode(string? value)
{
return value switch
{
_ when string.Equals(value, "Compact", StringComparison.OrdinalIgnoreCase) => "Compact",
_ when string.Equals(value, "Custom", StringComparison.OrdinalIgnoreCase) => "Custom",
_ => "Relaxed"
};
}
private static string? TryGetSelectedComboBoxTag(ComboBox? comboBox)
{
if (comboBox?.SelectedItem is ComboBoxItem item)
{
return item.Tag?.ToString();
}
return comboBox?.SelectedItem?.ToString();
}
private static double ResolveGridGapRatio(string preset)
{
return string.Equals(preset, "Compact", StringComparison.OrdinalIgnoreCase) ? 0.06 : 0.12;
}
private static double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent)
{
if (hostWidth <= 1 || hostHeight <= 1)
{
return 0;
}
var cells = Math.Max(1, shortSideCells);
var shortSidePx = Math.Max(1, Math.Min(hostWidth, hostHeight));
var baseCell = shortSidePx / cells;
// --- 姣斾緥鍖栫暀鐧?(Proportional Inset) ---
// 鍏佽鐢ㄦ埛鐧惧垎姣旇皟鑺傦紝浣嗚瀹氭洿鍚堢悊鐨勫熀鍑嗗拰闄愬埗
var clampedPercent = Math.Clamp(insetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
var insetRatio = clampedPercent / 100d;
// 纭繚鏈€灏忕暀鐧借兘瀹圭撼涓€瀹氱殑闃村奖鎵╁睍
// 鍏佽 0 杈硅窛锛屾渶澶т笂闄愮淮鎸?80px
return Math.Clamp(baseCell * insetRatio, 0, 80);
}
private static GridMetrics CalculateGridMetrics(
double hostWidth,
double hostHeight,
int shortSideCells,
double gapRatio,
double edgeInsetPx)
{
if (hostWidth <= 1 || hostHeight <= 1)
{
return default;
}
var shortSideCells = Math.Max(1, targetShortSideCells);
var shortSide = Math.Max(1, shortSideCells);
var clampedGapRatio = Math.Max(0, gapRatio);
var inset = Math.Max(0, edgeInsetPx);
// Edge inset should come only from user setting.
// Remaining free space is handled by container centering, not baked into inset.
var availableWidth = Math.Max(1, hostWidth - inset * 2);
var availableHeight = Math.Max(1, hostHeight - inset * 2);
if (hostWidth >= hostHeight)
{
var rowCount = shortSideCells;
var cellSize = hostHeight / rowCount;
var columnCount = Math.Max(1, (int)Math.Floor(hostWidth / cellSize));
return new GridMetrics(columnCount, rowCount, cellSize);
var rowCount = shortSide;
var denominator = rowCount + Math.Max(0, rowCount - 1) * clampedGapRatio;
if (denominator <= 0)
{
return default;
}
var columns = shortSideCells;
var size = hostWidth / columns;
var rows = Math.Max(1, (int)Math.Floor(hostHeight / size));
return new GridMetrics(columns, rows, size);
var cellSize = availableHeight / denominator;
var gapPx = cellSize * clampedGapRatio;
var pitch = cellSize + gapPx;
if (pitch <= 0)
{
return default;
}
var columnCount = Math.Max(1, (int)Math.Floor((availableWidth + gapPx) / pitch));
var gridWidth = columnCount * cellSize + Math.Max(0, columnCount - 1) * gapPx;
var gridHeight = rowCount * cellSize + Math.Max(0, rowCount - 1) * gapPx;
return new GridMetrics(columnCount, rowCount, cellSize, gapPx, inset, gridWidth, gridHeight);
}
else
{
var columnCount = shortSide;
var denominator = columnCount + Math.Max(0, columnCount - 1) * clampedGapRatio;
if (denominator <= 0)
{
return default;
}
var cellSize = availableWidth / denominator;
var gapPx = cellSize * clampedGapRatio;
var pitch = cellSize + gapPx;
if (pitch <= 0)
{
return default;
}
var rowCount = Math.Max(1, (int)Math.Floor((availableHeight + gapPx) / pitch));
var gridWidth = columnCount * cellSize + Math.Max(0, columnCount - 1) * gapPx;
var gridHeight = rowCount * cellSize + Math.Max(0, rowCount - 1) * gapPx;
return new GridMetrics(columnCount, rowCount, cellSize, gapPx, inset, gridWidth, gridHeight);
}
}
private static int ClampComponentSpan(int requestedSpan, int axisCellCount)
@@ -537,57 +943,77 @@ public partial class MainWindow : Window
private void ApplyWidgetSizing(double cellSize)
{
var margin = Math.Clamp(cellSize * 0.08, 1.5, 10);
var verticalPadding = Math.Clamp(cellSize * 0.08, 2, 12);
var horizontalPadding = Math.Clamp(cellSize * 0.20, 4, 22);
var taskbarCell = Math.Clamp(cellSize, 28, 128);
var unifiedFontSize = Math.Clamp(cellSize * 0.22, 8, 22);
var unifiedIconSize = Math.Clamp(cellSize * 0.28, 10, 26);
var taskbarCellHeight = Math.Clamp(cellSize * 0.76, 36, 76);
var taskbarTextSize = Math.Clamp(taskbarCellHeight * 0.36, 12, 22);
var taskbarIconSize = Math.Clamp(taskbarCellHeight * 0.46, 16, 34);
var taskbarButtonInset = Math.Clamp(taskbarCellHeight * 0.22, 6, 16);
var compactButtonInset = Math.Clamp(taskbarCellHeight * 0.20, 6, 14);
var buttonContentSpacing = Math.Clamp(taskbarCellHeight * 0.20, 6, 14);
var taskbarButtonPadding = new Thickness(taskbarButtonInset);
TopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1.5, 10));
ClockWidget.Margin = new Thickness(margin);
// Status bar and taskbar are special surfaces: they should fill their row.
TopStatusBarHost.Margin = new Thickness(0);
TopStatusBarHost.Padding = new Thickness(0);
BottomTaskbarContainer.Margin = new Thickness(0);
BottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(taskbarCellHeight * 0.58, 20, 44));
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(taskbarCellHeight * 0.16, 6, 14));
ClockWidget.Margin = new Thickness(0);
ClockWidget.ApplyCellSize(cellSize);
BottomTaskbarContainer.Margin = new Thickness(Math.Clamp(cellSize * 0.18, 6, 18));
BottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.24, 10, 24));
BottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 2, 10));
var buttonMinWidth = Math.Clamp(taskbarCellHeight * 2.35, 100, 340);
BackToWindowsButton.Margin = new Thickness(0);
BackToWindowsButton.Padding = new Thickness(horizontalPadding, verticalPadding);
BackToWindowsButton.FontSize = unifiedFontSize;
BackToWindowsButton.MinHeight = taskbarCell;
BackToWindowsButton.MinWidth = Math.Clamp(cellSize * 2.3, 90, 320);
BackToWindowsIcon.FontSize = unifiedIconSize;
BackToWindowsButton.Padding = taskbarButtonPadding;
BackToWindowsButton.FontSize = taskbarTextSize;
BackToWindowsButton.MinHeight = taskbarCellHeight;
BackToWindowsButton.MinWidth = buttonMinWidth;
BackToWindowsIcon.FontSize = taskbarIconSize;
BackToWindowsTextBlock.FontSize = taskbarTextSize;
SetButtonContentSpacing(BackToWindowsButton, buttonContentSpacing);
OpenComponentLibraryButton.Margin = new Thickness(0);
OpenComponentLibraryButton.Padding = new Thickness(horizontalPadding, verticalPadding);
OpenComponentLibraryButton.FontSize = unifiedFontSize;
OpenComponentLibraryButton.MinHeight = taskbarCell;
OpenComponentLibraryButton.MinWidth = Math.Clamp(cellSize * 2.0, 88, 300);
OpenComponentLibraryIcon.FontSize = unifiedIconSize;
OpenComponentLibraryButton.Padding = taskbarButtonPadding;
OpenComponentLibraryButton.FontSize = taskbarTextSize;
OpenComponentLibraryButton.MinHeight = taskbarCellHeight;
OpenComponentLibraryButton.MinWidth = Math.Clamp(taskbarCellHeight * 2.15, 92, 320);
OpenComponentLibraryIcon.FontSize = taskbarIconSize;
OpenComponentLibraryTextBlock.FontSize = taskbarTextSize;
SetButtonContentSpacing(OpenComponentLibraryButton, buttonContentSpacing);
OpenSettingsButton.Margin = new Thickness(0);
OpenSettingsButton.Height = taskbarCell;
OpenSettingsButton.MinHeight = taskbarCell;
OpenSettingsIcon.FontSize = unifiedIconSize;
OpenSettingsButton.Height = taskbarCellHeight;
OpenSettingsButton.MinHeight = taskbarCellHeight;
OpenSettingsButton.FontSize = taskbarTextSize;
OpenSettingsButtonTextBlock.FontSize = taskbarTextSize;
OpenSettingsIcon.FontSize = taskbarIconSize;
SetButtonContentSpacing(OpenSettingsButton, Math.Clamp(taskbarCellHeight * 0.18, 4, 10));
if (_isSettingsOpen)
{
OpenSettingsButton.Width = double.NaN;
OpenSettingsButton.MinWidth = Math.Clamp(cellSize * 2.3, 120, 340);
OpenSettingsButton.Padding = new Thickness(horizontalPadding, verticalPadding);
OpenSettingsButton.FontSize = unifiedFontSize;
OpenSettingsButton.MinWidth = Math.Clamp(taskbarCellHeight * 2.45, 120, 360);
OpenSettingsButton.Padding = taskbarButtonPadding;
}
else
{
OpenSettingsButton.Width = taskbarCell;
OpenSettingsButton.MinWidth = taskbarCell;
OpenSettingsButton.Padding = new Thickness(Math.Clamp(taskbarCell * 0.2, 4, 12));
OpenSettingsButton.Width = taskbarCellHeight;
OpenSettingsButton.MinWidth = taskbarCellHeight;
OpenSettingsButton.Padding = new Thickness(compactButtonInset);
}
UpdateComponentLibraryLayout(cellSize);
}
private static void SetButtonContentSpacing(Button? button, double spacing)
{
if (button?.Content is StackPanel contentPanel)
{
contentPanel.Spacing = spacing;
}
}
private void UpdateComponentLibraryLayout(double cellSize)
{
if (ComponentLibraryWindow is null)
@@ -597,8 +1023,14 @@ public partial class MainWindow : Window
var horizontalMargin = Math.Clamp(cellSize * 0.7, 18, 44);
var bottomMargin = Math.Clamp(cellSize * 1.4, 56, 190);
ComponentLibraryWindow.Margin = new Thickness(horizontalMargin, 20, horizontalMargin, bottomMargin);
ComponentLibraryWindow.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.24, 12, 24));
var defaultMargin = new Thickness(horizontalMargin, 20, horizontalMargin, bottomMargin);
if (!_isComponentLibraryWindowPositionCustomized)
{
_savedComponentLibraryMargin = defaultMargin;
}
ComponentLibraryWindow.Margin = _savedComponentLibraryMargin;
ComponentLibraryWindow.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 24, 44));
ComponentLibraryWindow.Height = Math.Clamp(cellSize * 4.8, 220, 360);
ComponentLibraryWindow.Width = Math.Clamp(cellSize * 9.2, 360, 760);
}
@@ -613,10 +1045,26 @@ public partial class MainWindow : Window
var clampedCell = Math.Max(1, cellSize);
var horizontalInset = Math.Clamp(clampedCell * 0.45, 12, 64);
var verticalGap = Math.Clamp(clampedCell * 0.16, 6, 18);
var topInset = clampedCell + verticalGap;
var bottomInset = clampedCell + verticalGap;
var edgeInset = Math.Max(0, _currentDesktopEdgeInset);
// 添加额外的安全边距以确保圆角不被裁剪
var taskbarCellHeight = Math.Clamp(clampedCell * 0.76, 36, 76);
var taskbarPadding = Math.Clamp(taskbarCellHeight * 0.16, 6, 14);
var taskbarVisualHeight = Math.Max(clampedCell, taskbarCellHeight + taskbarPadding * 2);
if (BottomTaskbarContainer is not null && BottomTaskbarContainer.Bounds.Height > 1)
{
taskbarVisualHeight = Math.Max(taskbarVisualHeight, BottomTaskbarContainer.Bounds.Height);
}
var statusBarVisualHeight = clampedCell;
if (TopStatusBarHost is not null && TopStatusBarHost.Bounds.Height > 1)
{
statusBarVisualHeight = Math.Max(statusBarVisualHeight, TopStatusBarHost.Bounds.Height);
}
var topInset = Math.Max(clampedCell + verticalGap, edgeInset + statusBarVisualHeight + verticalGap);
var bottomInset = Math.Max(clampedCell + verticalGap, edgeInset + taskbarVisualHeight + verticalGap);
// Add extra safety margin so rounded panel corners never clip against viewport edges.
var cornerSafetyMargin = Math.Clamp(clampedCell * 0.12, 4, 12);
var inset = new Thickness(
horizontalInset + cornerSafetyMargin,
@@ -624,8 +1072,7 @@ public partial class MainWindow : Window
horizontalInset + cornerSafetyMargin,
bottomInset + cornerSafetyMargin);
// 使用 Margin 来定位,而不是直接设置 Width/Height
// 这样可以让面板自然填充可用空间,同时保持边距
// Keep panel stretched with explicit viewport insets so it never overlaps fixed chrome.
SettingsContentPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
SettingsContentPanel.VerticalAlignment = VerticalAlignment.Stretch;
SettingsContentPanel.Margin = inset;
@@ -656,29 +1103,46 @@ public partial class MainWindow : Window
var aspectRatio = desktopWidth / desktopHeight;
var availableWidth = Math.Max(100, WallpaperPreviewHost.Bounds.Width);
var availableHeight = WallpaperPreviewHost.Bounds.Height;
// During initial measure, host height can be too small and cause the preview to collapse.
// Ignore tiny heights so width-driven sizing can stabilize first.
if (availableHeight < 120)
{
availableHeight = double.PositiveInfinity;
}
var framePadding = WallpaperPreviewFrame.Padding;
var horizontalPadding = framePadding.Left + framePadding.Right;
var verticalPadding = framePadding.Top + framePadding.Bottom;
var previewWidth = availableWidth;
var previewWidth = Math.Min(availableWidth, WallpaperPreviewMaxWidth);
var previewHeight = previewWidth / aspectRatio;
if (double.IsFinite(availableHeight) && previewHeight > availableHeight)
{
previewHeight = availableHeight;
previewWidth = previewHeight * aspectRatio;
}
WallpaperPreviewFrame.Width = previewWidth;
WallpaperPreviewFrame.Height = previewHeight;
WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm");
var innerWidth = Math.Max(1, previewWidth - horizontalPadding);
var innerHeight = Math.Max(1, previewHeight - verticalPadding);
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells);
var gapRatio = ResolveGridGapRatio(_gridSpacingPreset);
var edgeInset = CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent);
var gridMetrics = CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset);
if (gridMetrics.CellSize <= 0)
{
return;
}
WallpaperPreviewGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize;
WallpaperPreviewGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize;
WallpaperPreviewGrid.Margin = new Thickness(gridMetrics.EdgeInsetPx);
WallpaperPreviewGrid.RowSpacing = gridMetrics.GapPx;
WallpaperPreviewGrid.ColumnSpacing = gridMetrics.GapPx;
WallpaperPreviewGrid.Width = gridMetrics.GridWidthPx;
WallpaperPreviewGrid.Height = gridMetrics.GridHeightPx;
// This can be triggered by layout changes; always rebuild the preview grid definitions
// to avoid definitions accumulating and shifting overlay components out of place.
@@ -712,6 +1176,7 @@ public partial class MainWindow : Window
ApplyTopStatusComponentVisibility();
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
ApplyPreviewWidgetSizing(gridMetrics.CellSize);
ApplyStatusBarComponentSpacingForPanel(WallpaperPreviewTopStatusComponentsPanel, gridMetrics.CellSize);
}
finally
{
@@ -721,22 +1186,33 @@ public partial class MainWindow : Window
private void ApplyPreviewWidgetSizing(double cellSize)
{
var margin = Math.Clamp(cellSize * 0.08, 1, 6);
var previewTaskbarCell = Math.Clamp(cellSize, 10, 36);
WallpaperPreviewTopStatusBarHost.Padding = new Thickness(Math.Clamp(cellSize * 0.08, 1, 4));
WallpaperPreviewBottomTaskbarContainer.Margin = new Thickness(margin);
WallpaperPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.22, 4, 10));
WallpaperPreviewBottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 1, 4));
var previewTaskbarCell = Math.Clamp(cellSize * 0.74, 10, 28);
var previewTextSize = Math.Clamp(previewTaskbarCell * 0.38, 7, 14);
var previewIconSize = Math.Clamp(previewTaskbarCell * 0.46, 8, 16);
var previewInset = Math.Clamp(previewTaskbarCell * 0.20, 2, 6);
var previewContentSpacing = Math.Clamp(previewTaskbarCell * 0.20, 2, 6);
// Match desktop behavior: special bars fill their preview row.
WallpaperPreviewTopStatusBarHost.Margin = new Thickness(0);
WallpaperPreviewTopStatusBarHost.Padding = new Thickness(0);
WallpaperPreviewBottomTaskbarContainer.Margin = new Thickness(0);
WallpaperPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 6, 14));
WallpaperPreviewBottomTaskbarContainer.Padding = new Thickness(previewInset);
WallpaperPreviewClockWidget.ApplyCellSize(cellSize);
WallpaperPreviewBackButtonTextBlock.FontSize = previewTextSize;
WallpaperPreviewComponentLibraryTextBlock.FontSize = previewTextSize;
WallpaperPreviewBackButtonVisual.Spacing = previewContentSpacing;
WallpaperPreviewComponentLibraryVisual.Spacing = previewContentSpacing;
WallpaperPreviewClockTextBlock.FontSize = Math.Clamp(cellSize * 0.30, 6, 18);
WallpaperPreviewBackButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13);
WallpaperPreviewComponentLibraryTextBlock.FontSize = Math.Clamp(cellSize * 0.18, 5, 12);
WallpaperPreviewBackButtonVisual.MinHeight = previewTaskbarCell;
WallpaperPreviewBackButtonVisual.MinWidth = Math.Clamp(cellSize * 2.1, 30, 120);
WallpaperPreviewComponentLibraryVisual.MinHeight = previewTaskbarCell;
WallpaperPreviewComponentLibraryVisual.MinWidth = Math.Clamp(cellSize * 2.0, 28, 110);
WallpaperPreviewSettingsButtonIcon.Width = Math.Clamp(previewTaskbarCell * 0.42, 6, 14);
WallpaperPreviewSettingsButtonIcon.Height = Math.Clamp(previewTaskbarCell * 0.42, 6, 14);
WallpaperPreviewSettingsButtonIcon.Width = previewIconSize;
WallpaperPreviewSettingsButtonIcon.Height = previewIconSize;
}
private void OnMinimizeClick(object? sender, RoutedEventArgs e)
@@ -767,7 +1243,7 @@ public partial class MainWindow : Window
private void InitializeTimeZoneSettings()
{
// 填充时区下拉框
// Populate timezone dropdown items before selecting current timezone.
TimeZoneComboBox.Items.Clear();
var timeZones = _timeZoneService.GetAllTimeZones();
foreach (var tz in timeZones)
@@ -780,7 +1256,7 @@ public partial class MainWindow : Window
};
TimeZoneComboBox.Items.Add(item);
// 选中当前时区
// 閫変腑褰撳墠鏃跺尯
if (tz.Id == _timeZoneService.CurrentTimeZone.Id)
{
TimeZoneComboBox.SelectedItem = item;
@@ -805,3 +1281,4 @@ public partial class MainWindow : Window
PersistSettings();
}
}

177
docs/CORNER_RADIUS_SPEC.md Normal file
View File

@@ -0,0 +1,177 @@
# 圆角设计规范 (Corner Radius Design System)
> 基于小米澎湃OS 3 (HyperOS) 设计语言
## 设计理念
澎湃OS 3 采用**"生命感美学"**设计语言,强调:
- **全局圆角设计** - 所有界面元素均采用圆角
- **视觉舒适统一** - 柔和、现代、细腻
- **多级渲染** - 配合模糊混色与阴影
- **层级分明** - 大容器使用大圆角,小元素使用小圆角
## 圆角数值体系
### 核心数值
| 级别 | 圆角值 (px) | 用途 |
|------|-------------|------|
| **Level 0** | 0 | 特殊场景(无圆角需求) |
| **Level 1** | 12 | 小元素、图标内边角、ListBoxItem |
| **Level 2** | 16 | 色块按钮、小组件 |
| **Level 3** | 20 | 普通按钮、组件预览 |
| **Level 4** | 24 | 输入框、小型面板 |
| **Level 5** | 28 | 面板/卡片 (glass-panel) |
| **Level 6** | 32 | Mica 风格面板 (mica-strong) |
| **Level 7** | 36 | 大容器 (glass-strong)、任务栏、窗口 |
### 动态圆角
动态圆角根据格子大小cellSize动态计算
```csharp
// 小元素
CornerRadius = Math.Clamp(cellSize * 0.35, 16, 28);
// 小组件
CornerRadius = Math.Clamp(cellSize * 0.45, 24, 44);
// 大容器(任务栏/窗口)
CornerRadius = Math.Clamp(cellSize * 0.45, 24, 44);
```
**系数参考**
- 系数范围:`0.35 - 0.45`
- 最小值限制:`12 - 24 px`
- 最大值限制:`28 - 44 px`
## 组件圆角速查表
### 基础控件
| 控件 | 圆角值 | 代码位置 |
|------|--------|---------|
| Button | 20px | GlassModule.axaml |
| ToggleSwitch | 继承系统 | - |
| TextBox | 20px | glass-panel |
| ComboBox | 20px | glass-panel |
| NumberBox | 20px | glass-panel |
### 容器样式类
| 样式类 | 圆角值 | 说明 |
|--------|--------|------|
| `.glass-panel` | 28px | 普通玻璃面板 |
| `.glass-strong` | 36px | 加强玻璃面板(任务栏) |
| `.mica-strong` | 36px | Mica 风格面板(设置页) |
| `.glass-overlay` | 0px | 覆盖层(无圆角) |
### 特殊场景
| 场景 | 圆角值 | 说明 |
|------|--------|------|
| 窗口整体 | 36px | 组件库/设置窗口 |
| 窗口标题栏 | 36px | 仅顶部圆角 (`36,36,0,0`) |
| 颜色选择器色块 | 12px | Monet 颜色/推荐色 |
| 设置页 ListBoxItem | 12px | 导航项 |
| 预览视口 | 12-16px | 壁纸/网格预览 |
## 圆角层级视觉示例
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Level 7: 大容器 (36px) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Level 6: Mica 面板 (36px) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ Level 5: 玻璃面板 (28px) │ │ │
│ │ │ ┌─────────────────────────────────────────────┐ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ Level 4: 输入面板 (24px) │ │ │ │
│ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ Level 3: 按钮 (20px) │ │ │ │ │
│ │ │ │ │ ┌─────────────────────────────┐ │ │ │ │ │
│ │ │ │ │ │ Level 2: 色块 (16px) │ │ │ │ │ │
│ │ │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ │
│ │ │ │ │ │ │ Level 1: 小元素 │ │ │ │ │ │ │
│ │ │ │ │ │ │ (12px) │ │ │ │ │ │ │
│ │ │ │ │ │ └─────────────────────┘ │ │ │ │ │ │
│ │ │ │ │ └─────────────────────────────┘ │ │ │ │ │
│ │ │ │ └─────────────────────────────────────┘ │ │ │ │
│ │ │ └─────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## 在 XAML 中使用
### 直接使用固定值
```xml
<Border Classes="glass-strong" CornerRadius="36">
<!-- 内容 -->
</Border>
<Button CornerRadius="20">点击</Button>
```
### 使用样式类
```xml
<!-- 使用预定义的 glass-panel 样式 -->
<Border Classes="glass-panel">
<TextBlock Text="面板内容" />
</Border>
<!-- 组合多个样式类 -->
<Border Classes="glass-strong mica-strong">
<!-- 内容 -->
</Border>
```
### 动态圆角Code-Behind
```csharp
// 根据格子大小动态计算圆角
var cellSize = 100; // 假设格子大小
var cornerRadius = Math.Clamp(cellSize * 0.45, 24, 44);
BottomTaskbarContainer.CornerRadius = new CornerRadius(cornerRadius);
```
## 新增控件时的圆角规范
1. **确定元素层级** - 根据容器大小选择合适的级别
2. **遵循视觉一致性** - 同层级的元素使用相同圆角
3. **考虑内容安全区** - 圆角不应遮挡重要内容
4. **响应式适配** - 大屏幕使用较大圆角,小屏幕使用较小圆角
### 快速参考
```
新控件圆角选择流程:
1. 是窗口/大容器? → Level 7 (36px)
2. 是面板/卡片? → Level 5-6 (28-36px)
3. 是按钮/输入框? → Level 3-4 (20-24px)
4. 是小组件/色块? → Level 2 (16px)
5. 是图标/小元素? → Level 1 (12px)
```
## 附录:修改历史
| 日期 | 修改人 | 说明 |
|------|--------|------|
| 2026-03-02 | AI Assistant | 初始规范基于澎湃OS 3 设计语言 |
## 参考资料
- 澎湃OS 生命感美学设计
- Xiaomi HyperOS Design Guidelines
- 小米小部件审核规范 (dev.mi.com)