From b964430567e7336f49fbfb20419f6344e17761d3 Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Wed, 25 Mar 2026 21:41:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9B=B4=E5=A4=9AUILib?= =?UTF-8?q?=E6=8E=A7=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BuildTime.txt | 2 +- GitCommit.txt | 2 +- Gui/Apps/UILibGallery.cs | 67 ++++++++++++++++++- Gui/UILib/Notification.cs | 131 +++++++++++++++++++++++++++++++++++++ Gui/UILib/NumericUpDown.cs | 110 +++++++++++++++++++++++++++++++ Gui/UILib/StatusBar.cs | 62 ++++++++++++++++++ Gui/UILib/Tabs.cs | 127 +++++++++++++++++++++++++++++++++++ 7 files changed, 497 insertions(+), 4 deletions(-) create mode 100644 Gui/UILib/Notification.cs create mode 100644 Gui/UILib/NumericUpDown.cs create mode 100644 Gui/UILib/StatusBar.cs create mode 100644 Gui/UILib/Tabs.cs diff --git a/BuildTime.txt b/BuildTime.txt index 5ba308a..ee341bb 100644 --- a/BuildTime.txt +++ b/BuildTime.txt @@ -1 +1 @@ -2026-03-25 20:24:01 \ No newline at end of file +2026-03-25 21:37:54 \ No newline at end of file diff --git a/GitCommit.txt b/GitCommit.txt index 9d40933..9d6d126 100644 --- a/GitCommit.txt +++ b/GitCommit.txt @@ -1 +1 @@ -a8ac938 \ No newline at end of file +d009660 \ No newline at end of file diff --git a/Gui/Apps/UILibGallery.cs b/Gui/Apps/UILibGallery.cs index f986cfd..697a816 100644 --- a/Gui/Apps/UILibGallery.cs +++ b/Gui/Apps/UILibGallery.cs @@ -145,6 +145,47 @@ namespace CMLeonOS.Gui.Apps AddDemo(info); } + private void ShowTabsDemo() + { + ClearPreview(); + SetHeader("Tabs", "Tab strip control for switching between sections."); + + Tabs tabs = new Tabs(previewHost, 20, 24, 360, 28); + tabs.Items.Add("General"); + tabs.Items.Add("Appearance"); + tabs.Items.Add("Advanced"); + tabs.RefreshItems(); + tabs.SelectionChanged = (index, text) => SetHeader("Tabs", "Selected tab: " + text); + AddDemo(tabs); + + TextBlock block = new TextBlock(previewHost, 20, 70, 360, 48); + block.Text = "Tabs are ideal for settings and editors."; + block.Background = UITheme.AccentLight; + block.Foreground = UITheme.TextPrimary; + block.HorizontalAlignment = Alignment.Middle; + block.VerticalAlignment = Alignment.Middle; + AddDemo(block); + } + + private void ShowNumericDemo() + { + ClearPreview(); + SetHeader("NumericUpDown", "Increment and decrement integer values without typing."); + + NumericUpDown numeric = new NumericUpDown(previewHost, 20, 24, 120, 32); + numeric.Minimum = 0; + numeric.Maximum = 256; + numeric.Step = 8; + numeric.Value = 64; + numeric.Changed = (value) => SetHeader("NumericUpDown", "Value changed to " + value.ToString()); + AddDemo(numeric); + + TextBlock hint = new TextBlock(previewHost, 160, 28, 320, 24); + hint.Text = "Click + / - to change the value."; + hint.Foreground = UITheme.TextSecondary; + AddDemo(hint); + } + private void ShowTableDemo() { ClearPreview(); @@ -193,6 +234,11 @@ namespace CMLeonOS.Gui.Apps image.Image = AppManager.DefaultAppIcon.Resize(48, 48); image.Alpha = true; AddDemo(image); + + StatusBar statusBar = new StatusBar(previewHost, 20, 132, 360, 24); + statusBar.Text = "Ready"; + statusBar.DetailText = "UILib Demo"; + AddDemo(statusBar); } private void ShowDialogsDemo() @@ -231,6 +277,15 @@ namespace CMLeonOS.Gui.Apps fileBrowser.Show(); }; AddDemo(fileBrowserButton); + + Button notificationButton = new Button(previewHost, 20, 68, 140, 28); + notificationButton.Text = "Notification"; + notificationButton.OnClick = (_, _) => + { + Notification notification = new Notification(this, "UILib Gallery", "This is a toast notification."); + notification.Show(); + }; + AddDemo(notificationButton); } private void CategorySelected(int index) @@ -247,12 +302,18 @@ namespace CMLeonOS.Gui.Apps ShowDropdownDemo(); break; case 3: - ShowTableDemo(); + ShowTabsDemo(); break; case 4: - ShowBarsDemo(); + ShowNumericDemo(); break; case 5: + ShowTableDemo(); + break; + case 6: + ShowBarsDemo(); + break; + case 7: ShowDialogsDemo(); break; } @@ -300,6 +361,8 @@ namespace CMLeonOS.Gui.Apps categoryTable.Cells.Add(new TableCell("Buttons")); categoryTable.Cells.Add(new TableCell("Inputs")); categoryTable.Cells.Add(new TableCell("Dropdown")); + categoryTable.Cells.Add(new TableCell("Tabs")); + categoryTable.Cells.Add(new TableCell("Numeric")); categoryTable.Cells.Add(new TableCell("Table")); categoryTable.Cells.Add(new TableCell("Bars && Blocks")); categoryTable.Cells.Add(new TableCell("Dialogs")); diff --git a/Gui/UILib/Notification.cs b/Gui/UILib/Notification.cs new file mode 100644 index 0000000..39394a3 --- /dev/null +++ b/Gui/UILib/Notification.cs @@ -0,0 +1,131 @@ +using CMLeonOS; +using CMLeonOS.UILib.Animations; +using System.Drawing; + +namespace CMLeonOS.Gui.UILib +{ + internal class Notification + { + internal Notification(Process process, string title, string message) + { + owner = process; + Title = title; + Message = message; + } + + private readonly Process owner; + internal string Title { get; } + internal string Message { get; } + internal void Show() + { + ProcessManager.AddProcess(owner, new NotificationProcess(owner, Title, Message)).Start(); + } + + private class NotificationProcess : Process + { + internal NotificationProcess(Process parent, string title, string message) : base("Notification", ProcessType.Background, parent) + { + this.title = title; + this.message = message; + } + + private readonly string title; + private readonly string message; + + private Window window; + private Button closeButton; + private readonly WindowManager wm = ProcessManager.GetProcess(); + private bool closeAnimationRunning = false; + private int targetX; + private int targetY; + + public override void Start() + { + base.Start(); + + int width = 280; + int height = 84; + targetX = (int)wm.ScreenWidth - width - 16; + targetY = (int)wm.ScreenHeight - height - 44; + int startX = (int)wm.ScreenWidth + 12; + + window = new Window(this, startX, targetY, width, height); + window.Clear(UITheme.WindowTitleBackground); + window.DrawFilledRectangle(0, 0, window.Width, window.Height, UITheme.WindowTitleBackground); + window.DrawRectangle(0, 0, window.Width, window.Height, UITheme.SurfaceBorder); + window.DrawFilledRectangle(0, 0, window.Width, 22, UITheme.Accent); + window.DrawString(title, UITheme.WindowTitleText, 10, 4); + window.DrawString(message, Color.White, 10, 30); + wm.AddWindow(window); + + closeButton = new Button(window, window.Width - 54, window.Height - 28, 44, 20); + closeButton.Text = "Close"; + closeButton.OnClick = (_, _) => BeginCloseAnimation(); + wm.AddWindow(closeButton); + + wm.Update(window); + StartOpenAnimation(startX, targetY, width, height); + } + + private void StartOpenAnimation(int startX, int startY, int width, int height) + { + MovementAnimation animation = new MovementAnimation(window) + { + From = new Rectangle(startX, startY, width, height), + To = new Rectangle(targetX, targetY, width, height), + Duration = 10, + EasingType = EasingType.Sine, + EasingDirection = EasingDirection.Out + }; + animation.Completed = () => + { + wm.Update(window); + closeButton.Render(); + }; + animation.Start(); + } + + private void BeginCloseAnimation() + { + if (closeAnimationRunning || window == null) + { + return; + } + + closeAnimationRunning = true; + + int endX = (int)wm.ScreenWidth + 12; + int width = window.Width; + int height = window.Height; + + MovementAnimation animation = new MovementAnimation(window) + { + From = new Rectangle(window.X, window.Y, width, height), + To = new Rectangle(endX, targetY, width, height), + Duration = 9, + EasingType = EasingType.Sine, + EasingDirection = EasingDirection.In + }; + animation.Completed = TryStop; + animation.Start(); + } + + public override void Run() + { + } + + public override void Stop() + { + base.Stop(); + if (closeButton != null) + { + wm.RemoveWindow(closeButton); + } + if (window != null) + { + wm.RemoveWindow(window); + } + } + } + } +} diff --git a/Gui/UILib/NumericUpDown.cs b/Gui/UILib/NumericUpDown.cs new file mode 100644 index 0000000..7f5d853 --- /dev/null +++ b/Gui/UILib/NumericUpDown.cs @@ -0,0 +1,110 @@ +using System; +using System.Drawing; + +namespace CMLeonOS.Gui.UILib +{ + internal class NumericUpDown : Control + { + public NumericUpDown(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height) + { + OnClick = NumericUpDownClicked; + } + + internal Action Changed; + + private Color _background = UITheme.Surface; + internal Color Background + { + get { return _background; } + set { _background = value; Render(); } + } + + private Color _foreground = UITheme.TextPrimary; + internal Color Foreground + { + get { return _foreground; } + set { _foreground = value; Render(); } + } + + private Color _border = UITheme.SurfaceBorder; + internal Color Border + { + get { return _border; } + set { _border = value; Render(); } + } + + private int _minimum = 0; + internal int Minimum + { + get { return _minimum; } + set { _minimum = value; if (_value < _minimum) { _value = _minimum; } Render(); } + } + + private int _maximum = 100; + internal int Maximum + { + get { return _maximum; } + set { _maximum = value; if (_value > _maximum) { _value = _maximum; } Render(); } + } + + private int _step = 1; + internal int Step + { + get { return _step; } + set { _step = Math.Max(1, value); Render(); } + } + + private int _value = 0; + internal int Value + { + get { return _value; } + set + { + int newValue = Math.Clamp(value, _minimum, _maximum); + if (_value != newValue) + { + _value = newValue; + Render(); + Changed?.Invoke(_value); + } + } + } + + private void NumericUpDownClicked(int x, int y) + { + int buttonWidth = 22; + if (x >= Width - buttonWidth) + { + if (y < Height / 2) + { + Value += _step; + } + else + { + Value -= _step; + } + } + } + + internal override void Render() + { + int buttonWidth = 22; + + Clear(Background); + DrawFilledRectangle(0, 0, Width, Height, Background); + DrawRectangle(0, 0, Width, Height, Border); + + DrawFilledRectangle(Width - buttonWidth, 0, buttonWidth, Height, UITheme.SurfaceMuted); + DrawRectangle(Width - buttonWidth, 0, buttonWidth, Height, Border); + DrawHorizontalLine(buttonWidth, Width - buttonWidth, Height / 2, Border); + + string text = _value.ToString(); + DrawString(text, Foreground, 6, (Height / 2) - 8); + + DrawString("+", Foreground, Width - buttonWidth + 7, 2); + DrawString("-", Foreground, Width - buttonWidth + 7, (Height / 2) - 2); + + WM.Update(this); + } + } +} diff --git a/Gui/UILib/StatusBar.cs b/Gui/UILib/StatusBar.cs new file mode 100644 index 0000000..fc0e702 --- /dev/null +++ b/Gui/UILib/StatusBar.cs @@ -0,0 +1,62 @@ +using System.Drawing; + +namespace CMLeonOS.Gui.UILib +{ + internal class StatusBar : Control + { + public StatusBar(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height) + { + } + + private string _text = "Ready"; + internal string Text + { + get { return _text; } + set { _text = value ?? string.Empty; Render(); } + } + + private string _detailText = string.Empty; + internal string DetailText + { + get { return _detailText; } + set { _detailText = value ?? string.Empty; Render(); } + } + + private Color _background = UITheme.SurfaceMuted; + internal Color Background + { + get { return _background; } + set { _background = value; Render(); } + } + + private Color _foreground = UITheme.TextPrimary; + internal Color Foreground + { + get { return _foreground; } + set { _foreground = value; Render(); } + } + + private Color _border = UITheme.SurfaceBorder; + internal Color Border + { + get { return _border; } + set { _border = value; Render(); } + } + + internal override void Render() + { + Clear(Background); + DrawFilledRectangle(0, 0, Width, Height, Background); + DrawHorizontalLine(Width, 0, 0, Border); + + DrawString(Text, Foreground, 6, (Height / 2) - 8); + + if (!string.IsNullOrWhiteSpace(DetailText)) + { + DrawString(DetailText, Foreground, Width - (DetailText.Length * 8) - 6, (Height / 2) - 8); + } + + WM.Update(this); + } + } +} diff --git a/Gui/UILib/Tabs.cs b/Gui/UILib/Tabs.cs new file mode 100644 index 0000000..80ef9af --- /dev/null +++ b/Gui/UILib/Tabs.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace CMLeonOS.Gui.UILib +{ + internal class Tabs : Control + { + public Tabs(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height) + { + OnClick = TabsClicked; + } + + internal Action SelectionChanged; + + internal List Items { get; } = new List(); + + private Color _background = UITheme.Surface; + internal Color Background + { + get { return _background; } + set { _background = value; Render(); } + } + + private Color _foreground = UITheme.TextPrimary; + internal Color Foreground + { + get { return _foreground; } + set { _foreground = value; Render(); } + } + + private Color _border = UITheme.SurfaceBorder; + internal Color Border + { + get { return _border; } + set { _border = value; Render(); } + } + + private int _selectedIndex = 0; + internal int SelectedIndex + { + get { return _selectedIndex; } + set + { + int newIndex = Math.Clamp(value, 0, Math.Max(0, Items.Count - 1)); + if (_selectedIndex != newIndex) + { + _selectedIndex = newIndex; + Render(); + if (Items.Count > 0) + { + SelectionChanged?.Invoke(_selectedIndex, Items[_selectedIndex]); + } + } + } + } + + internal string SelectedText + { + get + { + if (Items.Count == 0 || _selectedIndex < 0 || _selectedIndex >= Items.Count) + { + return string.Empty; + } + + return Items[_selectedIndex]; + } + } + + internal void RefreshItems() + { + if (_selectedIndex >= Items.Count) + { + _selectedIndex = Math.Max(0, Items.Count - 1); + } + Render(); + } + + private void TabsClicked(int x, int y) + { + if (Items.Count == 0) + { + return; + } + + int tabWidth = Math.Max(1, Width / Items.Count); + int index = Math.Min(Items.Count - 1, Math.Max(0, x / tabWidth)); + SelectedIndex = index; + } + + internal override void Render() + { + Clear(Background); + DrawFilledRectangle(0, 0, Width, Height, Background); + DrawRectangle(0, 0, Width, Height, Border); + + if (Items.Count == 0) + { + WM.Update(this); + return; + } + + int tabWidth = Math.Max(1, Width / Items.Count); + for (int i = 0; i < Items.Count; i++) + { + int tabX = i * tabWidth; + int actualWidth = i == Items.Count - 1 ? Width - tabX : tabWidth; + bool selected = i == _selectedIndex; + + DrawFilledRectangle(tabX, 0, actualWidth, Height, selected ? UITheme.AccentLight : UITheme.SurfaceMuted); + DrawRectangle(tabX, 0, actualWidth, Height, selected ? UITheme.Accent : Border); + + string text = Items[i]; + int maxChars = Math.Max(1, (actualWidth - 8) / 8); + if (text.Length > maxChars) + { + text = text.Substring(0, Math.Max(1, maxChars - 1)) + "~"; + } + + DrawString(text, Foreground, tabX + (actualWidth / 2) - (text.Length * 4), (Height / 2) - 8); + } + + WM.Update(this); + } + } +}