diff --git a/BuildTime.txt b/BuildTime.txt index 9bc983a..f4c10f2 100644 --- a/BuildTime.txt +++ b/BuildTime.txt @@ -1 +1 @@ -2026-03-26 19:55:43 \ No newline at end of file +2026-03-26 20:06:05 \ No newline at end of file diff --git a/GitCommit.txt b/GitCommit.txt index 92c6fb2..6e0ab9a 100644 --- a/GitCommit.txt +++ b/GitCommit.txt @@ -1 +1 @@ -b607cee \ No newline at end of file +2c2f93c \ No newline at end of file diff --git a/Gui/Apps/UILibGallery.cs b/Gui/Apps/UILibGallery.cs index 697a816..5787936 100644 --- a/Gui/Apps/UILibGallery.cs +++ b/Gui/Apps/UILibGallery.cs @@ -18,6 +18,7 @@ namespace CMLeonOS.Gui.Apps private readonly List demoWindows = new List(); private FileBrowser fileBrowser; + private ToolTip toolTip; private string headerTitle = "UILib Gallery"; private string headerDescription = "Browse and test the current UILib controls."; @@ -186,6 +187,21 @@ namespace CMLeonOS.Gui.Apps AddDemo(hint); } + private void ShowProgressDemo() + { + ClearPreview(); + SetHeader("ProgressRing", "Animated busy indicator for indeterminate loading states."); + + ProgressRing ring = new ProgressRing(previewHost, 24, 24, 56, 56); + ring.Active = true; + AddDemo(ring); + + TextBlock hint = new TextBlock(previewHost, 100, 38, 320, 24); + hint.Text = "ProgressRing keeps animating while Active=true."; + hint.Foreground = UITheme.TextSecondary; + AddDemo(hint); + } + private void ShowTableDemo() { ClearPreview(); @@ -239,6 +255,35 @@ namespace CMLeonOS.Gui.Apps statusBar.Text = "Ready"; statusBar.DetailText = "UILib Demo"; AddDemo(statusBar); + + Separator separator = new Separator(previewHost, 20, 172, 360, 8); + separator.Background = Color.FromArgb(250, 252, 255); + separator.Foreground = UITheme.SurfaceBorder; + AddDemo(separator); + } + + private void ShowTreeDemo() + { + ClearPreview(); + SetHeader("TreeView", "Hierarchical tree control for folders, settings and project structures."); + + TreeView tree = new TreeView(previewHost, 20, 24, 300, 180); + + TreeNode rootA = new TreeNode("System"); + rootA.Expanded = true; + rootA.Children.Add(new TreeNode("Apps")); + rootA.Children.Add(new TreeNode("Config")); + + TreeNode rootB = new TreeNode("User"); + rootB.Children.Add(new TreeNode("Desktop")); + rootB.Children.Add(new TreeNode("Documents")); + rootB.Children.Add(new TreeNode("Pictures")); + + tree.Nodes.Add(rootA); + tree.Nodes.Add(rootB); + tree.NodeSelected = (node) => SetHeader("TreeView", "Selected node: " + node.Text); + tree.Render(); + AddDemo(tree); } private void ShowDialogsDemo() @@ -286,6 +331,20 @@ namespace CMLeonOS.Gui.Apps notification.Show(); }; AddDemo(notificationButton); + + Button toolTipButton = new Button(previewHost, 172, 68, 140, 28); + toolTipButton.Text = "ToolTip"; + toolTipButton.OnClick = (_, _) => + { + if (toolTip == null) + { + toolTip = new ToolTip(this, wm); + } + toolTip.Show(toolTipButton, "Manual ToolTip demo"); + SetHeader("Dialogs", "ToolTip shown near the button."); + }; + toolTipButton.OnUnfocused = () => toolTip?.Hide(); + AddDemo(toolTipButton); } private void CategorySelected(int index) @@ -308,12 +367,18 @@ namespace CMLeonOS.Gui.Apps ShowNumericDemo(); break; case 5: - ShowTableDemo(); + ShowProgressDemo(); break; case 6: - ShowBarsDemo(); + ShowTableDemo(); break; case 7: + ShowTreeDemo(); + break; + case 8: + ShowBarsDemo(); + break; + case 9: ShowDialogsDemo(); break; } @@ -363,7 +428,9 @@ namespace CMLeonOS.Gui.Apps categoryTable.Cells.Add(new TableCell("Dropdown")); categoryTable.Cells.Add(new TableCell("Tabs")); categoryTable.Cells.Add(new TableCell("Numeric")); + categoryTable.Cells.Add(new TableCell("Progress")); categoryTable.Cells.Add(new TableCell("Table")); + categoryTable.Cells.Add(new TableCell("TreeView")); categoryTable.Cells.Add(new TableCell("Bars && Blocks")); categoryTable.Cells.Add(new TableCell("Dialogs")); wm.AddWindow(categoryTable); diff --git a/Gui/UILib/ProgressRing.cs b/Gui/UILib/ProgressRing.cs new file mode 100644 index 0000000..bcbf3e7 --- /dev/null +++ b/Gui/UILib/ProgressRing.cs @@ -0,0 +1,91 @@ +using System; +using System.Drawing; + +namespace CMLeonOS.Gui.UILib +{ + internal class ProgressRing : Control + { + public ProgressRing(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height) + { + } + + private Color _background = UITheme.Surface; + internal Color Background + { + get { return _background; } + set { _background = value; Render(); } + } + + private Color _ringColor = UITheme.Accent; + internal Color RingColor + { + get { return _ringColor; } + set { _ringColor = value; Render(); } + } + + private Color _trackColor = UITheme.SurfaceBorder; + internal Color TrackColor + { + get { return _trackColor; } + set { _trackColor = value; Render(); } + } + + private bool _active = true; + internal bool Active + { + get { return _active; } + set { _active = value; Render(); } + } + + internal int Thickness { get; set; } = 3; + + private int frame = 0; + + internal override void Render() + { + Clear(Background); + + int radius = Math.Max(6, (Math.Min(Width, Height) / 2) - 4); + int centerX = Width / 2; + int centerY = Height / 2; + + for (int angle = 0; angle < 360; angle += 12) + { + DrawArcPoint(centerX, centerY, radius, angle, TrackColor); + } + + if (Active) + { + for (int i = 0; i < 8; i++) + { + int angle = (frame + (i * 18)) % 360; + Color color = Color.FromArgb( + Math.Max(60, RingColor.R - (i * 12)), + Math.Max(60, RingColor.G - (i * 12)), + Math.Max(60, RingColor.B - (i * 12))); + DrawArcPoint(centerX, centerY, radius, angle, color); + } + + frame = (frame + 10) % 360; + WM.UpdateQueue.Enqueue(this); + } + + WM.Update(this); + } + + private void DrawArcPoint(int centerX, int centerY, int radius, int angleDegrees, Color color) + { + double radians = angleDegrees * (Math.PI / 180d); + int pointX = centerX + (int)(Math.Cos(radians) * radius); + int pointY = centerY + (int)(Math.Sin(radians) * radius); + + for (int y = -Thickness / 2; y <= Thickness / 2; y++) + { + for (int x = -Thickness / 2; x <= Thickness / 2; x++) + { + DrawPoint(pointX + x, pointY + y, color); + } + } + } + } +} diff --git a/Gui/UILib/Separator.cs b/Gui/UILib/Separator.cs new file mode 100644 index 0000000..d44471c --- /dev/null +++ b/Gui/UILib/Separator.cs @@ -0,0 +1,51 @@ +using System.Drawing; + +namespace CMLeonOS.Gui.UILib +{ + internal class Separator : Control + { + internal enum SeparatorOrientation + { + Horizontal, + Vertical + } + + public Separator(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height) + { + } + + private Color _background = UITheme.Surface; + internal Color Background + { + get { return _background; } + set { _background = value; Render(); } + } + + private Color _foreground = UITheme.SurfaceBorder; + internal Color Foreground + { + get { return _foreground; } + set { _foreground = value; Render(); } + } + + internal SeparatorOrientation Orientation { get; set; } = SeparatorOrientation.Horizontal; + + internal override void Render() + { + Clear(Background); + + if (Orientation == SeparatorOrientation.Horizontal) + { + int y = Height / 2; + DrawHorizontalLine(Width, 0, y, Foreground); + } + else + { + int x = Width / 2; + DrawVerticalLine(Height, x, 0, Foreground); + } + + WM.Update(this); + } + } +} diff --git a/Gui/UILib/ToolTip.cs b/Gui/UILib/ToolTip.cs new file mode 100644 index 0000000..0b7a3ff --- /dev/null +++ b/Gui/UILib/ToolTip.cs @@ -0,0 +1,56 @@ +using System; +using System.Drawing; + +namespace CMLeonOS.Gui.UILib +{ + internal class ToolTip + { + internal ToolTip(Process process, WindowManager wm) + { + this.process = process; + this.wm = wm; + } + + private readonly Process process; + private readonly WindowManager wm; + private Window window; + + internal void Show(Window anchor, string text, int offsetX = 0, int offsetY = 0) + { + Hide(); + + string message = text ?? string.Empty; + int width = Math.Max(80, (message.Length * 8) + 12); + int height = 24; + + int x = anchor.ScreenX + offsetX; + int y = anchor.ScreenY + anchor.Height + 6 + offsetY; + + if (x + width > wm.ScreenWidth) + { + x = (int)wm.ScreenWidth - width - 6; + } + if (y + height > wm.ScreenHeight) + { + y = anchor.ScreenY - height - 6; + } + + window = new Window(process, x, y, width, height); + window.Clear(Color.FromArgb(33, 39, 49)); + window.DrawFilledRectangle(0, 0, width, height, Color.FromArgb(33, 39, 49)); + window.DrawRectangle(0, 0, width, height, UITheme.SurfaceBorder); + window.DrawString(message, Color.White, 6, 4); + wm.AddWindow(window); + wm.Update(window); + } + + internal void Hide() + { + if (window != null) + { + wm.RemoveWindow(window); + window = null; + } + } + } +} diff --git a/Gui/UILib/TreeNode.cs b/Gui/UILib/TreeNode.cs new file mode 100644 index 0000000..375c150 --- /dev/null +++ b/Gui/UILib/TreeNode.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace CMLeonOS.Gui.UILib +{ + internal class TreeNode + { + internal TreeNode(string text, object tag = null) + { + Text = text; + Tag = tag; + } + + internal string Text { get; set; } + internal object Tag { get; set; } + internal bool Expanded { get; set; } = false; + internal List Children { get; } = new List(); + } +} diff --git a/Gui/UILib/TreeView.cs b/Gui/UILib/TreeView.cs new file mode 100644 index 0000000..88c675c --- /dev/null +++ b/Gui/UILib/TreeView.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace CMLeonOS.Gui.UILib +{ + internal class TreeView : Control + { + public TreeView(Window parent, int x, int y, int width, int height) : base(parent, x, y, width, height) + { + OnClick = TreeViewClicked; + } + + internal Action NodeSelected; + + internal List Nodes { get; } = new List(); + + private readonly List visibleNodes = new List(); + private readonly List visibleLevels = 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 Color _selectedBackground = UITheme.AccentLight; + internal Color SelectedBackground + { + get { return _selectedBackground; } + set { _selectedBackground = value; Render(); } + } + + internal int RowHeight { get; set; } = 22; + + internal TreeNode SelectedNode { get; private set; } + + private void BuildVisibleNodes() + { + visibleNodes.Clear(); + visibleLevels.Clear(); + + for (int i = 0; i < Nodes.Count; i++) + { + AddVisibleNode(Nodes[i], 0); + } + } + + private void AddVisibleNode(TreeNode node, int level) + { + visibleNodes.Add(node); + visibleLevels.Add(level); + + if (node.Expanded) + { + for (int i = 0; i < node.Children.Count; i++) + { + AddVisibleNode(node.Children[i], level + 1); + } + } + } + + private void TreeViewClicked(int x, int y) + { + BuildVisibleNodes(); + int index = y / RowHeight; + if (index < 0 || index >= visibleNodes.Count) + { + return; + } + + TreeNode node = visibleNodes[index]; + int level = visibleLevels[index]; + int indentX = 8 + (level * 16); + + if (node.Children.Count > 0 && x >= indentX && x <= indentX + 10) + { + node.Expanded = !node.Expanded; + Render(); + return; + } + + SelectedNode = node; + NodeSelected?.Invoke(node); + Render(); + } + + internal override void Render() + { + Clear(Background); + DrawRectangle(0, 0, Width, Height, Border); + + BuildVisibleNodes(); + + int maxRows = Math.Min(visibleNodes.Count, Math.Max(0, Height / RowHeight)); + for (int i = 0; i < maxRows; i++) + { + TreeNode node = visibleNodes[i]; + int level = visibleLevels[i]; + int rowY = i * RowHeight; + + if (node == SelectedNode) + { + DrawFilledRectangle(1, rowY + 1, Width - 2, RowHeight - 2, SelectedBackground); + } + + int indentX = 8 + (level * 16); + if (node.Children.Count > 0) + { + DrawRectangle(indentX, rowY + 6, 10, 10, Border); + DrawHorizontalLine(6, indentX + 2, rowY + 11, Foreground); + if (!node.Expanded) + { + DrawVerticalLine(6, indentX + 5, rowY + 8, Foreground); + } + } + + int textX = indentX + (node.Children.Count > 0 ? 16 : 8); + DrawString(node.Text, Foreground, textX, rowY + 3); + } + + WM.Update(this); + } + } +}