diff --git a/Paper/Paper.csproj b/Paper/Paper.csproj
index a6530e9..6975884 100644
--- a/Paper/Paper.csproj
+++ b/Paper/Paper.csproj
@@ -31,7 +31,7 @@
-
+
diff --git a/Samples/RaylibSample/RaylibCanvasRenderer.cs b/Samples/RaylibSample/RaylibCanvasRenderer.cs
index 91b53e3..23a52fa 100644
--- a/Samples/RaylibSample/RaylibCanvasRenderer.cs
+++ b/Samples/RaylibSample/RaylibCanvasRenderer.cs
@@ -188,7 +188,7 @@ public object CreateTexture(uint width, uint height)
{
var image = GenImageColor((int)width, (int)height, new Color(0, 0, 0, 0));
var texture = LoadTextureFromImage(image);
- SetTextureFilter(texture, TextureFilter.Point);
+ SetTextureFilter(texture, TextureFilter.Bilinear);
return texture;
}
diff --git a/Samples/Shared/PaperDemo.Components.cs b/Samples/Shared/PaperDemo.Components.cs
index 03845db..9817f3b 100644
--- a/Samples/Shared/PaperDemo.Components.cs
+++ b/Samples/Shared/PaperDemo.Components.cs
@@ -21,119 +21,30 @@ public static class Button
{
public static void DefineStyles()
{
- // Primary button variant
- PaperDemo.Gui.CreateStyleFamily("shadcs-button-primary")
- .Base(new StyleTemplate()
- .Height(40)
- .Rounded(8)
- .BackgroundColor(Themes.primaryColor)
- .Transition(GuiProp.BackgroundColor, 0.2)
- .Transition(GuiProp.ScaleX, 0.1)
- .Transition(GuiProp.ScaleY, 0.1))
- .Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(150, Themes.primaryColor)))
- .Active(new StyleTemplate()
- .Scale(0.95)
- .BackgroundColor(Color.FromArgb(200, Themes.primaryColor)))
- .Register();
-
// Secondary button variant
PaperDemo.Gui.CreateStyleFamily("shadcs-button-secondary")
.Base(new StyleTemplate()
.Height(40)
.Rounded(8)
- .BackgroundColor(Themes.secondaryColor)
+ .BackgroundColor(Themes.base300)
.Transition(GuiProp.BackgroundColor, 0.2)
.Transition(GuiProp.ScaleX, 0.1)
.Transition(GuiProp.ScaleY, 0.1))
.Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(150, Themes.secondaryColor)))
+ .BackgroundColor(Color.FromArgb(150, Themes.base300)))
.Active(new StyleTemplate()
.Scale(0.95)
- .BackgroundColor(Color.FromArgb(200, Themes.primaryColor)))
- .Register();
-
- // Outline button variant
- PaperDemo.Gui.CreateStyleFamily("shadcs-button-outline")
- .Base(new StyleTemplate()
- .Height(40)
- .Rounded(8)
- .BackgroundColor(Themes.backgroundColor)
- .BorderColor(Themes.secondaryColor)
- .BorderWidth(1)
- .Transition(GuiProp.BackgroundColor, 0.2)
- .Transition(GuiProp.ScaleX, 0.1)
- .Transition(GuiProp.ScaleY, 0.1))
- .Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(150, Themes.backgroundColor)))
- .Active(new StyleTemplate()
- .Scale(0.95)
- .BackgroundColor(Color.FromArgb(200, Themes.primaryColor)))
- .Register();
-
- // Icon button styles
- PaperDemo.Gui.CreateStyleFamily("shadcs-icon-button-primary")
- .Base(new StyleTemplate()
- .Width(40)
- .Height(40)
- .Rounded(8)
- .BackgroundColor(Themes.primaryColor)
- // .Transition(GuiProp.BackgroundColor, 0.2)
- .Transition(GuiProp.Rounded, 0.2))
- .Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(100, Themes.primaryColor))
- .Rounded(20))
- .Register();
-
- // Icon button styles secondary
- PaperDemo.Gui.CreateStyleFamily("shadcs-icon-button-secondary")
- .Base(new StyleTemplate()
- .Width(40)
- .Height(40)
- .Rounded(8)
- .BackgroundColor(Themes.secondaryColor)
- // .Transition(GuiProp.BackgroundColor, 0.2)
- .Transition(GuiProp.Rounded, 0.2))
- .Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(100, Themes.secondaryColor))
- .Rounded(20))
+ .BackgroundColor(Color.FromArgb(200, Themes.primary)))
.Register();
}
- public static ElementBuilder Primary(string stringID, string text = "", int intID = 0, [CallerLineNumber] int lineID = 0)
- {
- return PaperDemo.Gui.Box(stringID, intID, lineID)
- .Text(text, Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleCenter)
- .Style("shadcs-button-primary");
- }
public static ElementBuilder Secondary(string stringID, string text = "", int intID = 0, [CallerLineNumber] int lineID = 0)
{
return PaperDemo.Gui.Box(stringID, intID, lineID)
- .Text(text, Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleCenter)
+ .Text(text, Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleCenter)
.Style("shadcs-button-secondary");
}
-
- public static ElementBuilder Outline(string stringID, string text = "", int intID = 0, [CallerLineNumber] int lineID = 0)
- {
- return PaperDemo.Gui.Box(stringID, intID, lineID)
- .Text(text, Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleCenter)
- .Style("shadcs-button-outline");
- }
-
- public static ElementBuilder IconPrimary(string stringID, string text = "", int intID = 0, [CallerLineNumber] int lineID = 0)
- {
- return PaperDemo.Gui.Box(stringID, intID, lineID)
- .Text(text, Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleCenter)
- .Style("shadcs-icon-button-primary");
- }
-
- public static ElementBuilder IconSecondary(string stringID, string text = "", int intID = 0, [CallerLineNumber] int lineID = 0)
- {
- return PaperDemo.Gui.Box(stringID, intID, lineID)
- .Text(text, Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleCenter)
- .Style("shadcs-icon-button-secondary");
- }
}
public static class Input
@@ -146,15 +57,15 @@ public static void DefineStyles()
.Width(300)
.Height(40)
.Rounded(8)
- .BackgroundColor(Themes.secondaryColor)
+ .BackgroundColor(Themes.base300)
.BorderColor(Color.Transparent)
.BorderWidth(0)
.Transition(GuiProp.BorderColor, 0.2)
.Transition(GuiProp.BorderWidth, 0.2))
.Focused(new StyleTemplate()
- .BorderColor(Themes.primaryColor)
+ .BorderColor(Themes.primary)
.BorderWidth(1)
- .BackgroundColor(Themes.secondaryColor))
+ .BackgroundColor(Themes.base300))
.Register();
}
@@ -170,14 +81,9 @@ public static ElementBuilder Secondary(string stringID, string value, Action
@@ -249,7 +155,7 @@ public static ElementBuilder Primary(string stringID, double sliderValue, Action
.Width(PaperDemo.Gui.Percent(sliderValue * 100))
.MinWidth(10)
.RoundedLeft(10)
- .BackgroundColor(Themes.primaryColor)
+ .BackgroundColor(Themes.primary)
//.Style(BoxStyle.SolidRounded(primaryColor, 10f))
.Enter())
{
@@ -259,7 +165,7 @@ public static ElementBuilder Primary(string stringID, double sliderValue, Action
.Width(20)
.Height(20)
.Rounded(10)
- .BackgroundColor(Themes.textColor)
+ .BackgroundColor(Themes.primaryContent)
//.Style(BoxStyle.SolidRounded(textColor, 10f))
.PositionType(PositionType.SelfDirected)
.Enter()) { }
@@ -268,40 +174,6 @@ public static ElementBuilder Primary(string stringID, double sliderValue, Action
cleanup.Dispose();
return parent;
- // // OLD
- // using (PaperDemo.Gui.Box("SliderTrack")
- // .Height(20)
- // .BackgroundColor(Color.FromArgb(30, 0, 0, 0))
- // //.Style(BoxStyle.SolidRounded(Color.FromArgb(30, 0, 0, 0), 10f))
- // .Margin(0, 0, 20, 0)
- // .OnHeld((e) =>
- // {
- // double parentWidth = e.ElementRect.width;
- // double pointerX = e.PointerPosition.x - e.ElementRect.x;
-
- // // Calculate new slider value based on pointer position
- // sliderValue = Math.Clamp(pointerX / parentWidth, 0f, 1f);
- // })
- // .Enter())
- // {
- // // Filled part of slider
- // using (Gui.Box("SliderFill")
- // .Width(Gui.Percent(sliderValue * 100))
- // .BackgroundColor(Themes.primaryColor)
- // //.Style(BoxStyle.SolidRounded(primaryColor, 10f))
- // .Enter())
- // {
- // // Slider handle
- // using (Gui.Box("SliderHandle")
- // .Left(Gui.Percent(100, -10))
- // .Width(20)
- // .Height(20)
- // .BackgroundColor(Themes.textColor)
- // //.Style(BoxStyle.SolidRounded(textColor, 10f))
- // .PositionType(PositionType.SelfDirected)
- // .Enter()) { }
- // }
- // }
}
public static ElementBuilder Secondary(string stringID, double sliderValue, Action onChange, int intID = 0, [CallerLineNumber] int lineID = 0)
@@ -309,7 +181,7 @@ public static ElementBuilder Secondary(string stringID, double sliderValue, Acti
var parent = PaperDemo.Gui.Box(stringID, intID, lineID)
.Height(20)
.Rounded(10)
- .BackgroundColor(Themes.backgroundColor)
+ .BackgroundColor(Themes.base100)
//.Style(BoxStyle.SolidRounded(Color.FromArgb(30, 0, 0, 0), 10f))
.Margin(0, 0, 20, 0)
.OnHeld((e) =>
@@ -329,7 +201,7 @@ public static ElementBuilder Secondary(string stringID, double sliderValue, Acti
.Width(PaperDemo.Gui.Percent(sliderValue * 100))
.MinWidth(10)
.RoundedLeft(10)
- .BackgroundColor(Themes.secondaryColor)
+ .BackgroundColor(Themes.base300)
//.Style(BoxStyle.SolidRounded(primaryColor, 10f))
.Enter())
{
@@ -339,7 +211,7 @@ public static ElementBuilder Secondary(string stringID, double sliderValue, Acti
.Width(20)
.Height(20)
.Rounded(10)
- .BackgroundColor(Themes.textColor)
+ .BackgroundColor(Themes.primaryContent)
//.Style(BoxStyle.SolidRounded(textColor, 10f))
.PositionType(PositionType.SelfDirected)
.Enter()) { }
@@ -366,10 +238,10 @@ public static void DefineStyles()
.Register();
PaperDemo.Gui.RegisterStyle("toggle-on", new StyleTemplate()
- .BackgroundColor(Themes.primaryColor));
+ .BackgroundColor(Themes.primary));
PaperDemo.Gui.RegisterStyle("toggle-off", new StyleTemplate()
- .BackgroundColor(Color.FromArgb(100, Themes.secondaryColor)));
+ .BackgroundColor(Color.FromArgb(100, Themes.base300)));
PaperDemo.Gui.RegisterStyle("toggle-dot", new StyleTemplate()
.Width(24)
@@ -472,94 +344,9 @@ public static ElementBuilder Primary(string stringID, double[] values, double st
vg.Circle(centerX, centerY, radius * 0.4f);
vg.SetFillColor(Color.White);
vg.Fill();
-
- // Draw center text
- // Draw center text
- //vg.FillColor(textColor);
- //vg.TextAlign(NvgSharp.Align.Center | NvgSharp.Align.Middle);
- //vg.FontSize(20);
- //vg.Text(centerX, centerY, $"Analytics\n{(sliderValue * 100):F0}%");
- //vg.Text(fontSmall, $"Analytics\n{(sliderValue * 100):F0}%", centerX, centerY);
});
}
return builder;
}
-
- // // OLD
- // // "Analysis" mock content
- // using (Gui.Box("AnalyticsVisual")
- // .Margin(20)
- // .Enter())
- // {
- // // Add a simple pie chart visualization
- // Gui.AddActionElement((vg, rect) => {
- // double centerX = rect.x + rect.width / 2;
- // double centerY = rect.y + rect.height / 2;
- // double radius = Math.Min(rect.width, rect.height) * 0.4f;
-
- // double startAngle = 0;
- // double[] values = { sliderValue, 0.2f, 0.15f, 0.25f, 0.1f };
-
- // // Normalize Values
- // double total = values.Sum();
- // for (int i = 0; i < values.Length; i++)
- // values[i] /= total;
-
-
- // for (int i = 0; i < values.Length; i++)
- // {
- // // Calculate angles
- // double angle = values[i] * Math.PI * 2;
- // double endAngle = startAngle + angle;
-
- // // Draw pie slice
- // vg.BeginPath();
- // vg.MoveTo(centerX, centerY);
- // vg.Arc(centerX, centerY, radius, startAngle, endAngle);
- // vg.LineTo(centerX, centerY);
- // vg.SetFillColor(Themes.colorPalette[i % Themes.colorPalette.Length]);
- // vg.Fill();
-
- // // Draw outline
- // vg.BeginPath();
- // vg.MoveTo(centerX, centerY);
- // vg.Arc(centerX, centerY, radius, startAngle, endAngle);
- // vg.LineTo(centerX, centerY);
- // vg.SetStrokeColor(Color.White);
- // vg.SetStrokeWidth(2);
- // vg.Stroke();
-
- // // Draw percentage labels
- // double labelAngle = startAngle + angle / 2;
- // double labelRadius = radius * 0.7f;
- // double labelX = centerX + Math.Cos(labelAngle) * labelRadius;
- // double labelY = centerY + Math.Sin(labelAngle) * labelRadius;
-
- // string label = $"{values[i] * 100:F0}%";
- // vg.SetFillColor(Color.White);
- // //vg.TextAlign(Align.Center | Align.Middle);
- // //vg.FontSize(16);
- // //vg.Text(labelX, labelY, label);
- // vg.DrawText(Fonts.fontSmall, label, labelX, labelY, Color.White);
-
- // // Move to next slice
- // startAngle = endAngle;
- // }
-
- // // Draw center circle
- // vg.BeginPath();
- // vg.Circle(centerX, centerY, radius * 0.4f);
- // vg.SetFillColor(Color.White);
- // vg.Fill();
-
- // // Draw center text
- // // Draw center text
- // //vg.FillColor(textColor);
- // //vg.TextAlign(NvgSharp.Align.Center | NvgSharp.Align.Middle);
- // //vg.FontSize(20);
- // //vg.Text(centerX, centerY, $"Analytics\n{(sliderValue * 100):F0}%");
- // //vg.Text(fontSmall, $"Analytics\n{(sliderValue * 100):F0}%", centerX, centerY);
- // });
- // }
}
-}
+}
\ No newline at end of file
diff --git a/Samples/Shared/PaperDemo.New.cs b/Samples/Shared/PaperDemo.New.cs
new file mode 100644
index 0000000..3b3cba4
--- /dev/null
+++ b/Samples/Shared/PaperDemo.New.cs
@@ -0,0 +1,156 @@
+using Prowl.PaperUI;
+
+namespace Shared
+{
+ public static partial class PaperDemo
+ {
+
+ public static TabsManager tabsManager;
+
+ public static Paper Gui;
+ static double value = 0;
+
+ public static void Initialize(Paper paper)
+ {
+ Gui = paper;
+ Fonts.Initialize(Gui);
+ Themes.Initialize();
+
+ // create the tabs
+ tabsManager = new TabsManager(paper);
+ }
+
+ public static void RenderUI()
+ {
+ using (Gui.Box("App").BackgroundColor(Themes.base100).Enter())
+ {
+ using (Gui.Column("EditorContainer").Margin(8).Enter())
+ {
+ TitleBarUI();
+
+ using (Gui.Row("3 Columns Editor Layout").RowBetween(6).Enter())
+ {
+ using (Gui.Column("Left Panel").ColBetween(8).Width(250).Enter())
+ {
+ using (WindowContainer("Scene Tree Window").Enter())
+ {
+ tabsManager.DrawGroup(["hierarchy", "assets"]);
+ }
+
+ using (WindowContainer("Files Window Container").Enter())
+ {
+ tabsManager.DrawGroup(["files", "settings"]);
+ }
+ }
+
+ using (Gui.Column("Center Panel").Enter())
+ {
+ using (WindowContainer("Game and Scene Window").Enter())
+ {
+ tabsManager.DrawGroup(["chart", "pong", "platformer", "slimefriends", "asteroids"]);
+ }
+ }
+
+ using (Gui.Column("Right Panel").Width(250).Enter())
+ {
+ using (WindowContainer("Inspector Window").Enter())
+ {
+ tabsManager.DrawGroup(["inspector", "example"]);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private static ElementBuilder WindowContainer(string id)
+ {
+ return Gui.Box(id)
+ .BackgroundColor(Themes.base100)
+ .Rounded(5)
+ .BorderColor(Themes.base200)
+ .BorderWidth(1);
+ }
+
+ private static void TitleBarUI()
+ {
+ using (Gui.Row("Header").Height(28).Bottom(8).Enter())
+ {
+ Gui.Box("tab 1")
+ .Width(80).Height(28)
+ .BackgroundColor(Themes.base100)
+ .Text(Icons.Hammer + " Bevy", Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Hovered
+ .BackgroundColor(Themes.base300)
+ .End()
+ .Rounded(5)
+ .Alignment(TextAlignment.MiddleCenter);
+
+ Gui.Box("tab 2")
+ .Width(45).Height(28)
+ .BackgroundColor(Themes.base100)
+ .Text("File", Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Rounded(5)
+ .Hovered
+ .BackgroundColor(Themes.base300)
+ .End()
+ .Alignment(TextAlignment.MiddleCenter)
+ .Left(5);
+
+ Gui.Box("tab 3")
+ .Width(45).Height(28)
+ .BackgroundColor(Themes.base100)
+ .Text("Edit", Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Rounded(5)
+ .Hovered
+ .BackgroundColor(Themes.base300)
+ .End()
+ .Alignment(TextAlignment.MiddleCenter)
+ .Left(5);
+
+ Gui.Box("tab 4")
+ .Width(65).Height(28)
+ .BackgroundColor(Themes.base100)
+ .Text("Debug", Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Rounded(5)
+ .Hovered
+ .BackgroundColor(Themes.base300)
+ .End()
+ .Alignment(TextAlignment.MiddleCenter)
+ .Left(5);
+
+ Gui.Box("Spacer");
+
+ Gui.Box("Play / Pause")
+ .Width(64).Height(28)
+ .BackgroundColor(Themes.base200)
+ .Text(Icons.Play + " " + Icons.Pause, Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Rounded(5)
+ .Hovered
+ .BackgroundColor(Themes.base250)
+ .End()
+ .Alignment(TextAlignment.MiddleCenter)
+ .Left(500);
+ }
+ }
+
+ private static void ComponentDemo()
+ {
+ using (Gui.Column("EditorContainer").Margin(5).Enter())
+ {
+ Button.Secondary("Primary Button").OnClick((_) => Console.WriteLine("Primary Button Clicked")).Margin(5);
+ Button.Secondary("Secondary Button").OnClick((_) => Console.WriteLine("Secondary Button Clicked")).Margin(5);
+
+ TextArea.Secondary("TextArea", "This is a secondary text area. It can hold multiple lines of text and is styled differently from the primary text area.").Margin(5);
+ Slider.Primary("Slider 1", value, (v) => { value = v; });
+ Slider.Secondary("Slider 1", value, (v) => { value = v; });
+ PieChart.Primary("Pie Chart", new double[] { 10, 20, 30, 40 }, value).Height(200).Margin(5);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/PaperDemo.cs b/Samples/Shared/PaperDemo.Old.cs
similarity index 91%
rename from Samples/Shared/PaperDemo.cs
rename to Samples/Shared/PaperDemo.Old.cs
index 649bd23..a572388 100644
--- a/Samples/Shared/PaperDemo.cs
+++ b/Samples/Shared/PaperDemo.Old.cs
@@ -8,7 +8,7 @@
namespace Shared
{
- public static partial class PaperDemo
+ public static partial class PaperDemoOld
{
// Track state for interactive elements
static double sliderValue = 0.5f;
@@ -44,7 +44,7 @@ public static void RenderUI()
// Main container with light gray background
using (Gui.Column("MainContainer")
- .BackgroundColor(Themes.backgroundColor)
+ .BackgroundColor(Themes.base100)
//.Style(BoxStyle.Solid(backgroundColor))
.Enter())
{
@@ -158,12 +158,12 @@ private static void RenderTopNavBar()
Gui.Box("LogoInner")
.Size(50)
.Margin(10)
- .Text(Icons.Newspaper, Fonts.arial).FontSize(32).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleCenter);
+ .Text(Icons.Newspaper, Fonts.arial).FontSize(32).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleCenter);
Gui.Box("LogoText")
.PositionType(PositionType.SelfDirected)
.Left(50 + 15)
- .Text("PaperUI Demo", Fonts.arial).FontSize(40).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft);
+ .Text("PaperUI Demo", Fonts.arial).FontSize(40).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft);
}
// Spacer
@@ -174,17 +174,17 @@ private static void RenderTopNavBar()
.Margin(0, 15, 15, 0);
// Theme Switch - using icon-button style
- Button.IconPrimary("LightIcon", Icons.Lightbulb)
+ Button.Secondary("LightIcon", Icons.Lightbulb)
.Margin(0, 10, 15, 0)
- .OnClick((rect) => Themes.ToggleTheme());
+ .OnClick((rect) => {/*Themes.ToggleTheme() */});
// Notification icon
- Button.IconSecondary("NotificationIcon", Icons.CircleExclamation)
+ Button.Secondary("NotificationIcon", Icons.CircleExclamation)
.Margin(0, 10, 15, 0)
.OnClick((rect) => Console.WriteLine("Notifications clicked"));
// User Profile
- Button.IconPrimary("UserProfile", "M")
+ Button.Secondary("UserProfile", "M")
.Margin(0, 15, 15, 0)
.OnClick((rect) => Console.WriteLine("Profile clicked"));
}
@@ -200,7 +200,7 @@ private static void RenderSidebar()
// Menu header
Gui.Box("MenuHeader")
.Height(60)
- .Text("Menu", Fonts.arial).FontSize(26).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleCenter);
+ .Text("Menu", Fonts.arial).FontSize(26).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleCenter);
string[] menuIcons = { Icons.House, Icons.ChartBar, Icons.User, Icons.Gear, Icons.WindowMaximize };
string[] menuItems = { "Dashboard", "Analytics", "Users", "Settings" };//, "Windows" };
@@ -220,13 +220,13 @@ private static void RenderSidebar()
Gui.Box("MenuItemIcon", i)
.Width(55)
.Height(50)
- .Text(menuIcons[i], Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleCenter).FontSize(19);
+ .Text(menuIcons[i], Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleCenter).FontSize(19);
Gui.Box("MenuItem", i)
.Width(100)
.PositionType(PositionType.SelfDirected)
.Left(50 + 15)
- .Text(menuItems[i], Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleCenter).FontSize(19);
+ .Text(menuItems[i], Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleCenter).FontSize(19);
}
}
@@ -238,7 +238,7 @@ private static void RenderSidebar()
.Margin(15)
.Height(Gui.Auto)
.Rounded(8)
- .BackgroundColor(Themes.primaryColor)
+ .BackgroundColor(Themes.primary)
.AspectRatio(0.5f)
.Enter())
{
@@ -254,7 +254,7 @@ private static void RenderSidebar()
.Style("button")
.Height(30)
.BackgroundColor(Color.White)
- .Text("Upgrade", Fonts.arial).TextColor(Themes.primaryColor).Alignment(TextAlignment.MiddleCenter).FontSize(19)
+ .Text("Upgrade", Fonts.arial).TextColor(Themes.primary).Alignment(TextAlignment.MiddleCenter).FontSize(19)
.OnClick((rect) => Console.WriteLine("Upgrade clicked"));
}
}
@@ -294,7 +294,7 @@ private static void RenderTabsNavigation()
{
int index = i;
bool isSelected = i == selectedTabIndex;
- Color tabColor = isSelected ? Themes.primaryColor : Themes.lightTextColor;
+ Color tabColor = isSelected ? Themes.primary : Themes.baseContent;
double tabWidth = 1.0f / tabNames.Length;
using (Gui.Box("Tab", i)
@@ -309,7 +309,7 @@ private static void RenderTabsNavigation()
{
Gui.Box("TabIndicator", i)
.Height(4)
- .BackgroundColor(Themes.primaryColor)
+ .BackgroundColor(Themes.primary)
.Rounded(2);
}
}
@@ -356,10 +356,10 @@ private static void RenderDashboardTab()
{
Gui.Box("StatLabel", i)
.Height(Gui.Pixels(25))
- .Text(statNames[i], Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleLeft).FontSize(19);
+ .Text(statNames[i], Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19);
Gui.Box("StatValue", i)
- .Text(statValues[i], Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(32);
+ .Text(statValues[i], Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(32);
}
}
}
@@ -374,7 +374,7 @@ private static void RenderDashboardTab()
using (Gui.Box("ChartArea")
.Width(Gui.Stretch(0.7f))
.Rounded(8)
- .BackgroundColor(Themes.cardBackground)
+ .BackgroundColor(Themes.base200)
//.Style(BoxStyle.SolidRounded(cardBackground, 8f))
.Enter())
{
@@ -385,7 +385,7 @@ private static void RenderDashboardTab()
.Enter())
{
using (Gui.Box("ChartTitle")
- .Text("Performance Overview", Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(26)
+ .Text("Performance Overview", Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(26)
.Enter()) { }
using (Gui.Row("ChartControls")
@@ -400,12 +400,12 @@ private static void RenderDashboardTab()
.Height(30)
.Rounded(8)
.Margin(5, 5, 0, 0)
- .BackgroundColor(period == "Week" ? Themes.primaryColor : Color.FromArgb(50, 0, 0, 0))
+ .BackgroundColor(period == "Week" ? Themes.primary : Color.FromArgb(50, 0, 0, 0))
.Hovered
- .BackgroundColor(Color.FromArgb(50, Themes.primaryColor))
+ .BackgroundColor(Color.FromArgb(50, Themes.primary))
.End()
.Transition(GuiProp.BackgroundColor, 0.2f)
- .Text(period, Fonts.arial).TextColor(period == "Week" ? Color.White : Themes.lightTextColor).Alignment(TextAlignment.MiddleCenter).FontSize(19)
+ .Text(period, Fonts.arial).TextColor(period == "Week" ? Color.White : Themes.baseContent).Alignment(TextAlignment.MiddleCenter).FontSize(19)
.OnClick((rect) => Console.WriteLine($"Period {period} clicked"))
.Enter()) { }
}
@@ -435,7 +435,7 @@ private static void RenderDashboardTab()
vg.BeginPath();
vg.MoveTo(rect.x, y);
vg.LineTo(rect.x + rect.width, y);
- vg.SetStrokeColor(Themes.lightTextColor);
+ vg.SetStrokeColor(Themes.baseContent);
vg.SetStrokeWidth(1);
vg.Stroke();
}
@@ -475,7 +475,7 @@ private static void RenderDashboardTab()
// Color.FromArgb(10, primaryColor));
//vg.SetFillPaint(paint);
vg.SaveState();
- vg.SetLinearBrush(rect.x, rect.y, rect.x, rect.y + rect.height, Color.FromArgb(100, Themes.primaryColor), Color.FromArgb(10, Themes.primaryColor));
+ vg.SetLinearBrush(rect.x, rect.y, rect.x, rect.y + rect.height, Color.FromArgb(100, Themes.primary), Color.FromArgb(10, Themes.primary));
vg.FillComplex();
vg.RestoreState();
@@ -498,7 +498,7 @@ private static void RenderDashboardTab()
vg.LineTo(x, y);
}
- vg.SetStrokeColor(Themes.primaryColor);
+ vg.SetStrokeColor(Themes.primary);
vg.SetStrokeWidth(3);
vg.Stroke();
@@ -519,7 +519,7 @@ private static void RenderDashboardTab()
vg.BeginPath();
vg.Circle(x, y, 4);
- vg.SetFillColor(Themes.primaryColor);
+ vg.SetFillColor(Themes.primary);
vg.Fill();
}
@@ -537,7 +537,7 @@ private static void RenderDashboardTab()
{
// Activity panel
using (Gui.Box("ActivityPanel")
- .BackgroundColor(Themes.cardBackground)
+ .BackgroundColor(Themes.base200)
//.Style(BoxStyle.SolidRounded(cardBackground, 8f))
.Rounded(8)
.Enter())
@@ -546,7 +546,7 @@ private static void RenderDashboardTab()
using (Gui.Box("PanelHeader")
.Height(60)
.Margin(20, 20, 20, 0)
- .Text("Recent Activity", Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(26)
+ .Text("Recent Activity", Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(26)
.Enter()) { }
// Activity items
@@ -587,12 +587,12 @@ private static void RenderDashboardTab()
using (Gui.Box("ActivityText", i)
.Height(Gui.Pixels(20))
.Margin(0, 0, 15, 0)
- .Text(activities[i], Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(19)
+ .Text(activities[i], Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(19)
.Enter()) { }
using (Gui.Box("ActivityTime", i)
.Height(Gui.Pixels(20))
- .Text(timestamps[i], Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleLeft).FontSize(19)
+ .Text(timestamps[i], Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19)
.Enter()) { }
}
}
@@ -614,7 +614,7 @@ private static void RenderAnalyticsTab()
.Enter())
{
using (Gui.Box("AnalyticsContent")
- .BackgroundColor(Themes.cardBackground)
+ .BackgroundColor(Themes.base200)
//.Style(BoxStyle.SolidRounded(cardBackground, 8f))
.Margin(0, 15 / 2, 15, 0)
.Enter())
@@ -623,7 +623,7 @@ private static void RenderAnalyticsTab()
using (Gui.Box("AnalyticsHeader")
.Height(80)
.Margin(20)
- .Text("Analytics Dashboard", Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(32)
+ .Text("Analytics Dashboard", Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(32)
.Enter()) { }
// Interactive slider as a demo control
@@ -634,7 +634,7 @@ private static void RenderAnalyticsTab()
{
using (Gui.Box("SliderLabel")
.Height(30)
- .Text($"Green Amount: {sliderValue:F2}", Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(26)
+ .Text($"Green Amount: {sliderValue:F2}", Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(26)
.Enter()) { }
// Slider control
@@ -646,7 +646,7 @@ private static void RenderAnalyticsTab()
}
using (Gui.Box("ScrollTest")
- .BackgroundColor(Themes.cardBackground)
+ .BackgroundColor(Themes.base200)
//.Style(BoxStyle.SolidRounded(cardBackground, 8f))
.Margin(15 / 2, 0, 15, 0)
.Enter())
@@ -706,7 +706,7 @@ private static void RenderAnalyticsTab()
.Height(70)
.Margin(10, 10, 5, 5)
.BackgroundColor(Color.FromArgb(230, itemColor))
- .BorderColor(Themes.isDark ? Color.FromArgb(50, 255, 255, 255) : Color.FromArgb(50, 0, 0, 0))
+ .BorderColor(Color.FromArgb(50, 255, 255, 255))
.BorderWidth(1)
.Rounded(12)
.Enter())
@@ -721,7 +721,7 @@ private static void RenderAnalyticsTab()
.Height(50)
.Rounded(25)
.BackgroundColor(Color.FromArgb(60, 255, 255, 255))
- .Text(icon, Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleCenter).FontSize(26)
+ .Text(icon, Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleCenter).FontSize(26)
.Enter()) { }
// Content
@@ -731,11 +731,11 @@ private static void RenderAnalyticsTab()
{
using (Gui.Box("CardTitle", i)
.Height(25)
- .Text($"Item {i}", Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(26)
+ .Text($"Item {i}", Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(26)
.Enter()) { }
using (Gui.Box("CardDescription", i)
- .Text("Interactive card with animations", Fonts.arial).TextColor(Color.FromArgb(200, Themes.textColor)).Alignment(TextAlignment.MiddleLeft).FontSize(19)
+ .Text("Interactive card with animations", Fonts.arial).TextColor(Color.FromArgb(200, Themes.primaryContent)).Alignment(TextAlignment.MiddleLeft).FontSize(19)
.Enter()) { }
}
}
@@ -757,7 +757,7 @@ private static void RenderProfileTab()
// Left panel - profile info
using (Gui.Column("ProfileDetails")
.Width(Gui.Stretch(0.4f))
- .BackgroundColor(Themes.cardBackground)
+ .BackgroundColor(Themes.base200)
//.Style(BoxStyle.SolidRounded(cardBackground, 8f))
.Enter())
{
@@ -779,7 +779,7 @@ private static void RenderProfileTab()
using (Gui.Box("Avatar")
.Width(120)
.Height(120)
- .BackgroundColor(Themes.secondaryColor)
+ .BackgroundColor(Themes.base300)
//.Style(BoxStyle.SolidRounded(secondaryColor, 60f))
.Text("J", Fonts.arial).TextColor(Color.White).Alignment(TextAlignment.MiddleCenter).FontSize(48)
.Enter()) { }
@@ -792,13 +792,13 @@ private static void RenderProfileTab()
// User name
using (Gui.Box("UserName")
.Height(40)
- .Text("John Doe", Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleCenter).FontSize(32)
+ .Text("John Doe", Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleCenter).FontSize(32)
.Enter()) { }
// User title
using (Gui.Box("UserTitle")
.Height(30)
- .Text("Senior Developer", Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleCenter).FontSize(26)
+ .Text("Senior Developer", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleCenter).FontSize(26)
.Enter()) { }
}
@@ -819,12 +819,12 @@ private static void RenderProfileTab()
{
using (Gui.Box("StatValue", i)
.Height(40)
- .Text(statValues[i], Fonts.arial).TextColor(Themes.primaryColor).Alignment(TextAlignment.MiddleCenter).FontSize(32)
+ .Text(statValues[i], Fonts.arial).TextColor(Themes.primary).Alignment(TextAlignment.MiddleCenter).FontSize(32)
.Enter()) { }
using (Gui.Box("StatLabel", i)
.Height(30)
- .Text(statLabels[i], Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleCenter).FontSize(19)
+ .Text(statLabels[i], Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleCenter).FontSize(19)
.Enter()) { }
}
}
@@ -846,11 +846,11 @@ private static void RenderProfileTab()
{
using (Gui.Box("ContactLabel", i)
.Width(100)
- .Text(contactLabels[i] + ":", Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleLeft).FontSize(19)
+ .Text(contactLabels[i] + ":", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19)
.Enter()) { }
using (Gui.Box("ContactValue", i)
- .Text(contactValues[i], Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(19)
+ .Text(contactValues[i], Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(19)
.Enter()) { }
}
}
@@ -866,7 +866,7 @@ private static void RenderProfileTab()
// Activity tracker
using (Gui.Box("ActivityTracker")
.Height(Gui.Stretch(0.6f))
- .BackgroundColor(Themes.cardBackground)
+ .BackgroundColor(Themes.base200)
//.Style(BoxStyle.SolidRounded(cardBackground, 8f))
.Enter())
{
@@ -874,7 +874,7 @@ private static void RenderProfileTab()
using (Gui.Box("ActivityHeader")
.Height(60)
.Margin(20, 20, 0, 0)
- .Text("Activity Tracker", Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(26)
+ .Text("Activity Tracker", Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(26)
.Enter()) { }
// Week days
@@ -887,7 +887,7 @@ private static void RenderProfileTab()
foreach (var day in days)
{
using (Gui.Box("Day", day.GetHashCode())
- .Text(day, Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleCenter).FontSize(19)
+ .Text(day, Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleCenter).FontSize(19)
.Enter()) { }
}
}
@@ -924,7 +924,7 @@ private static void RenderProfileTab()
// Apply color based on intensity
int alpha = (int)(40 + value * 215);
- vg.SetFillColor(Color.FromArgb(alpha, Themes.primaryColor));
+ vg.SetFillColor(Color.FromArgb(alpha, Themes.primary));
vg.Fill();
}
}
@@ -935,7 +935,7 @@ private static void RenderProfileTab()
// Skills section
using (Gui.Box("SkillsSection")
.Height(Gui.Stretch(0.4f))
- .BackgroundColor(Themes.cardBackground)
+ .BackgroundColor(Themes.base200)
//.Style(BoxStyle.SolidRounded(cardBackground, 8f))
.Margin(0, 0, 15, 0)
.Enter())
@@ -944,7 +944,7 @@ private static void RenderProfileTab()
using (Gui.Box("SkillsHeader")
.Height(20)
.Margin(20, 20, 20, 0)
- .Text("Skills", Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(26)
+ .Text("Skills", Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(26)
.Enter()) { }
// Skill bars
@@ -965,7 +965,7 @@ private static void RenderProfileTab()
// Skill label
using (Gui.Box("SkillLabel", i)
.Height(25)
- .Text(skills[i], Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(19)
+ .Text(skills[i], Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(19)
.Enter()) { }
// Skill bar
@@ -987,7 +987,7 @@ private static void RenderProfileTab()
// Percentage label
using (Gui.Box("SkillPercent", i)
.Width(40)
- .Text($"{animatedLevel * 100:F0}%", Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.Right).FontSize(19)
+ .Text($"{animatedLevel * 100:F0}%", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.Right).FontSize(19)
.Enter()) { }
}
}
@@ -1029,10 +1029,10 @@ private static void RenderSettingsTab()
for (int i = 0; i < categories.Length; i++)
{
bool isSelected = i == 0;
- Color itemTextColor = isSelected ? Themes.primaryColor : Themes.textColor;
+ Color itemTextColor = isSelected ? Themes.primary : Themes.primaryContent;
var index = i;
- Button.Outline("SettingsCat", $" {categories[i]}", i)
+ Button.Secondary("SettingsCat", $" {categories[i]}", i)
.Margin(10, 10, 5, 5)
// .StyleIf(isSelected, "period-button-selected")
.OnClick((rect) => { Console.WriteLine($"Category {categories[index]} clicked"); });
@@ -1049,7 +1049,7 @@ private static void RenderSettingsTab()
Gui.Box("SettingsHeader")
.Height(80)
.Margin(20)
- .Text("General Settings", Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(32);
+ .Text("General Settings", Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(32);
// Toggle options - much cleaner now!
string[] options = {
@@ -1066,7 +1066,7 @@ private static void RenderSettingsTab()
{
// Option label
Gui.Box("SettingLabel", i)
- .Text(options[i], Fonts.arial).TextColor(Themes.textColor).Alignment(TextAlignment.MiddleLeft).FontSize(26);
+ .Text(options[i], Fonts.arial).TextColor(Themes.primaryContent).Alignment(TextAlignment.MiddleLeft).FontSize(26);
bool isOn = toggleState[i];
int index = i;
@@ -1127,20 +1127,20 @@ private static void RenderFooter()
using (Gui.Row("Footer")
.Height(50)
.Rounded(8)
- .BackgroundColor(Themes.cardBackground)
+ .BackgroundColor(Themes.base200)
.Margin(15, 15, 0, 15)
.Enter())
{
// Copyright
using (Gui.Box("Copyright")
.Margin(15, 0, 0, 0)
- .Text("© 2025 PaperUI Demo.", Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleLeft).FontSize(19)
+ .Text("© 2025 PaperUI Demo.", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19)
.Enter()) { }
// FPS Counter
- Gui.Box("FPS").Text($"FPS: {1f / Gui.DeltaTime:F1}", Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleLeft).FontSize(19);
- Gui.Box("NodeCounter").Text($"Nodes: {Gui.CountOfAllElements}", Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleLeft).FontSize(19);
- Gui.Box("MS").Text($"Frame ms: {Gui.MillisecondsSpent}", Fonts.arial).TextColor(Themes.lightTextColor).Alignment(TextAlignment.MiddleLeft).FontSize(19);
+ Gui.Box("FPS").Text($"FPS: {1f / Gui.DeltaTime:F1}", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19);
+ Gui.Box("NodeCounter").Text($"Nodes: {Gui.CountOfAllElements}", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19);
+ Gui.Box("MS").Text($"Frame ms: {Gui.MillisecondsSpent}", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19);
// Footer links
string[] links = { "Terms", "Privacy", "Contact", "Help" };
@@ -1151,7 +1151,7 @@ private static void RenderFooter()
{
using (Gui.Box("Link", link.GetHashCode())
.Width(Gui.Stretch(1f / links.Length))
- .Text(link, Fonts.arial).TextColor(Themes.primaryColor).Alignment(TextAlignment.MiddleCenter).FontSize(19)
+ .Text(link, Fonts.arial).TextColor(Themes.primary).Alignment(TextAlignment.MiddleCenter).FontSize(19)
.OnClick((rect) => Console.WriteLine($"Link {link} clicked"))
.Enter()) { }
}
diff --git a/Samples/Shared/PaperDemo.Tabs.cs b/Samples/Shared/PaperDemo.Tabs.cs
new file mode 100644
index 0000000..f6875a2
--- /dev/null
+++ b/Samples/Shared/PaperDemo.Tabs.cs
@@ -0,0 +1,197 @@
+using System.Formats.Asn1;
+
+using Prowl.PaperUI;
+using Prowl.PaperUI.LayoutEngine;
+
+using Shared.Tabs;
+
+namespace Shared
+{
+ public abstract class Tab
+ {
+ public string id;
+ public string title;
+ public double width;
+ protected Paper Gui;
+
+ public Tab(Paper gui)
+ {
+ this.Gui = gui;
+ }
+
+ public virtual void Draw()
+ {
+ Gui.Box("Tab Title").Text("[" + this.title + " Tab]", Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Left(8)
+ .Alignment(TextAlignment.MiddleCenter);
+ }
+
+ ///
+ /// When the tab becomes focused
+ ///
+ public virtual void Focus()
+ {
+ Console.WriteLine("FOCUS " + GetType().ToString());
+ }
+
+ ///
+ /// When the tab loses focus
+ ///
+ public virtual void Blur()
+ {
+ Console.WriteLine("BLUR " + GetType().ToString());
+ }
+ }
+
+ public class TabsManager
+ {
+ public string ActiveTabId = "";
+ public Dictionary Entries = new Dictionary();
+
+ private Paper Gui;
+ private Tab FocusedTab;
+
+ public TabsManager(Paper gui)
+ {
+ this.Gui = gui;
+
+ AddTab("assets", new AssetsTab(gui));
+ AddTab("files", new FilesTab(gui));
+ AddTab("chart", new ChartTab(gui));
+ AddTab("hierarchy", new HierarchyTab(gui));
+ AddTab("inspector", new InspectorTab(gui));
+ AddTab("settings", new SettingsTab(gui));
+ AddTab("example", new ExampleTab(gui));
+ AddTab("pong", new PongTab(gui));
+ AddTab("platformer", new PlatformerTab(gui));
+ AddTab("slimefriends", new SlimeFriendsTab(gui));
+ AddTab("asteroids", new AsteroidsTab(gui));
+ }
+
+ public void AddTab(string id, Tab tab)
+ {
+ Entries.Add(id, tab);
+ ActiveTabId = id;
+ }
+
+ public void DrawGroup(string[] tabs)
+ {
+ var group = Entries.Values.Where((tab, i) => tabs.Contains(tab.id)).ToArray();
+
+ // Get stored active tab ID or use first tab as default
+ var storageKey = string.Join("_", tabs) + "_activeTabId"; // auto invalidate the key if the group changes
+ var localGroupTabId = Gui.GetElementStorage(storageKey, tabs[0]);
+
+ var currentElement = Gui.CurrentParent;
+
+ TabsDisplay("Tabs Container", group, storageKey, currentElement, localGroupTabId);
+
+ if (tabs.Length == 0 || group.Length == 0) return;
+
+ // If active tab exists in current group, draw its body
+ if (Entries.TryGetValue(localGroupTabId, out Tab tab))
+ {
+ using (Gui.Column("Body")
+ .OnPress((_) =>
+ {
+ ActiveTabId = tab.id;
+ Console.WriteLine("Pressed " + tab.GetType().ToString());
+ })
+ .OnEnter((_) =>
+ {
+ // blur old focused tab
+ // then call focus on the new focused tab
+ FocusedTab?.Blur();
+ FocusedTab = tab;
+ FocusedTab.Focus();
+ })
+ .BackgroundColor(Themes.base200)
+ .Enter())
+ {
+ tab.Draw();
+ }
+ }
+ }
+
+ private void TabsDisplay(string id, Tab[] tabs, string tabIdStorageKey, ElementHandle elementWithTabIdStorage, string localGroupId)
+ {
+ using (Gui.Row("Tabs " + id).Height(28).Enter())
+ {
+ foreach (var tab in tabs)
+ {
+ if (tab.id == localGroupId)
+ {
+ using (Gui.Box("Active Tab" + tab.id).Width(tab.width).Height(28).Left(5)
+ .OnClick((_) =>
+ {
+ Console.WriteLine("clicked tab " + tab.id);
+ ActiveTabId = tab.id;
+ Gui.SetElementStorage(elementWithTabIdStorage, tabIdStorageKey, tab.id);
+ })
+ .Enter())
+ {
+ if (tab.id == ActiveTabId)
+ {
+ Gui.Box("Highlight").Layer(Layer.Overlay).PositionType(PositionType.SelfDirected).RoundedTop(3).BackgroundColor(Themes.primary).Height(3);
+ }
+
+ Gui.Box("tab 1")
+ .RoundedTop(3)
+ .BackgroundColor(Themes.base200)
+ .Text(tab.title, Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Alignment(TextAlignment.MiddleCenter);
+ }
+ }
+ else
+ {
+ Gui.Box("Inactive Tab" + tab.id)
+ .OnClick((_) =>
+ {
+ Gui.SetElementStorage(elementWithTabIdStorage, tabIdStorageKey, tab.id);
+ Console.WriteLine("clicked tab " + tab.id);
+ ActiveTabId = tab.id;
+ })
+ .RoundedTop(3)
+ .Left(5)
+ .Width(tab.width).Height(28)
+ .BackgroundColor(Themes.base100)
+ .Text(tab.title, Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Alignment(TextAlignment.MiddleCenter)
+ .Hovered
+ .BackgroundColor(Themes.base300)
+ .End();
+ }
+ }
+
+ Gui.Box("Plus Tab")
+ .Width(20).Height(20)
+ .BackgroundColor(Themes.base100)
+ .Text("+", Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Alignment(TextAlignment.MiddleCenter)
+ .Rounded(5)
+ .Margin(4)
+ .Hovered
+ .BackgroundColor(Themes.base300)
+ .End();
+
+ Gui.Box("Spacer"); // automatically grows
+
+ Gui.Box("Options")
+ .Width(20).Height(20)
+ .BackgroundColor(Themes.base100)
+ .FontSize(12)
+ .Text(Icons.Grip, Fonts.arial).TextColor(Themes.baseContent)
+ .Alignment(TextAlignment.MiddleCenter)
+ .Rounded(5)
+ .Margin(4)
+ .Hovered
+ .BackgroundColor(Themes.base300)
+ .End();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/PaperDemo.Theme.cs b/Samples/Shared/PaperDemo.Theme.cs
index aee8222..f20e820 100644
--- a/Samples/Shared/PaperDemo.Theme.cs
+++ b/Samples/Shared/PaperDemo.Theme.cs
@@ -1,330 +1,40 @@
using System.Drawing;
-using Prowl.PaperUI;
-
-// SHADCN discord theme colors
-// discovered through https://ui.jln.dev/
-// @layer base {
-//
-// .dark {
-// --background: 217.5 9.09% 17.25%; /* 40, 43, 48 */
-// --foreground: 334 34% 98%; /* 255, 247, 252 */
-// --muted: 210 9.09% 12.94%; /* 30, 32, 36 */
-// --muted-foreground: 334 0% 60.77%; /* 155, 155, 155 */
-// --popover: 210 9.09% 12.94%; /* 30, 32, 36 */
-// --popover-foreground: 334 34% 98%; /* 255, 247, 252 */
-// --card: 210 9.09% 12.94%; /* 30, 32, 36 */
-// --card-foreground: 334 34% 98%; /* 255, 247, 252 */
-// --border: 334 0% 18.46%; /* 47, 47, 47 */
-// --input: 214.29 5.04% 27.25%; /* 66, 68, 72 */
-// --primary: 226.73 58.43% 65.1%; /* 69, 135, 235 */
-// --primary-foreground: 0 0% 100%; /* 255, 255, 255 */
-// --secondary: 214.29 5.04% 27.25%; /* 66, 68, 72 */
-// --secondary-foreground: 334 0% 100%; /* 255, 255, 255 */
-// --accent: 217.5 9.09% 17.25%; /* 40, 43, 48 */
-// --accent-foreground: 226.73 58.43% 65.1%; /* 69, 135, 235 */
-// --destructive: 358.16 68.78% 53.53%; /* 230, 43, 52 */
-// --destructive-foreground: 0 0% 100%; /* 255, 255, 255 */
-// --ring: 217.5 9.09% 17.25%; /* 40, 43, 48 */
-// --chart-1: 226.73 58.43% 65.1%; /* 69, 135, 235 */
-// --chart-2: 214.29 5.04% 27.25%; /* 66, 68, 72 */
-// --chart-3: 217.5 9.09% 17.25%; /* 40, 43, 48 */
-// --chart-4: 214.29 5.04% 30.25%; /* 72, 74, 79 */
-// --chart-5: 226.73 61.43% 65.1%; /* 64, 135, 239 */
-// }
-//
-// :root {
-// --background: 0 0% 97.69%; /* 249, 249, 249 */
-// --foreground: 334 55% 1%; /* 4, 1, 3 */
-// --muted: 0 0% 93.85%; /* 239, 239, 239 */
-// --muted-foreground: 0 0% 10.2%; /* 26, 26, 26 */
-// --popover: 0 0% 100%; /* 255, 255, 255 */
-// --popover-foreground: 0 0% 10.2%; /* 26, 26, 26 */
-// --card: 0 0% 100%; /* 255, 255, 255 */
-// --card-foreground: 0 0% 13.73%; /* 35, 35, 35 */
-// --border: 0 0% 80.78%; /* 206, 206, 206 */
-// --input: 0 0% 80.78%; /* 206, 206, 206 */
-// --primary: 211.29 100% 50%; /* 0, 149, 255 */
-// --primary-foreground: 0 0% 100%; /* 255, 255, 255 */
-// --secondary: 0 0% 85.49%; /* 218, 218, 218 */
-// --secondary-foreground: 334 0% 10%; /* 26, 26, 26 */
-// --accent: 211.29 100% 50%; /* 0, 149, 255 */
-// --accent-foreground: 334 0% 100%; /* 255, 255, 255 */
-// --destructive: 3.19 100% 59.41%; /* 303, 1, 0 */
-// --destructive-foreground: 18 0% 100%; /* 255, 255, 255 */
-// --ring: 0 0% 60%; /* 153, 153, 153 */
-// --chart-1: 211.29 100% 50%; /* 0, 149, 255 */
-// --chart-2: 0 0% 85.49%; /* 218, 218, 218 */
-// --chart-3: 211.29 100% 50%; /* 0, 149, 255 */
-// --chart-4: 0 0% 88.49%; /* 226, 226, 226 */
-// --chart-5: 211.29 103% 50%; /* 0, 152, 255 */
-// --radius: 0.5rem;
-// }
-// }
namespace Shared
{
public static class Themes
{
//Theme
- public static Color backgroundColor;
- public static Color cardBackground;
- public static Color primaryColor;
- public static Color secondaryColor;
- public static Color textColor;
- public static Color lightTextColor;
+ public static Color base100;
+ public static Color base200;
+ public static Color base250;
+ public static Color base300;
+ public static Color base400;
+ public static Color baseContent; // text
+ public static Color primary;
+ public static Color primaryContent; // text
public static Color[] colorPalette;
- public static bool isDark;
public static void Initialize()
{
- ToggleTheme();
- }
-
- public static void ToggleTheme()
- {
- isDark = !isDark;
+ //Dark
+ base100 = Color.FromArgb(255, 31, 31, 36);
+ base200 = Color.FromArgb(255, 42, 42, 46);
+ base250 = Color.FromArgb(255, 54, 55, 59);
+ base300 = Color.FromArgb(255, 64, 64, 68);
+ base400 = Color.FromArgb(255, 70, 71, 76);
+ baseContent = Color.FromArgb(255, 230, 230, 230);
+ primary = Color.FromArgb(255, 69, 135, 235);
+ primaryContent = Color.FromArgb(255, 226, 232, 240);
+ colorPalette = [
+ Color.FromArgb(255, 94, 234, 212), // Cyan
+ Color.FromArgb(255, 162, 155, 254), // Purple
+ Color.FromArgb(255, 249, 115, 22), // Orange
+ Color.FromArgb(255, 248, 113, 113), // Red
+ Color.FromArgb(255, 250, 204, 21) // Yellow
+ ];
- if (isDark)
- {
- //Dark
- backgroundColor = Color.FromArgb(255, 40, 43, 48);
- cardBackground = Color.FromArgb(255, 30, 32, 36);
- primaryColor = Color.FromArgb(255, 69, 135, 235);
- secondaryColor = Color.FromArgb(255, 66, 68, 72);
- textColor = Color.FromArgb(255, 226, 232, 240);
- lightTextColor = Color.FromArgb(255, 148, 163, 184);
- colorPalette = [
- Color.FromArgb(255, 94, 234, 212), // Cyan
- Color.FromArgb(255, 162, 155, 254), // Purple
- Color.FromArgb(255, 249, 115, 22), // Orange
- Color.FromArgb(255, 248, 113, 113), // Red
- Color.FromArgb(255, 250, 204, 21) // Yellow
- ];
- }
- else
- {
- //Light
- backgroundColor = Color.FromArgb(255, 240, 240, 240);
- cardBackground = Color.FromArgb(255, 255, 255, 255);
- primaryColor = Color.FromArgb(255, 0, 149, 255);
- secondaryColor = Color.FromArgb(255, 218, 218, 218);
- textColor = Color.FromArgb(255, 31, 41, 55);//4, 1, 3);
- lightTextColor = Color.FromArgb(255, 107, 114, 128);
- colorPalette = [
- Color.FromArgb(255, 59, 130, 246), // Blue
- Color.FromArgb(255, 16, 185, 129), // Teal
- Color.FromArgb(255, 239, 68, 68), // Red
- Color.FromArgb(255, 245, 158, 11), // Amber
- Color.FromArgb(255, 139, 92, 246) // Purple
- ];
- }
-
- // Redefine styles with new theme colors
- DefineStyles();
Components.DefineStyles();
}
-
- public static void DefineStyles()
- {
- // Card styles with hover effects
- PaperDemo.Gui.CreateStyleFamily("card")
- .Base(new StyleTemplate()
- .BackgroundColor(cardBackground)
- .Rounded(8)
- .BoxShadow(0, 2, 6, 0, Color.FromArgb(100, 0, 0, 0))
- .Transition(GuiProp.BoxShadow, 0.2)
- .Transition(GuiProp.Rounded, 0.2)
- .Transition(GuiProp.BorderColor, 0.3)
- .Transition(GuiProp.BorderWidth, 0.2)
- .Transition(GuiProp.ScaleX, 0.2)
- .Transition(GuiProp.ScaleY, 0.2))
- .Hovered(new StyleTemplate()
- .Rounded(12)
- .BorderColor(primaryColor)
- .BorderWidth(2)
- .BoxShadow(0, 4, 12, 0, Color.FromArgb(160, 0, 0, 0))
- .Scale(1.05))
- .Register();
-
- // Button styles
- PaperDemo.Gui.CreateStyleFamily("button")
- .Base(new StyleTemplate()
- .Height(40)
- .Rounded(8)
- .BackgroundColor(Color.FromArgb(50, 0, 0, 0))
- .Transition(GuiProp.BackgroundColor, 0.2)
- .Transition(GuiProp.Rounded, 0.2)
- .Transition(GuiProp.ScaleX, 0.1)
- .Transition(GuiProp.ScaleY, 0.1))
- .Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(100, primaryColor))
- .Rounded(12))
- .Active(new StyleTemplate()
- .Scale(0.95)
- .BackgroundColor(Color.FromArgb(150, primaryColor)))
- .Register();
-
- // Primary button variant
- PaperDemo.Gui.CreateStyleFamily("button-primary")
- .Base(new StyleTemplate()
- .Height(50)
- .Rounded(8)
- .BackgroundColor(primaryColor)
- .BackgroundLinearGradient(0, 0, 0, 1, primaryColor, secondaryColor)
- .Transition(GuiProp.BackgroundGradient, 0.2)
- .Transition(GuiProp.BackgroundColor, 0.2)
- .Transition(GuiProp.ScaleX, 0.1)
- .Transition(GuiProp.ScaleY, 0.1))
- .Hovered(new StyleTemplate()
- .BackgroundColor(secondaryColor)
- .BackgroundLinearGradient(0, 0, 0, 1, secondaryColor, primaryColor))
- .Active(new StyleTemplate()
- .Scale(0.95)
- .BackgroundColor(Color.FromArgb(200, primaryColor))
- .BackgroundLinearGradient(0, 0, 0, 1, Color.FromArgb(200, primaryColor), Color.FromArgb(200, secondaryColor)))
- .Register();
-
- // Icon button styles
- PaperDemo.Gui.CreateStyleFamily("icon-button")
- .Base(new StyleTemplate()
- .Width(40)
- .Height(40)
- .Rounded(8)
- .BackgroundColor(Color.FromArgb(50, 0, 0, 0))
- .Transition(GuiProp.BackgroundColor, 0.2)
- .Transition(GuiProp.Rounded, 0.2))
- .Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(100, primaryColor))
- .Rounded(20))
- .Register();
-
- // Sidebar styles
- PaperDemo.Gui.CreateStyleFamily("sidebar")
- .Base(new StyleTemplate()
- .BackgroundColor(cardBackground)
- .Rounded(8)
- .Width(75)
- .Transition(GuiProp.Width, 0.25, Easing.EaseIn)
- .Transition(GuiProp.BorderColor, 0.75)
- .Transition(GuiProp.BorderWidth, 0.75)
- .Transition(GuiProp.Rounded, 0.25))
- .Hovered(new StyleTemplate()
- .Width(240)
- .BorderColor(primaryColor)
- .BorderWidth(3)
- .Rounded(16))
- .Register();
-
- // Menu item styles
- PaperDemo.Gui.CreateStyleFamily("menu-item")
- .Base(new StyleTemplate()
- .Height(50)
- .Margin(10, 10, 5, 5)
- .Rounded(8)
- .BorderColor(Color.Transparent)
- .BorderWidth(0)
- .Transition(GuiProp.BackgroundColor, 0.2)
- .Transition(GuiProp.BorderWidth, 0.1)
- .Transition(GuiProp.BorderColor, 0.2))
- .Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(20, primaryColor))
- .BorderWidth(2)
- .BorderColor(primaryColor))
- .Register();
-
- // Menu item selected state
- PaperDemo.Gui.RegisterStyle("menu-item-selected", new StyleTemplate()
- .BorderColor(primaryColor)
- .BorderWidth(2)
- .BackgroundColor(Color.FromArgb(30, primaryColor)));
-
- // Tab styles
- PaperDemo.Gui.CreateStyleFamily("tab")
- .Base(new StyleTemplate()
- .Transition(GuiProp.BackgroundColor, 0.2))
- .Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(20, primaryColor)))
- .Register();
-
- // Text field styles
- PaperDemo.Gui.CreateStyleFamily("text-field")
- .Base(new StyleTemplate()
- .Width(300)
- .Height(40)
- .Rounded(8)
- .BackgroundColor(Color.FromArgb(50, 0, 0, 0))
- .BorderColor(Color.Transparent)
- .BorderWidth(0)
- .Transition(GuiProp.BorderColor, 0.2)
- .Transition(GuiProp.BorderWidth, 0.2)
- .Transition(GuiProp.BackgroundColor, 0.2))
- .Focused(new StyleTemplate()
- .BorderColor(primaryColor)
- .BorderWidth(2)
- .BackgroundColor(Color.FromArgb(80, 0, 0, 0)))
- .Register();
-
- // Stat card styles
- PaperDemo.Gui.CreateStyleFamily("stat-card")
- .Base(new StyleTemplate()
- .BackgroundColor(cardBackground)
- .Rounded(8)
- .BoxShadow(4, 4, 5, 0, Color.FromArgb(100, 0, 0, 0))
- .Transition(GuiProp.BoxShadow, 0.2)
- .Transition(GuiProp.Rounded, 0.2)
- .Transition(GuiProp.BorderColor, 0.3)
- .Transition(GuiProp.BorderWidth, 0.2)
- .Transition(GuiProp.ScaleX, 0.2)
- .Transition(GuiProp.ScaleY, 0.2))
- .Hovered(new StyleTemplate()
- .Rounded(12)
- .BorderWidth(2)
- .BoxShadow(0, 0, 4, 0, Color.FromArgb(0, 0, 0, 0))
- .Scale(1.05))
- .Register();
-
- // Period button styles
- PaperDemo.Gui.CreateStyleFamily("period-button")
- .Base(new StyleTemplate()
- .Width(60)
- .Height(30)
- .Rounded(8)
- .Margin(5, 5, 0, 0)
- .BackgroundColor(Color.FromArgb(50, 0, 0, 0))
- .Transition(GuiProp.BackgroundColor, 0.2))
- .Hovered(new StyleTemplate()
- .BackgroundColor(Color.FromArgb(50, primaryColor)))
- .Register();
-
- PaperDemo.Gui.RegisterStyle("period-button-selected", new StyleTemplate()
- .BackgroundColor(primaryColor));
-
- // Activity icon styles
- PaperDemo.Gui.RegisterStyle("activity-icon", new StyleTemplate()
- .Width(40)
- .Height(40)
- .Rounded(20));
-
- // Separator style
- PaperDemo.Gui.RegisterStyle("separator", new StyleTemplate()
- .Height(1)
- .Margin(15, 15, 0, 0)
- .BackgroundColor(Color.FromArgb(30, 0, 0, 0)));
-
- // Container styles
- PaperDemo.Gui.RegisterStyle("container", new StyleTemplate()
- .BackgroundColor(cardBackground)
- .Rounded(8));
-
- // Skill bar styles
- PaperDemo.Gui.RegisterStyle("skill-bar-bg", new StyleTemplate()
- .Height(15)
- .Rounded(7.5)
- .BackgroundColor(Color.FromArgb(30, 0, 0, 0)));
-
- PaperDemo.Gui.RegisterStyle("skill-bar-fg", new StyleTemplate()
- .Rounded(7.5));
- }
-
}
-}
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/AssetsTab.cs b/Samples/Shared/Tabs/AssetsTab.cs
new file mode 100644
index 0000000..0dc9592
--- /dev/null
+++ b/Samples/Shared/Tabs/AssetsTab.cs
@@ -0,0 +1,58 @@
+using Prowl.PaperUI;
+
+namespace Shared.Tabs
+{
+ public class AssetsTab : Tab
+ {
+ public AssetsTab(Paper gui) : base(gui)
+ {
+ title = "Assets";
+ id = "assets";
+ width = 65;
+ }
+
+ public override void Draw()
+ {
+ using (Gui.Box("Search Box").Margin(5).Height(28).Bottom(8).Rounded(5).BackgroundColor(Themes.base300).Enter())
+ {
+ Gui.Box("Search").Text("Filter...", Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Left(8)
+ .Alignment(TextAlignment.MiddleLeft);
+ }
+
+ using (Gui.Column("Assets view").Margin(5).ColBetween(8).SetScroll(Scroll.ScrollY).Enter())
+ {
+ for (int i = 0; i < 64; i++)
+ {
+ using (Gui.Row("Folders and assets", i).Height(100).RowBetween(8).Margin(5).Enter())
+ {
+ Gui.Box("Folder")
+ .Width(52)
+ .Height(100)
+ .FontSize(32)
+ .Text(Icons.Folder, Fonts.arial);
+
+ Gui.Box("Folder")
+ .Width(52)
+ .Height(100)
+ .FontSize(32)
+ .Text(Icons.Folder, Fonts.arial);
+
+ Gui.Box("Folder")
+ .Width(52)
+ .Height(100)
+ .FontSize(32)
+ .Text(Icons.Folder, Fonts.arial);
+
+ Gui.Box("Folder")
+ .Width(52)
+ .Height(100)
+ .FontSize(32)
+ .Text(Icons.Folder, Fonts.arial);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/AsteroidsTab.cs b/Samples/Shared/Tabs/AsteroidsTab.cs
new file mode 100644
index 0000000..4dd879a
--- /dev/null
+++ b/Samples/Shared/Tabs/AsteroidsTab.cs
@@ -0,0 +1,589 @@
+using Prowl.PaperUI;
+using Prowl.Vector;
+using Prowl.Scribe;
+using System;
+using System.Collections.Generic;
+
+namespace Shared.Tabs
+{
+ public class AsteroidsTab : Tab
+ {
+ const int SHIP_SIZE = 15;
+ const double SHIP_ROTATION_SPEED = 4.0;
+ const double SHIP_THRUST = 300.0;
+ const double SHIP_DRAG = 0.98;
+ const double SHIP_MAX_SPEED = 400.0;
+ const double BULLET_SPEED = 500.0;
+ const double BULLET_LIFETIME = 1.5;
+ const int MAX_BULLETS = 5;
+ const double FIRE_COOLDOWN = 0.2;
+ const int STARTING_LIVES = 3;
+ const double INVULNERABILITY_TIME = 2.0;
+ const double RESPAWN_DELAY = 1.5;
+ const int STARTING_ASTEROIDS = 4;
+
+ enum AsteroidSize
+ {
+ Large,
+ Medium,
+ Small
+ }
+
+ struct Ship
+ {
+ public Vector2 position;
+ public Vector2 velocity;
+ public double rotation;
+ public bool thrusting;
+ public double invulnerabilityTimer;
+
+ public Ship(Vector2 position)
+ {
+ this.position = position;
+ this.velocity = Vector2.zero;
+ this.rotation = -Math.PI / 2;
+ this.thrusting = false;
+ this.invulnerabilityTimer = INVULNERABILITY_TIME;
+ }
+ }
+
+ struct Bullet
+ {
+ public Vector2 position;
+ public Vector2 velocity;
+ public double lifetime;
+
+ public Bullet(Vector2 position, Vector2 velocity)
+ {
+ this.position = position;
+ this.velocity = velocity;
+ this.lifetime = BULLET_LIFETIME;
+ }
+ }
+
+ struct Asteroid
+ {
+ public Vector2 position;
+ public Vector2 velocity;
+ public double rotation;
+ public double rotationSpeed;
+ public AsteroidSize size;
+ public Vector2[] shape;
+
+ public Asteroid(Vector2 position, Vector2 velocity, AsteroidSize size, System.Random rand)
+ {
+ this.position = position;
+ this.velocity = velocity;
+ this.size = size;
+ this.rotation = rand.NextDouble() * Math.PI * 2;
+ this.rotationSpeed = (rand.NextDouble() - 0.5) * 2.0;
+
+ // Generate random asteroid shape
+ int points = rand.Next(8, 12);
+ this.shape = new Vector2[points];
+ double baseRadius = GetRadius(size);
+
+ for (int i = 0; i < points; i++)
+ {
+ double angle = (i / (double)points) * Math.PI * 2;
+ double radius = baseRadius * (0.7 + rand.NextDouble() * 0.3);
+ this.shape[i] = new Vector2(
+ Math.Cos(angle) * radius,
+ Math.Sin(angle) * radius
+ );
+ }
+ }
+
+ public static double GetRadius(AsteroidSize size)
+ {
+ return size switch
+ {
+ AsteroidSize.Large => 40,
+ AsteroidSize.Medium => 25,
+ AsteroidSize.Small => 15,
+ _ => 40
+ };
+ }
+
+ public static int GetScore(AsteroidSize size)
+ {
+ return size switch
+ {
+ AsteroidSize.Large => 20,
+ AsteroidSize.Medium => 50,
+ AsteroidSize.Small => 100,
+ _ => 20
+ };
+ }
+ }
+
+ struct Particle
+ {
+ public Vector2 position;
+ public Vector2 velocity;
+ public double lifetime;
+ public double maxLifetime;
+
+ public Particle(Vector2 position, Vector2 velocity, double lifetime)
+ {
+ this.position = position;
+ this.velocity = velocity;
+ this.lifetime = lifetime;
+ this.maxLifetime = lifetime;
+ }
+ }
+
+ Ship ship;
+ List bullets = new List();
+ List asteroids = new List();
+ List particles = new List();
+
+ bool leftPressed = false;
+ bool rightPressed = false;
+ bool thrustPressed = false;
+ bool shootPressed = false;
+
+ double fireCooldownTimer = 0;
+ int score = 0;
+ int lives = STARTING_LIVES;
+ bool gameOver = false;
+ double respawnTimer = 0;
+ bool shipActive = true;
+ int level = 1;
+
+ Rect _rect;
+ System.Random rand = new System.Random();
+
+ TextLayoutSettings scoreLayout = new TextLayoutSettings
+ {
+ Font = Fonts.arial,
+ PixelSize = 20,
+ Alignment = Prowl.Scribe.TextAlignment.Left
+ };
+
+ TextLayoutSettings gameOverLayout = new TextLayoutSettings
+ {
+ Font = Fonts.arial,
+ PixelSize = 32,
+ Alignment = Prowl.Scribe.TextAlignment.Center
+ };
+
+ public AsteroidsTab(Paper gui) : base(gui)
+ {
+ title = "Asteroids";
+ id = "asteroids";
+ width = 90;
+
+ InitializeGame();
+ }
+
+ private void InitializeGame()
+ {
+ ship = new Ship(new Vector2(0, 0));
+ bullets.Clear();
+ asteroids.Clear();
+ particles.Clear();
+ score = 0;
+ lives = STARTING_LIVES;
+ gameOver = false;
+ shipActive = true;
+ level = 1;
+ respawnTimer = 0;
+
+ SpawnAsteroids(STARTING_ASTEROIDS);
+ }
+
+ private void SpawnAsteroids(int count)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ Vector2 position;
+ do
+ {
+ position = new Vector2(
+ rand.NextDouble() * (_rect.width > 0 ? _rect.width : 800),
+ rand.NextDouble() * (_rect.height > 0 ? _rect.height : 600)
+ );
+ } while (shipActive && (position - ship.position).magnitude < 150);
+
+ Vector2 velocity = new Vector2(
+ (rand.NextDouble() - 0.5) * 100,
+ (rand.NextDouble() - 0.5) * 100
+ );
+
+ asteroids.Add(new Asteroid(position, velocity, AsteroidSize.Large, rand));
+ }
+ }
+
+ private void SpawnParticles(Vector2 position, int count, double speed)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ double angle = rand.NextDouble() * Math.PI * 2;
+ Vector2 velocity = new Vector2(
+ Math.Cos(angle) * speed * (0.5 + rand.NextDouble()),
+ Math.Sin(angle) * speed * (0.5 + rand.NextDouble())
+ );
+ particles.Add(new Particle(position, velocity, 0.5 + rand.NextDouble() * 0.5));
+ }
+ }
+
+ private void RespawnShip()
+ {
+ ship = new Ship(new Vector2(_rect.width / 2, _rect.height / 2));
+ shipActive = true;
+ }
+
+ private void DestroyShip()
+ {
+ SpawnParticles(ship.position, 20, 150);
+ shipActive = false;
+ lives--;
+
+ if (lives <= 0)
+ {
+ gameOver = true;
+ }
+ else
+ {
+ respawnTimer = RESPAWN_DELAY;
+ }
+ }
+
+ private Vector2 WrapPosition(Vector2 position)
+ {
+ double x = position.x;
+ double y = position.y;
+
+ if (x < 0) x = _rect.width;
+ if (x > _rect.width) x = 0;
+ if (y < 0) y = _rect.height;
+ if (y > _rect.height) y = 0;
+
+ return new Vector2(x, y);
+ }
+
+ private void SplitAsteroid(Asteroid asteroid)
+ {
+ score += Asteroid.GetScore(asteroid.size);
+ SpawnParticles(asteroid.position, 10, 100);
+
+ if (asteroid.size != AsteroidSize.Small)
+ {
+ AsteroidSize newSize = asteroid.size == AsteroidSize.Large ?
+ AsteroidSize.Medium : AsteroidSize.Small;
+
+ for (int i = 0; i < 2; i++)
+ {
+ double angle = rand.NextDouble() * Math.PI * 2;
+ Vector2 velocity = new Vector2(
+ Math.Cos(angle) * 120,
+ Math.Sin(angle) * 120
+ );
+ asteroids.Add(new Asteroid(asteroid.position, velocity, newSize, rand));
+ }
+ }
+ }
+
+ public override void Focus()
+ {
+ Console.WriteLine("Asteroids is focused");
+ }
+
+ public override void Blur()
+ {
+ Console.WriteLine("Asteroids is blurred");
+ }
+
+ public override void Draw()
+ {
+ using (Gui.Box("Container")
+ .Margin(5)
+ .Focused
+ .BorderWidth(2)
+ .BorderColor(Themes.primary)
+ .End()
+ .OnKeyPressed((e) =>
+ {
+ if (e.Key == PaperKey.A || e.Key == PaperKey.Left) leftPressed = true;
+ if (e.Key == PaperKey.D || e.Key == PaperKey.Right) rightPressed = true;
+ if (e.Key == PaperKey.W || e.Key == PaperKey.Up) thrustPressed = true;
+ if (e.Key == PaperKey.Space) shootPressed = true;
+ if (e.Key == PaperKey.R && gameOver) InitializeGame();
+ })
+ .Enter())
+ {
+ Gui.AddActionElement((canvas, rect) =>
+ {
+ _rect = rect;
+ var deltaTime = Gui.DeltaTime;
+
+ // Initialize ship position if needed
+ if (ship.position.x == 0 && ship.position.y == 0)
+ {
+ ship.position = new Vector2(rect.width / 2, rect.height / 2);
+ }
+
+ if (!gameOver)
+ {
+ // Handle respawn
+ if (!shipActive)
+ {
+ respawnTimer -= deltaTime;
+ if (respawnTimer <= 0)
+ {
+ RespawnShip();
+ }
+ }
+
+ // Update ship
+ if (shipActive)
+ {
+ if (leftPressed) ship.rotation -= SHIP_ROTATION_SPEED * deltaTime;
+ if (rightPressed) ship.rotation += SHIP_ROTATION_SPEED * deltaTime;
+
+ ship.thrusting = thrustPressed;
+
+ if (thrustPressed)
+ {
+ Vector2 thrust = new Vector2(
+ Math.Cos(ship.rotation),
+ Math.Sin(ship.rotation)
+ ) * SHIP_THRUST * deltaTime;
+ ship.velocity += thrust;
+
+ // Spawn thrust particles
+ if (rand.NextDouble() < 0.5)
+ {
+ Vector2 exhaustPos = ship.position - new Vector2(
+ Math.Cos(ship.rotation) * SHIP_SIZE,
+ Math.Sin(ship.rotation) * SHIP_SIZE
+ );
+ SpawnParticles(exhaustPos, 1, 50);
+ }
+ }
+
+ ship.velocity *= SHIP_DRAG;
+ double speed = ship.velocity.magnitude;
+ if (speed > SHIP_MAX_SPEED)
+ {
+ ship.velocity = ship.velocity.normalized * SHIP_MAX_SPEED;
+ }
+
+ ship.position += ship.velocity * deltaTime;
+ ship.position = WrapPosition(ship.position);
+
+ if (ship.invulnerabilityTimer > 0)
+ {
+ ship.invulnerabilityTimer -= deltaTime;
+ }
+
+ // Shooting
+ if (shootPressed && fireCooldownTimer <= 0 && bullets.Count < MAX_BULLETS)
+ {
+ Vector2 bulletVelocity = new Vector2(
+ Math.Cos(ship.rotation),
+ Math.Sin(ship.rotation)
+ ) * BULLET_SPEED;
+
+ bullets.Add(new Bullet(ship.position, bulletVelocity));
+ fireCooldownTimer = FIRE_COOLDOWN;
+ }
+ }
+
+ // Update fire cooldown
+ if (fireCooldownTimer > 0)
+ {
+ fireCooldownTimer -= deltaTime;
+ }
+
+ // Update bullets
+ for (int i = bullets.Count - 1; i >= 0; i--)
+ {
+ var bullet = bullets[i];
+ bullet.position += bullet.velocity * deltaTime;
+ bullet.position = WrapPosition(bullet.position);
+ bullet.lifetime -= deltaTime;
+
+ if (bullet.lifetime <= 0)
+ {
+ bullets.RemoveAt(i);
+ }
+ else
+ {
+ bullets[i] = bullet;
+ }
+ }
+
+ // Update asteroids
+ for (int i = 0; i < asteroids.Count; i++)
+ {
+ var asteroid = asteroids[i];
+ asteroid.position += asteroid.velocity * deltaTime;
+ asteroid.position = WrapPosition(asteroid.position);
+ asteroid.rotation += asteroid.rotationSpeed * deltaTime;
+ asteroids[i] = asteroid;
+ }
+
+ // Check bullet-asteroid collisions
+ for (int i = bullets.Count - 1; i >= 0; i--)
+ {
+ for (int j = asteroids.Count - 1; j >= 0; j--)
+ {
+ double distance = (bullets[i].position - asteroids[j].position).magnitude;
+ if (distance < Asteroid.GetRadius(asteroids[j].size))
+ {
+ SplitAsteroid(asteroids[j]);
+ asteroids.RemoveAt(j);
+ bullets.RemoveAt(i);
+ break;
+ }
+ }
+ }
+
+ // Check ship-asteroid collisions
+ if (shipActive && ship.invulnerabilityTimer <= 0)
+ {
+ for (int i = 0; i < asteroids.Count; i++)
+ {
+ double distance = (ship.position - asteroids[i].position).magnitude;
+ if (distance < SHIP_SIZE + Asteroid.GetRadius(asteroids[i].size))
+ {
+ DestroyShip();
+ break;
+ }
+ }
+ }
+
+ // Update particles
+ for (int i = particles.Count - 1; i >= 0; i--)
+ {
+ var particle = particles[i];
+ particle.position += particle.velocity * deltaTime;
+ particle.lifetime -= deltaTime;
+
+ if (particle.lifetime <= 0)
+ {
+ particles.RemoveAt(i);
+ }
+ else
+ {
+ particles[i] = particle;
+ }
+ }
+
+ // Check for level complete
+ if (asteroids.Count == 0)
+ {
+ level++;
+ SpawnAsteroids(STARTING_ASTEROIDS + level - 1);
+ }
+ }
+
+ // Draw background
+ canvas.RectFilled(rect.x, rect.y, rect.width, rect.height, System.Drawing.Color.Black);
+
+ // Draw particles
+ foreach (var particle in particles)
+ {
+ double alpha = particle.lifetime / particle.maxLifetime;
+ canvas.CircleFilled(
+ rect.x + particle.position.x,
+ rect.y + particle.position.y,
+ 2,
+ System.Drawing.Color.FromArgb((int)(255 * alpha), 255, 255, 255)
+ );
+ }
+
+ // Draw asteroids
+ foreach (var asteroid in asteroids)
+ {
+ canvas.BeginPath();
+ for (int i = 0; i < asteroid.shape.Length; i++)
+ {
+ double cos = Math.Cos(asteroid.rotation);
+ double sin = Math.Sin(asteroid.rotation);
+ double x = asteroid.shape[i].x * cos - asteroid.shape[i].y * sin;
+ double y = asteroid.shape[i].x * sin + asteroid.shape[i].y * cos;
+
+ if (i == 0)
+ canvas.MoveTo(rect.x + asteroid.position.x + x, rect.y + asteroid.position.y + y);
+ else
+ canvas.LineTo(rect.x + asteroid.position.x + x, rect.y + asteroid.position.y + y);
+ }
+ canvas.ClosePath();
+ canvas.SetStrokeColor(System.Drawing.Color.White);
+ canvas.SetStrokeWidth(2);
+ canvas.Stroke();
+ }
+
+ // Draw ship
+ if (shipActive && (ship.invulnerabilityTimer <= 0 || ((int)(ship.invulnerabilityTimer * 10) % 2 == 0)))
+ {
+ Vector2 front = new Vector2(
+ Math.Cos(ship.rotation) * SHIP_SIZE,
+ Math.Sin(ship.rotation) * SHIP_SIZE
+ );
+ Vector2 back1 = new Vector2(
+ Math.Cos(ship.rotation + 2.5) * SHIP_SIZE * 0.7,
+ Math.Sin(ship.rotation + 2.5) * SHIP_SIZE * 0.7
+ );
+ Vector2 back2 = new Vector2(
+ Math.Cos(ship.rotation - 2.5) * SHIP_SIZE * 0.7,
+ Math.Sin(ship.rotation - 2.5) * SHIP_SIZE * 0.7
+ );
+
+ canvas.BeginPath();
+ canvas.MoveTo(rect.x + ship.position.x + front.x, rect.y + ship.position.y + front.y);
+ canvas.LineTo(rect.x + ship.position.x + back1.x, rect.y + ship.position.y + back1.y);
+ canvas.LineTo(rect.x + ship.position.x + back2.x, rect.y + ship.position.y + back2.y);
+ canvas.ClosePath();
+ canvas.SetStrokeColor(System.Drawing.Color.White);
+ canvas.SetStrokeWidth(2);
+ canvas.Stroke();
+
+ // Draw thrust
+ if (ship.thrusting)
+ {
+ Vector2 thrustPoint = new Vector2(
+ Math.Cos(ship.rotation + Math.PI) * SHIP_SIZE * 0.8,
+ Math.Sin(ship.rotation + Math.PI) * SHIP_SIZE * 0.8
+ );
+ canvas.BeginPath();
+ canvas.MoveTo(rect.x + ship.position.x + back1.x, rect.y + ship.position.y + back1.y);
+ canvas.LineTo(rect.x + ship.position.x + thrustPoint.x, rect.y + ship.position.y + thrustPoint.y);
+ canvas.LineTo(rect.x + ship.position.x + back2.x, rect.y + ship.position.y + back2.y);
+ canvas.SetStrokeColor(System.Drawing.Color.Orange);
+ canvas.SetStrokeWidth(2);
+ canvas.Stroke();
+ }
+ }
+
+ // Draw bullets
+ foreach (var bullet in bullets)
+ {
+ canvas.CircleFilled(rect.x + bullet.position.x, rect.y + bullet.position.y, 2, System.Drawing.Color.White);
+ }
+
+ // Draw UI
+ canvas.DrawText($"Score: {score}", rect.x + 10, rect.y + 25, System.Drawing.Color.White, scoreLayout);
+ canvas.DrawText($"Lives: {lives}", rect.x + 10, rect.y + 50, System.Drawing.Color.White, scoreLayout);
+ canvas.DrawText($"Level: {level}", rect.x + 10, rect.y + 75, System.Drawing.Color.White, scoreLayout);
+
+ // Draw game over
+ if (gameOver)
+ {
+ canvas.DrawText("GAME OVER", rect.x + rect.width / 2, rect.y + rect.height / 2 - 20, System.Drawing.Color.White, gameOverLayout);
+ canvas.DrawText($"Final Score: {score}", rect.x + rect.width / 2, rect.y + rect.height / 2 + 20, System.Drawing.Color.White, scoreLayout);
+ canvas.DrawText("Press R to Restart", rect.x + rect.width / 2, rect.y + rect.height / 2 + 50, System.Drawing.Color.White, scoreLayout);
+ }
+
+ leftPressed = false;
+ rightPressed = false;
+ thrustPressed = false;
+ shootPressed = false;
+ });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/ChartTab.cs b/Samples/Shared/Tabs/ChartTab.cs
new file mode 100644
index 0000000..d34e6b1
--- /dev/null
+++ b/Samples/Shared/Tabs/ChartTab.cs
@@ -0,0 +1,185 @@
+using Prowl.PaperUI;
+
+using Prowl.Vector;
+
+namespace Shared.Tabs
+{
+ public class ChartTab : Tab
+ {
+ double time = 0;
+ static double[] dataPoints = { 0.2f, 0.5f, 0.3f, 0.8f, 0.4f, 0.7f, 0.6f };
+ static Vector2 chartPosition = new Vector2(0, 0);
+ static double zoomLevel = 1.0f;
+
+ public ChartTab(Paper gui) : base(gui)
+ {
+ title = "Chart";
+ id = "chart";
+ width = 64;
+ }
+
+ public override void Draw()
+ {
+ // Update time for animations
+ // time += 0.016f; // Assuming ~60fps
+ time += Gui.DeltaTime;
+
+ using (Gui.Row("Tools").RowBetween(5).Margin(4).Top(5).Height(20).Enter())
+ {
+ Gui.Box("Tool 1")
+ .Text(Icons.ArrowsTurnToDots, Fonts.arial).FontSize(12)
+ .Width(20).Rounded(5).Alignment(TextAlignment.MiddleCenter)
+ .Hovered.BackgroundColor(Themes.base250).End();
+ Gui.Box("Tool 2")
+ .Text(Icons.ArrowsDownToLine, Fonts.arial).FontSize(12)
+ .Width(20).Rounded(5).Alignment(TextAlignment.MiddleCenter)
+ .Hovered.BackgroundColor(Themes.base250).End();
+ Gui.Box("Tool 3")
+ .Text(Icons.Anchor, Fonts.arial).FontSize(12)
+ .Width(20).Rounded(5).Alignment(TextAlignment.MiddleCenter)
+ .Hovered.BackgroundColor(Themes.base250).End();
+ Gui.Box("Tool 4")
+ .Text(Icons.AngleUp, Fonts.arial).FontSize(12)
+ .Width(20).Rounded(5).Alignment(TextAlignment.MiddleCenter)
+ .Hovered.BackgroundColor(Themes.base250).End();
+
+ Gui.Box("Spacer");
+
+ Gui.Box("Tool 4")
+ .Text(Icons.ArrowsLeftRight, Fonts.arial).FontSize(12)
+ .Width(20).Rounded(5).Alignment(TextAlignment.MiddleCenter)
+ .Hovered.BackgroundColor(Themes.base250).End();
+ }
+
+ using (Gui.Box("Game View").Margin(2).Rounded(5).BackgroundColor(System.Drawing.Color.Black).Enter())
+ {
+
+ // Chart content
+ using (Gui.Box("Chart")
+ .Margin(20)
+ .OnDragging((e) => chartPosition += e.Delta)
+ .OnScroll((e) => zoomLevel = Math.Clamp(zoomLevel + e.Delta * 0.1f, 0.5f, 2.0f))
+ .Clip()
+ .Enter())
+ {
+ using (Gui.Box("ChartCanvas")
+ .Translate(chartPosition.x, chartPosition.y)
+ .Scale(zoomLevel)
+ .Enter())
+ {
+ // Draw a simple chart with animated data
+ Gui.AddActionElement((vg, rect) =>
+ {
+
+ // Draw grid lines
+ for (int i = 0; i <= 5; i++)
+ {
+ double y = rect.y + (rect.height / 5) * i;
+ vg.BeginPath();
+ vg.MoveTo(rect.x, y);
+ vg.LineTo(rect.x + rect.width, y);
+ vg.SetStrokeColor(Themes.baseContent);
+ vg.SetStrokeWidth(1);
+ vg.Stroke();
+ }
+
+ // Draw animated data points
+ vg.BeginPath();
+ double pointSpacing = rect.width / (dataPoints.Length - 1);
+ double animatedValue;
+
+ // Draw fill
+ vg.MoveTo(rect.x, rect.y + rect.height);
+
+ for (int i = 0; i < dataPoints.Length; i++)
+ {
+ animatedValue = dataPoints[i] + Math.Sin(time * 0.25f + i * 0.5f) * 0.1f;
+ //animatedValue = Math.Clamp(animatedValue, 0.1f, 0.9f);
+ animatedValue = Math.Min(Math.Max(animatedValue, 0.1f), 0.9f); // Clamp to [0.1, 0.9]
+
+ double x = rect.x + i * pointSpacing;
+ double y = rect.y + rect.height - (animatedValue * rect.height);
+
+ if (i == 0)
+ vg.MoveTo(x, y);
+ else
+ vg.LineTo(x, y);
+ }
+
+ // Complete the fill path
+ vg.LineTo(rect.x + rect.width, rect.y + rect.height);
+ vg.LineTo(rect.x, rect.y + rect.height);
+
+ // Fill with gradient
+ //var paint = vg.LinearGradient(
+ // rect.x, rect.y,
+ // rect.x, rect.y + rect.height,
+ // Color.FromArgb(100, primaryColor),
+ // Color.FromArgb(10, primaryColor));
+ //vg.SetFillPaint(paint);
+ vg.SaveState();
+ vg.SetLinearBrush(rect.x, rect.y, rect.x, rect.y + rect.height, System.Drawing.Color.FromArgb(100, Themes.primary), System.Drawing.Color.FromArgb(10, Themes.primary));
+ vg.FillComplex();
+ vg.RestoreState();
+
+ vg.ClosePath();
+
+ // Draw the line
+ vg.BeginPath();
+ for (int i = 0; i < dataPoints.Length; i++)
+ {
+ animatedValue = dataPoints[i] + Math.Sin(time * 0.25f + i * 0.5f) * 0.1f;
+ //animatedValue = Math.Clamp(animatedValue, 0.1f, 0.9f);
+ animatedValue = Math.Min(Math.Max(animatedValue, 0.1f), 0.9f); // Clamp to [0.1, 0.9]
+
+ double x = rect.x + i * pointSpacing;
+ double y = rect.y + rect.height - (animatedValue * rect.height);
+
+ if (i == 0)
+ vg.MoveTo(x, y);
+ else
+ vg.LineTo(x, y);
+ }
+
+ vg.SetStrokeColor(Themes.primary);
+ vg.SetStrokeWidth(3);
+ vg.Stroke();
+
+ // Draw points
+ for (int i = 0; i < dataPoints.Length; i++)
+ {
+ animatedValue = dataPoints[i] + Math.Sin(time * 0.25f + i * 0.5f) * 0.1f;
+ //animatedValue = Math.Clamp(animatedValue, 0.1f, 0.9f);
+ animatedValue = Math.Min(Math.Max(animatedValue, 0.1f), 0.9f); // Clamp to [0.1, 0.9]
+
+ double x = rect.x + i * pointSpacing;
+ double y = rect.y + rect.height - (animatedValue * rect.height);
+
+ vg.BeginPath();
+ vg.Circle(x, y, 6);
+ vg.SetFillColor(System.Drawing.Color.White);
+ vg.Fill();
+
+ vg.BeginPath();
+ vg.Circle(x, y, 4);
+ vg.SetFillColor(Themes.primary);
+ vg.Fill();
+ }
+
+ vg.ClosePath();
+ });
+ }
+ }
+
+
+ using (Gui.Row("Details").Height(28).Margin(8).Enter())
+ {
+ // FPS Counter
+ Gui.Box("FPS").Text($"FPS: {1f / Gui.DeltaTime:F1}", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19);
+ Gui.Box("NodeCounter").Text($"Nodes: {Gui.CountOfAllElements}", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19);
+ Gui.Box("MS").Text($"Frame ms: {Gui.MillisecondsSpent}", Fonts.arial).TextColor(Themes.baseContent).Alignment(TextAlignment.MiddleLeft).FontSize(19);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/ExampleTab.cs b/Samples/Shared/Tabs/ExampleTab.cs
new file mode 100644
index 0000000..e1c1536
--- /dev/null
+++ b/Samples/Shared/Tabs/ExampleTab.cs
@@ -0,0 +1,14 @@
+using Prowl.PaperUI;
+
+namespace Shared.Tabs
+{
+ public class ExampleTab : Tab
+ {
+ public ExampleTab(Paper gui) : base(gui)
+ {
+ title = "Example";
+ id = "example";
+ width = 70;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/FilesTab.cs b/Samples/Shared/Tabs/FilesTab.cs
new file mode 100644
index 0000000..23d7db5
--- /dev/null
+++ b/Samples/Shared/Tabs/FilesTab.cs
@@ -0,0 +1,98 @@
+using Prowl.PaperUI;
+
+namespace Shared.Tabs
+{
+ public class FilesTab : Tab
+ {
+ public FilesTab(Paper gui) : base(gui)
+ {
+ title = "Files";
+ id = "files";
+ width = 52;
+ }
+
+ private string selectedFolderId;
+
+ public override void Draw()
+ {
+ using (Gui.Box("Search Box").Height(28).Margin(5).Rounded(5).BackgroundColor(Themes.base300).Enter())
+ {
+ Gui.Box("Search").Text("Filter...", Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Left(8)
+ .Alignment(TextAlignment.MiddleLeft);
+ }
+
+ using (Gui.Column("Folders view").SetScroll(Scroll.ScrollY).Enter())
+ {
+ DrawFolderItem("Folders", true, new[] {
+ ("Models", new[] {
+ "Character.fbx",
+ "Tree.fbx",
+ "House.fbx"
+ }),
+ ("Textures", new[] {
+ "Grass.png",
+ "Sky.png"
+ }),
+ ("Scripts", new[] {
+ "Player.cs",
+ "Enemy.cs"
+ })
+ });
+ }
+ }
+
+ private void DrawFolderItem(string name, bool expanded = false, (string name, string[] children)[] subFolders = null)
+ {
+ var isSelected = selectedFolderId == name;
+
+ using (Gui.Row(name).Height(28).Margin(5).Top(0).Bottom(0).Rounded(5)
+ .BackgroundColor(isSelected ? Themes.base250 : Themes.base200)
+ .Hovered
+ .BackgroundColor(Themes.base250)
+ .End()
+ .OnClick((_) => selectedFolderId = name)
+ .Enter())
+ {
+ if (subFolders != null)
+ {
+ Gui.Box($"toggle{name}").Text(expanded ? Icons.ChevronDown : Icons.ChevronRight, Fonts.arial)
+ .Width(28)
+ .Alignment(TextAlignment.MiddleCenter)
+ .FontSize(8);
+ }
+
+ Gui.Box($"icon{name}").Text(Icons.Folder, Fonts.arial)
+ .Width(28)
+ .Alignment(TextAlignment.MiddleCenter)
+ .FontSize(10);
+
+ Gui.Box($"name{name}").Text(name, Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Alignment(TextAlignment.MiddleLeft);
+ }
+
+ if (expanded && subFolders != null)
+ {
+ using (Gui.Box($"Children{name}").Left(20).Enter())
+ {
+ foreach (var (folderName, children) in subFolders)
+ {
+ DrawFolderItem(folderName, false, null);
+ if (children != null)
+ {
+ using (Gui.Box($"Files{folderName}").Left(20).Enter())
+ {
+ foreach (var file in children)
+ {
+ DrawFolderItem(file);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/HierarchyTab.cs b/Samples/Shared/Tabs/HierarchyTab.cs
new file mode 100644
index 0000000..6f2affe
--- /dev/null
+++ b/Samples/Shared/Tabs/HierarchyTab.cs
@@ -0,0 +1,221 @@
+using Prowl.PaperUI;
+using Prowl.PaperUI.LayoutEngine;
+
+namespace Shared.Tabs
+{
+ public class HierarchyTab : Tab
+ {
+ public HierarchyTab(Paper gui) : base(gui)
+ {
+ title = "Hierarchy";
+ id = "hierarchy";
+ width = 83;
+ }
+
+ public override void Draw()
+ {
+ using (Gui.Box("Search Box")
+ .OnDragEnd((e) =>
+ {
+ Console.WriteLine("drag end", e);
+ })
+ .OnDragStart((e) =>
+ {
+ // e.DataTransfer.setData("tabId", id);
+ // e.DataTransfer.setImage(imageOfThingBeingDragged); // shows an image that moves with your cursor as long as you are dragging
+ Console.WriteLine("drag start", e);
+ })
+ .OnDragging((e) =>
+ {
+ Console.WriteLine("dragging", e);
+ })
+ // .OnDrop((e) =>
+ // {
+ // var data = e.DataTransfer.getData("tabId");
+ // })
+ .Height(28).Margin(5).Top(8).Bottom(8).Rounded(5)
+ .BackgroundColor(Themes.base300).Enter())
+ {
+ Gui.Box("Search").Text("Filter...", Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Left(8)
+ .Alignment(TextAlignment.MiddleLeft);
+ }
+
+ using (Gui.Box("Hierarchy container").SetScroll(Scroll.ScrollY).Enter())
+ {
+ HierarchyItem(rootItem, 0);
+ Gui.Box("Spacer"); // this will take up any remaining space (do not remove)
+ }
+ }
+
+ public class Item
+ {
+ public string id;
+ public string title;
+ public Item[] children;
+ public bool expanded;
+ public bool selected;
+ public Action- onClick; // Open inspector etc
+ }
+
+ public static string selectedItemId = "";
+
+ private void HierarchyItem(Item item, int depth)
+ {
+ var isSelected = selectedItemId == item.id;
+
+ using (Gui.Column("Hierarchy item " + item.id, depth).MinHeight(item.expanded ? UnitValue.Auto : 28).Left(depth * 8).Enter())
+ {
+ // ensure proper padding with parent
+ using (Gui.Row(item.id).Height(28).Margin(5).Top(0).Bottom(0).Rounded(5)
+ .BackgroundColor(isSelected ? Themes.base250 : Themes.base200)
+ .Hovered
+ .BackgroundColor(Themes.base250)
+ .End()
+ .OnClick((_) =>
+ {
+ selectedItemId = item.id;
+ item.onClick?.Invoke(item);
+ })
+ .OnDragging((e) =>
+ {
+ var source = e.Source;
+ Console.WriteLine("dragged over", item.title);
+ })
+ .Enter())
+ {
+ if (isSelected)
+ {
+ Gui.Box("indicator")
+ .PositionType(PositionType.SelfDirected)
+ .Height(28).Width(3).Rounded(1)
+ .BackgroundColor(Themes.primary);
+ }
+
+ if (item.children != null && item.children.Length > 0)
+ {
+ Gui.Box("toggle" + item.id).Text(item.expanded ? Icons.ChevronDown : Icons.ChevronRight, Fonts.arial)
+ .Width(28)
+ .Alignment(TextAlignment.MiddleCenter)
+ .FontSize(8)
+ .OnClick((_) => item.expanded = !item.expanded);
+ }
+ else
+ {
+ Gui.Box("icon" + item.id).Text(Icons.Cube, Fonts.arial)
+ .Width(28)
+ .Alignment(TextAlignment.MiddleCenter)
+ .FontSize(10)
+ .OnClick((_) => item.expanded = !item.expanded);
+ }
+
+ Gui.Box("box" + item.id).Text(item.title, Fonts.arial)
+ .TextColor(Themes.baseContent)
+ .Alignment(TextAlignment.MiddleLeft);
+
+ }
+
+ if (item.expanded && item.children != null)
+ {
+ foreach (var child in item.children)
+ {
+ HierarchyItem(child, depth + 1);
+ }
+ }
+ }
+ }
+
+ public static Item rootItem = new Item
+ {
+ id = "root object",
+ title = "Game World",
+ expanded = true,
+ selected = false,
+ onClick = (item) => Console.WriteLine($"Clicked {item.title}"),
+ children = new[] {
+ new Item {
+ id = "2",
+ title = "Player",
+ expanded = true,
+ children = new[] {
+ new Item { id = "2.1", title = "Inventory"},
+ new Item { id = "2.2", title = "Equipment"},
+ new Item { id = "2.3", title = "Stats" },
+ new Item { id = "2.4", title = "Skills" }
+ }
+ },
+ new Item {
+ id = "3",
+ title = "Environment",
+ expanded = false,
+ children = new[] {
+ new Item {
+ id = "3.1",
+ title = "Terrain",
+ children = new[] {
+ new Item { id = "3.1.1", title = "Mountains" },
+ new Item { id = "3.1.2", title = "Forest" },
+ new Item { id = "3.1.3", title = "Desert" }
+ }
+ },
+ new Item {
+ id = "3.2",
+ title = "Weather",
+ children = new[] {
+ new Item { id = "3.2.1", title = "Rain System" },
+ new Item { id = "3.2.2", title = "Wind System" }
+ }
+ }
+ }
+ },
+ new Item {
+ id = "4",
+ title = "NPCs",
+ expanded = false,
+ children = new[] {
+ new Item {
+ id = "4.1",
+ title = "Friendly",
+ children = new[] {
+ new Item { id = "4.1.1", title = "Merchants" },
+ new Item { id = "4.1.2", title = "Quest Givers" },
+ new Item { id = "4.1.3", title = "Villagers" }
+ }
+ },
+ new Item {
+ id = "4.2",
+ title = "Hostile",
+ children = new[] {
+ new Item { id = "4.2.1", title = "Goblins" },
+ new Item { id = "4.2.2", title = "Dragons" },
+ new Item { id = "4.2.3", title = "Bandits" }
+ }
+ }
+ }
+ },
+ new Item {
+ id = "5",
+ title = "UI Elements",
+ expanded = false,
+ children = new[] {
+ new Item { id = "5.1", title = "Main Menu" },
+ new Item { id = "5.2", title = "HUD" },
+ new Item { id = "5.3", title = "Inventory UI" },
+ new Item { id = "5.4", title = "Quest Log" }
+ }
+ },
+ new Item {
+ id = "6",
+ title = "Audio",
+ expanded = false,
+ children = new[] {
+ new Item { id = "6.1", title = "Music" },
+ new Item { id = "6.2", title = "Sound Effects" },
+ new Item { id = "6.3", title = "Ambient" }
+ }
+ }
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/InspectorTab.cs b/Samples/Shared/Tabs/InspectorTab.cs
new file mode 100644
index 0000000..a2d8c55
--- /dev/null
+++ b/Samples/Shared/Tabs/InspectorTab.cs
@@ -0,0 +1,14 @@
+using Prowl.PaperUI;
+
+namespace Shared.Tabs
+{
+ public class InspectorTab : Tab
+ {
+ public InspectorTab(Paper gui) : base(gui)
+ {
+ title = "Inspector";
+ id = "inspector";
+ width = 70;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/PlatformerTab.cs b/Samples/Shared/Tabs/PlatformerTab.cs
new file mode 100644
index 0000000..987bda2
--- /dev/null
+++ b/Samples/Shared/Tabs/PlatformerTab.cs
@@ -0,0 +1,180 @@
+using Prowl.PaperUI;
+using Prowl.Vector;
+
+namespace Shared.Tabs
+{
+ public class PlatformerTab : Tab
+ {
+ const int PLAYER_SIZE = 20;
+ const double PLAYER_ACCELERATION = 1200.0;
+ const double PLAYER_MAX_SPEED = 200.0;
+ const double PLAYER_FRICTION = 0.93;
+ const double JUMP_FORCE = -400.0;
+ const double GRAVITY = 800.0;
+
+ double playerX = 100;
+ double playerY = 0;
+ double velocityX = 0;
+ double velocityY = 0;
+ bool isGrounded = false;
+ bool leftPressed = false;
+ bool rightPressed = false;
+
+ Rect _rect;
+
+ public PlatformerTab(Paper gui) : base(gui)
+ {
+ title = "Platformer";
+ id = "platformer";
+ width = 90;
+ }
+
+ public override void Focus()
+ {
+ Console.WriteLine("Platformer is focused");
+ }
+
+ public override void Blur()
+ {
+ Console.WriteLine("Platformer is blurred");
+ }
+
+ struct Platform
+ {
+ public double x;
+ public double y;
+ public double width;
+ public double height;
+
+ public Platform(double x, double y, double width, double height)
+ {
+ this.x = x;
+ this.y = y;
+ this.width = width;
+ this.height = height;
+ }
+ }
+
+ Platform[] platforms = new Platform[] {
+ new Platform(100, 300, 100, 20),
+ new Platform(250, 250, 100, 20),
+ new Platform(400, 200, 100, 20),
+ new Platform(200, 150, 100, 20),
+ };
+
+ private bool CheckCollision(double x, double y, Platform platform)
+ {
+ return x < platform.x + platform.width &&
+ x + PLAYER_SIZE > platform.x &&
+ y < platform.y + platform.height &&
+ y + PLAYER_SIZE > platform.y;
+ }
+
+ public override void Draw()
+ {
+ using (Gui.Box("Container")
+ .Margin(5)
+ .Focused
+ .BorderWidth(2)
+ .BorderColor(Themes.primary)
+ .End()
+ .OnKeyPressed((e) =>
+ {
+ if (e.Key == PaperKey.A) leftPressed = true;
+ if (e.Key == PaperKey.D) rightPressed = true;
+ if (e.Key == PaperKey.Space && isGrounded) velocityY = JUMP_FORCE;
+ })
+ .Enter())
+ {
+ Gui.AddActionElement((canvas, rect) =>
+ {
+ _rect = rect;
+ var deltaTime = Gui.DeltaTime;
+
+ // Horizontal movement with acceleration
+ if (leftPressed) velocityX -= PLAYER_ACCELERATION * deltaTime;
+ if (rightPressed) velocityX += PLAYER_ACCELERATION * deltaTime;
+
+ // Apply friction when no keys are pressed
+ velocityX *= PLAYER_FRICTION;
+
+ // Clamp horizontal speed
+ velocityX = Math.Clamp(velocityX, -PLAYER_MAX_SPEED, PLAYER_MAX_SPEED);
+
+ // Apply gravity
+ velocityY += GRAVITY * deltaTime;
+
+ // Update position
+ double newPlayerX = playerX + velocityX * deltaTime;
+ double newPlayerY = playerY + velocityY * deltaTime;
+
+ // Platform collision detection
+ isGrounded = false;
+ foreach (var platform in platforms)
+ {
+ if (CheckCollision(newPlayerX, newPlayerY, platform))
+ {
+ // Vertical collision
+ if (velocityY > 0 && playerY + PLAYER_SIZE <= platform.y + 5)
+ {
+ newPlayerY = platform.y - PLAYER_SIZE;
+ velocityY = 0;
+ isGrounded = true;
+ }
+ // Head collision
+ else if (velocityY < 0 && playerY >= platform.y + platform.height - 5)
+ {
+ newPlayerY = platform.y + platform.height;
+ velocityY = 0;
+ }
+ // Horizontal collision
+ else if (velocityX != 0)
+ {
+ newPlayerX = velocityX > 0 ?
+ platform.x - PLAYER_SIZE :
+ platform.x + platform.width;
+ velocityX = 0;
+ }
+ }
+ }
+
+ // Ground collision
+ if (newPlayerY + PLAYER_SIZE >= rect.height)
+ {
+ newPlayerY = rect.height - PLAYER_SIZE;
+ velocityY = 0;
+ isGrounded = true;
+ }
+
+ // Wall collision
+ newPlayerX = Math.Clamp(newPlayerX, 0, rect.width - PLAYER_SIZE);
+
+ // Update position
+ playerX = newPlayerX;
+ playerY = newPlayerY;
+
+ // Draw platforms
+ foreach (var platform in platforms)
+ {
+ canvas.RectFilled(
+ rect.x + platform.x,
+ rect.y + platform.y,
+ platform.width,
+ platform.height,
+ System.Drawing.Color.Gray
+ );
+ }
+
+ // Draw player
+ canvas.RectFilled(rect.x + playerX, rect.y + playerY, PLAYER_SIZE, PLAYER_SIZE, System.Drawing.Color.White);
+
+ // Draw ground
+ canvas.RectFilled(rect.x, rect.y + rect.height - 2, rect.width, 2, System.Drawing.Color.White);
+
+ leftPressed = false;
+ rightPressed = false;
+ });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/PongTab.cs b/Samples/Shared/Tabs/PongTab.cs
new file mode 100644
index 0000000..dcf6ee5
--- /dev/null
+++ b/Samples/Shared/Tabs/PongTab.cs
@@ -0,0 +1,212 @@
+using Prowl.PaperUI;
+using Prowl.Quill;
+using Prowl.Scribe;
+
+namespace Shared.Tabs
+{
+ public class PongTab : Tab
+ {
+ const int BALL_SIZE = 10;
+ const int PADDLE_WIDTH = 10;
+ const int PADDLE_HEIGHT = 80;
+ const double BALL_MAX_VELOCITY = 100.0;
+ const double PLAYERS_MAX_VELOCITY = 400.0;
+ const double PLAYER_START_VELOCITY = 150.0; // Base velocity for paddle movement
+
+ TextLayoutSettings _scoreLayoutSettings = TextLayoutSettings.Default;
+
+ int player1Score = 0;
+ double player1Y = -2;
+ bool player1UpPressed = false;
+ bool player1DownPressed = false;
+ double player1Velocity = 0.0;
+
+ int player2Score = 0;
+ double player2Y = -2;
+ bool player2UpPressed = false;
+ bool player2DownPressed = false;
+ double player2Velocity = 0.0;
+
+ bool ballPositionCentered = false;
+ bool gameStarted = false;
+ double ballX = 0;
+ double ballY = 0;
+ double ballVelocityX = 0;
+ double ballVelocityY = 0;
+
+ Prowl.Vector.Rect _rect;
+
+ public PongTab(Paper gui) : base(gui)
+ {
+ title = "Pong";
+ id = "pong";
+ width = 64;
+
+ // Larger font for score
+ _scoreLayoutSettings.Font = Fonts.arial;
+ _scoreLayoutSettings.PixelSize = 24;
+ _scoreLayoutSettings.Alignment = Prowl.Scribe.TextAlignment.Center;
+ }
+
+ public override void Focus()
+ {
+ Console.WriteLine("Pong is focused");
+ }
+
+ private void ResetBall()
+ {
+ ballX = _rect.width / 2;
+ ballY = _rect.height / 2;
+ ballVelocityX = 150 * (Random.Shared.Next(2) * 2 - 1); // Random direction
+ ballVelocityY = 150 * (Random.Shared.Next(2) * 2 - 1);
+ }
+
+ public override void Blur()
+ {
+ Console.WriteLine("Pong is blurred");
+ }
+
+ double ogBallVelX = 0;
+ double ogBallVelY = 0;
+
+ public override void Draw()
+ {
+ using (Gui.Box("Container")
+ .Margin(5)
+ .Focused
+ .BorderWidth(2)
+ .BorderColor(Themes.primary)
+ .End()
+ .OnFocusChange((e) =>
+ {
+ if (e.IsFocused)
+ {
+ gameStarted = true;
+ ballVelocityX = ogBallVelX; // Random direction
+ ballVelocityY = ogBallVelY;
+ }
+ else
+ {
+ ogBallVelX = ballVelocityX;
+ ogBallVelY = ballVelocityY;
+ ballVelocityX = 0;
+ ballVelocityY = 0;
+ }
+ })
+ .OnKeyPressed((e) =>
+ {
+ if (e.Key == PaperKey.W) player1UpPressed = true;
+ else player1UpPressed = false;
+ if (e.Key == PaperKey.S) player1DownPressed = true;
+ else player1DownPressed = false;
+
+ if (e.Key == PaperKey.Up) player2UpPressed = true;
+ else player2UpPressed = false;
+ if (e.Key == PaperKey.Down) player2DownPressed = true;
+ else player2DownPressed = false;
+ })
+ .Enter())
+ {
+ Gui.AddActionElement((canvas, rect) =>
+ {
+ _rect = rect;
+
+ if (!ballPositionCentered && !gameStarted)
+ {
+ ballX = _rect.width / 2;
+ ballY = _rect.height / 2;
+ }
+
+ if (gameStarted && !ballPositionCentered)
+ {
+ ResetBall();
+ ballPositionCentered = true;
+ }
+
+ // Initialize players in the middle if they haven't been positioned yet
+ if (player1Y < -1) player1Y = (rect.height - PADDLE_HEIGHT) / 2;
+ if (player2Y < -1) player2Y = (rect.height - PADDLE_HEIGHT) / 2;
+
+ var deltaTime = Gui.DeltaTime;
+
+ // Then modify the velocity calculations in the Draw() method:
+ if (player1UpPressed) player1Velocity = -PLAYER_START_VELOCITY * deltaTime; // Use constant for consistent initial velocity
+ if (player1DownPressed) player1Velocity = PLAYER_START_VELOCITY * deltaTime; // Use constant for consistent initial velocity
+
+ player1Y += player1Velocity;
+ player1Velocity = Math.Clamp(player1Velocity * 0.98, -PLAYERS_MAX_VELOCITY, PLAYERS_MAX_VELOCITY);
+
+ if (player2UpPressed) player2Velocity = -PLAYER_START_VELOCITY * deltaTime; // Use constant for consistent initial velocity
+ if (player2DownPressed) player2Velocity = PLAYER_START_VELOCITY * deltaTime; // Use constant for consistent initial velocity
+
+ player2Y += player2Velocity;
+ player2Velocity = Math.Clamp(player2Velocity * 0.98, -PLAYERS_MAX_VELOCITY, PLAYERS_MAX_VELOCITY);
+
+ player1Y = Math.Clamp(player1Y, 0, _rect.height - PADDLE_HEIGHT);
+ player2Y = Math.Clamp(player2Y, 0, _rect.height - PADDLE_HEIGHT);
+
+ // Update ball position
+ ballX += ballVelocityX * deltaTime;
+ ballY += ballVelocityY * deltaTime;
+
+ // Clamp ball velocity
+ ballVelocityX = Math.Clamp(ballVelocityX, -BALL_MAX_VELOCITY, BALL_MAX_VELOCITY);
+ ballVelocityY = Math.Clamp(ballVelocityY, -BALL_MAX_VELOCITY, BALL_MAX_VELOCITY);
+
+ // Ball collision with top and bottom
+ if (ballY <= 0 || ballY >= rect.height - BALL_SIZE)
+ {
+ ballVelocityY = -ballVelocityY;
+ }
+
+ // Ball collision with paddles
+ if (ballX <= PADDLE_WIDTH && ballY >= player1Y && ballY <= player1Y + PADDLE_HEIGHT)
+ {
+ ballVelocityX = Math.Abs(ballVelocityX); // Bounce right
+ }
+ if (ballX >= rect.width - PADDLE_WIDTH - BALL_SIZE && ballY >= player2Y && ballY <= player2Y + PADDLE_HEIGHT)
+ {
+ ballVelocityX = -Math.Abs(ballVelocityX); // Bounce left
+ }
+
+ // Reset ball and update score if it goes past paddles
+ if (ballX < 0)
+ {
+ player2Score++;
+ ResetBall();
+ }
+ else if (ballX > rect.width)
+ {
+ player1Score++;
+ ResetBall();
+ }
+
+ player1UpPressed = false;
+ player1DownPressed = false;
+ player2UpPressed = false;
+ player2DownPressed = false;
+
+ // NOTE (do not remove):
+ // - canvas coordinates are unrestricted and can draw to anywhere on the entire window
+ // - rect is the local coordinates of the parent element, use it to keep the drawing of shapes within the parent UI element
+
+ // Draw center line
+ // canvas.DashedLine(rect.x + rect.width / 2, rect.y, rect.x + rect.width / 2, rect.y + rect.height, System.Drawing.Color.White, 2, 5);
+
+ // paddle 1
+ canvas.RectFilled(rect.x, rect.y + player1Y, PADDLE_WIDTH, PADDLE_HEIGHT, System.Drawing.Color.White);
+
+ // ball (now using ball position)
+ canvas.CircleFilled(rect.x + ballX, rect.y + ballY, BALL_SIZE, System.Drawing.Color.White);
+
+ // paddle 2
+ canvas.RectFilled(rect.x - PADDLE_WIDTH + rect.width, rect.y + player2Y, PADDLE_WIDTH, PADDLE_HEIGHT, System.Drawing.Color.White);
+
+ // Draw score at the top
+ canvas.DrawText(player1Score.ToString(), rect.x + (rect.width / 4), rect.y + 30, Themes.baseContent, _scoreLayoutSettings);
+ canvas.DrawText(player2Score.ToString(), rect.x + (rect.width * 3 / 4), rect.y + 30, Themes.baseContent, _scoreLayoutSettings);
+ });
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/SettingsTab.cs b/Samples/Shared/Tabs/SettingsTab.cs
new file mode 100644
index 0000000..a947254
--- /dev/null
+++ b/Samples/Shared/Tabs/SettingsTab.cs
@@ -0,0 +1,14 @@
+using Prowl.PaperUI;
+
+namespace Shared.Tabs
+{
+ public class SettingsTab : Tab
+ {
+ public SettingsTab(Paper gui) : base(gui)
+ {
+ title = "Settings";
+ id = "settings";
+ width = 70;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Samples/Shared/Tabs/SlimeFriendsTab.cs b/Samples/Shared/Tabs/SlimeFriendsTab.cs
new file mode 100644
index 0000000..a254c6f
--- /dev/null
+++ b/Samples/Shared/Tabs/SlimeFriendsTab.cs
@@ -0,0 +1,338 @@
+using Prowl.PaperUI;
+using Prowl.Vector;
+using Prowl.Scribe;
+using Prowl.Quill;
+
+namespace Shared.Tabs
+{
+ public class SlimeFriendsTab : Tab
+ {
+ const int SLIME_SIZE = 30;
+ const double BASE_SLIME_SPEED = 80.0;
+ const double HOP_DURATION = 1.0;
+ const double AVOIDANCE_RADIUS = 60.0;
+ const double AVOIDANCE_STRENGTH = 200.0;
+ const double HEART_DISPLAY_DURATION = 2.0;
+ const double SPAWN_BUTTON_WIDTH = 120;
+ const double SPAWN_BUTTON_HEIGHT = 40;
+ const double SPAWN_BUTTON_X = 10;
+ const double SPAWN_BUTTON_Y = 10;
+
+ struct Slime
+ {
+ public Vector2 position;
+ public System.Drawing.Color color;
+ public string name;
+ public double hopProgress;
+ public double speed;
+ public double heartTimer;
+
+ public Slime(Vector2 position, System.Drawing.Color color, string name, double speed)
+ {
+ this.position = position;
+ this.color = color;
+ this.name = name;
+ this.hopProgress = 0;
+ this.speed = speed;
+ this.heartTimer = 0;
+ }
+ }
+
+ struct GrassBlade
+ {
+ public double x;
+ public double y;
+ public double height;
+ }
+
+ struct Flower
+ {
+ public double x;
+ public double y;
+ public System.Drawing.Color color;
+ }
+
+ List slimes = new List();
+ List grassBlades = new List();
+ List flowers = new List();
+ Vector2 pointerPosition = Vector2.zero;
+
+ TextLayoutSettings nameLayout = new TextLayoutSettings
+ {
+ Font = Fonts.arial,
+ PixelSize = 12,
+ Alignment = Prowl.Scribe.TextAlignment.Center
+ };
+
+ TextLayoutSettings buttonLayout = new TextLayoutSettings
+ {
+ Font = Fonts.arial,
+ PixelSize = 14,
+ Alignment = Prowl.Scribe.TextAlignment.Center
+ };
+
+ string[] slimeNames = { "Blob", "Gooey", "Squishy", "Bounce", "Jelly", "Puddle", "Glop", "Wobble", "Splat", "Mochi" };
+ int nameIndex = 0;
+
+ public SlimeFriendsTab(Paper gui) : base(gui)
+ {
+ title = "Slime Friends";
+ id = "slimefriends";
+ width = 110;
+
+ // Initialize background elements
+ System.Random rand = new System.Random(42);
+ System.Drawing.Color[] flowerColors = {
+ System.Drawing.Color.FromArgb(255, 182, 193),
+ System.Drawing.Color.FromArgb(255, 255, 102),
+ System.Drawing.Color.FromArgb(186, 143, 255)
+ };
+
+ // Generate grass blades (relative positions 0-1)
+ for (int i = 0; i < 30; i++)
+ {
+ grassBlades.Add(new GrassBlade
+ {
+ x = rand.NextDouble(),
+ y = 0.6 + rand.NextDouble() * 0.4,
+ height = 10 + rand.NextDouble() * 15
+ });
+ }
+
+ // Generate flowers (relative positions 0-1)
+ for (int i = 0; i < 10; i++)
+ {
+ flowers.Add(new Flower
+ {
+ x = rand.NextDouble(),
+ y = 0.65 + rand.NextDouble() * 0.3,
+ color = flowerColors[rand.Next(flowerColors.Length)]
+ });
+ }
+
+ // Start with only 1 slime
+ rand = new System.Random();
+ slimes.Add(new Slime(
+ new Vector2(250, 200),
+ System.Drawing.Color.FromArgb(
+ rand.Next(100, 255),
+ rand.Next(100, 255),
+ rand.Next(100, 255)
+ ),
+ slimeNames[nameIndex++],
+ BASE_SLIME_SPEED * (0.7 + rand.NextDouble() * 0.6)
+ ));
+ }
+
+ private double CircleSDF(Vector2 point, Vector2 center, double radius)
+ {
+ return (point - center).magnitude - radius;
+ }
+
+ private void DrawPrairieBackground(Canvas canvas, Rect rect)
+ {
+ // Sky
+ canvas.RectFilled(rect.x, rect.y, rect.width, rect.height * 0.2, System.Drawing.Color.FromArgb(135, 206, 235));
+
+ // Ground
+ canvas.RectFilled(rect.x, rect.y + rect.height * 0.2, rect.width, rect.height * 0.8, System.Drawing.Color.FromArgb(124, 185, 82));
+
+ // Draw grass blades
+ foreach (var grass in grassBlades)
+ {
+ double x = rect.x + grass.x * rect.width;
+ double y = rect.y + grass.y * rect.height;
+
+ canvas.BeginPath();
+ canvas.LineTo(x, y);
+ canvas.LineTo(x, y - grass.height);
+ canvas.SetStrokeColor(System.Drawing.Color.FromArgb(85, 155, 60));
+ canvas.SetStrokeWidth(2);
+ canvas.Stroke();
+ }
+
+ // Draw flowers
+ foreach (var flower in flowers)
+ {
+ double x = rect.x + flower.x * rect.width;
+ double y = rect.y + flower.y * rect.height;
+
+ // Stem
+ canvas.BeginPath();
+ canvas.LineTo(x, y);
+ canvas.LineTo(x, y - 15);
+ canvas.SetStrokeColor(System.Drawing.Color.FromArgb(85, 155, 60));
+ canvas.SetStrokeWidth(2);
+ canvas.Stroke();
+
+ // Petals
+ canvas.CircleFilled(x, y - 15, 4, flower.color);
+ }
+ }
+
+ private void DrawHeart(Canvas canvas, double x, double y, double size)
+ {
+ System.Drawing.Color heartColor = System.Drawing.Color.FromArgb(255, 105, 180);
+ double offset = size * 0.3;
+
+ canvas.CircleFilled(x - offset, y, size * 0.5, heartColor);
+ canvas.CircleFilled(x + offset, y, size * 0.5, heartColor);
+ canvas.CircleFilled(x, y, size * 2, heartColor);
+ }
+
+ private void SpawnNewSlime()
+ {
+ System.Random rand = new System.Random();
+ slimes.Add(new Slime(
+ new Vector2(rand.Next(50, 500), rand.Next(50, 300)),
+ System.Drawing.Color.FromArgb(
+ rand.Next(100, 255),
+ rand.Next(100, 255),
+ rand.Next(100, 255)
+ ),
+ slimeNames[nameIndex % slimeNames.Length],
+ BASE_SLIME_SPEED * (0.7 + rand.NextDouble() * 0.6)
+ ));
+ nameIndex++;
+ }
+
+ public override void Draw()
+ {
+ using (Gui.Box("Container")
+ .Margin(5)
+ .Focused
+ .BorderWidth(2)
+ .BorderColor(Themes.primary)
+ .End()
+ .OnHover((e) =>
+ {
+ pointerPosition = e.PointerPosition;
+ })
+ .Enter())
+ {
+ Gui.AddActionElement((canvas, rect) =>
+ {
+ DrawPrairieBackground(canvas, rect);
+
+ var deltaTime = Gui.DeltaTime;
+ Vector2 localPointerPosition = new Vector2(
+ pointerPosition.x - rect.x,
+ pointerPosition.y - rect.y
+ );
+
+ // Handle slime clicks
+ if (Gui.IsPointerPressed(PaperMouseBtn.Left))
+ {
+ for (int i = 0; i < slimes.Count; i++)
+ {
+ var slime = slimes[i];
+ double distance = (localPointerPosition - slime.position).magnitude;
+
+ if (distance <= SLIME_SIZE)
+ {
+ slime.heartTimer = HEART_DISPLAY_DURATION;
+ slimes[i] = slime;
+ }
+ }
+ }
+
+ // Update and draw slimes
+ for (int i = 0; i < slimes.Count; i++)
+ {
+ var slime = slimes[i];
+
+ if (slime.heartTimer > 0)
+ {
+ slime.heartTimer -= deltaTime;
+ }
+
+ Vector2 direction = localPointerPosition - slime.position;
+ if (direction.magnitude > 1)
+ {
+ direction = direction.normalized;
+ Vector2 moveVector = direction * slime.speed;
+
+ // SDF-based collision avoidance
+ for (int j = 0; j < slimes.Count; j++)
+ {
+ if (i != j)
+ {
+ double sdfDistance = CircleSDF(slime.position, slimes[j].position, SLIME_SIZE);
+
+ if (sdfDistance < AVOIDANCE_RADIUS)
+ {
+ Vector2 avoidanceDirection = (slime.position - slimes[j].position).normalized;
+ double avoidanceFactor = Math.Max(0, 1.0 - (sdfDistance / AVOIDANCE_RADIUS));
+ moveVector += avoidanceDirection * AVOIDANCE_STRENGTH * avoidanceFactor;
+ }
+ }
+ }
+
+ slime.position += moveVector * deltaTime;
+ }
+
+ // Hop animation
+ slime.hopProgress = (slime.hopProgress + deltaTime) % HOP_DURATION;
+ float scale = 1.0f + (float)Math.Sin(slime.hopProgress * (Math.PI * 2 / HOP_DURATION)) * 0.2f;
+
+ // Draw slime
+ int size = (int)(SLIME_SIZE * scale);
+ canvas.CircleFilled(
+ rect.x + slime.position.x,
+ rect.y + slime.position.y,
+ size,
+ slime.color
+ );
+
+ // Draw heart if active
+ if (slime.heartTimer > 0)
+ {
+ DrawHeart(
+ canvas,
+ rect.x + slime.position.x,
+ rect.y + slime.position.y - size - 20,
+ 10
+ );
+ }
+
+ // Draw name
+ canvas.DrawText(
+ slime.name,
+ rect.x + slime.position.x,
+ rect.y + slime.position.y - size - 10,
+ System.Drawing.Color.White,
+ nameLayout
+ );
+
+ slimes[i] = slime;
+ }
+
+ // Draw spawn button
+ double buttonX = rect.x + SPAWN_BUTTON_X;
+ double buttonY = rect.y + SPAWN_BUTTON_Y;
+ });
+ }
+
+ // Spawn button click handler
+ using (Gui.Box("SpawnButton")
+ .PositionType(PositionType.SelfDirected)
+ .Left(SPAWN_BUTTON_X)
+ .Top(SPAWN_BUTTON_Y)
+ .BackgroundColor(System.Drawing.Color.Violet).Rounded(5)
+ .Hovered
+ .BackgroundColor(System.Drawing.Color.Pink)
+ .End()
+ .Text("Spawn Slime", Fonts.arial).Alignment(Prowl.PaperUI.TextAlignment.MiddleCenter)
+ .Width(SPAWN_BUTTON_WIDTH)
+ .Height(SPAWN_BUTTON_HEIGHT)
+ .OnClick((e) => SpawnNewSlime())
+ .Enter())
+ {
+
+ }
+ }
+
+ public override void Focus() { }
+ public override void Blur() { }
+ }
+}
\ No newline at end of file