diff --git a/Dashboard.Common/Events/AnimationTickEventArgs.cs b/Dashboard.Common/Events/AnimationTickEventArgs.cs
new file mode 100644
index 0000000..a9971ac
--- /dev/null
+++ b/Dashboard.Common/Events/AnimationTickEventArgs.cs
@@ -0,0 +1,20 @@
+namespace Dashboard.Events
+{
+ public class AnimationTickEventArgs : UiEventArgs
+ {
+ ///
+ /// Animation delta time in seconds.
+ ///
+ public float Delta { get; }
+
+ public AnimationTickEventArgs(float delta) : base(UiEventType.AnimationTick)
+ {
+ Delta = delta;
+ }
+ }
+
+ public interface IAnimationTickEvent
+ {
+ event EventHandler AnimationTimerEvent;
+ }
+}
diff --git a/Dashboard.Common/Events/MouseEvents.cs b/Dashboard.Common/Events/MouseEvents.cs
new file mode 100644
index 0000000..0781c56
--- /dev/null
+++ b/Dashboard.Common/Events/MouseEvents.cs
@@ -0,0 +1,69 @@
+using System.Drawing;
+using System.Numerics;
+
+namespace Dashboard.Events
+{
+ [Flags]
+ public enum MouseButtons
+ {
+ M1 = 1 << 0,
+ M2 = 1 << 1,
+ M3 = 1 << 2,
+ M4 = 1 << 3,
+ M5 = 1 << 4,
+ M6 = 1 << 5,
+ M7 = 1 << 6,
+ M8 = 1 << 7,
+
+ Left = M1,
+ Right = M2,
+ Middle = M3,
+ }
+
+ public sealed class MouseMoveEventArgs : UiEventArgs
+ {
+ public Vector2 ClientPosition { get; }
+ public Vector2 Delta { get; }
+
+ public MouseMoveEventArgs(Vector2 clientPosition, Vector2 delta)
+ : base(UiEventType.MouseMove)
+ {
+ ClientPosition = clientPosition;
+ Delta = delta;
+ }
+ }
+
+ public sealed class MouseButtonEventArgs : UiEventArgs
+ {
+ public Vector2 ClientPosition { get; }
+ public MouseButtons Buttons { get; }
+
+ public MouseButtonEventArgs(Vector2 clientPosition, MouseButtons buttons, bool up)
+ : base(up ? UiEventType.MouseButtonUp : UiEventType.MouseButtonDown)
+ {
+ ClientPosition = clientPosition;
+ Buttons = buttons;
+ }
+ }
+
+ public sealed class MouseScrollEventArgs : UiEventArgs
+ {
+ public Vector2 ClientPosition { get; }
+ public Vector2 ScrollDelta { get; }
+
+ public MouseScrollEventArgs(Vector2 clientPosition, Vector2 scrollDelta)
+ : base(UiEventType.MouseScroll)
+ {
+ ClientPosition = clientPosition;
+ ScrollDelta = scrollDelta;
+ }
+ }
+
+ public interface IMouseEvents
+ {
+ event EventHandler MouseMoved;
+ event EventHandler MouseButtonDown;
+ event EventHandler MouseButtonUp;
+ event EventHandler MouseScroll;
+ }
+}
diff --git a/Dashboard.Common/Events/UiEventArgs.cs b/Dashboard.Common/Events/UiEventArgs.cs
new file mode 100644
index 0000000..eb7615b
--- /dev/null
+++ b/Dashboard.Common/Events/UiEventArgs.cs
@@ -0,0 +1,76 @@
+using System.Numerics;
+
+namespace Dashboard.Events
+{
+ public enum UiEventType
+ {
+ None,
+ AnimationTick, // Generic timer event.
+
+ // Text input related events.
+ KeyDown, // Keyboard key down.
+ KeyUp, // Keyboard key up.
+ TextInput, // Non-IME text event.
+ TextEdit, // IME text event.
+ TextCandidates, // IME text candidate list.
+ TextLanguage, // Keyboard language changed event.
+
+ // Mouse & touch related events
+ MouseButtonDown, // Mouse button down.
+ MouseButtonUp, // Mouse button up.
+ MouseMove, // Mouse moved.
+ MouseScroll, // Mouse scrolled.
+
+ // Reserved event names
+ StylusEnter, // The stylus has entered the hover region.
+ StylusLeave, // The stylus has left the hover region.
+ StylusMove, // The stylus has moved.
+ StylusDown, // The stylus is touching.
+ StylusUp, // The stylus is no longer touching.
+ StylusButtonUp, // Stylus button up.
+ StylusButtonDown, // Stylus button down.
+ StylusAxes, // Extra stylus axes data.
+
+ // Window & Control Events
+ ControlInvalidateVisual, // Force rendering the control again.
+ ControlStateChanged, // Control state changed.
+ ControlMoved, // Control moved.
+ ControlResized, // Control resized.
+ ControlEnter, // The pointing device entered the control.
+ ControlLeave, // The pointing device left the control.
+ ControlFocusGet, // The control acquired focus.
+ ControlFocusLost, // The control lost focus.
+ WindowClose, // The window closed.
+
+ UserRangeStart = 1 << 12,
+ }
+
+ public class UiEventArgs : EventArgs
+ {
+ public UiEventType Type { get; }
+
+ public UiEventArgs(UiEventType type)
+ {
+ Type = type;
+ }
+
+ public static readonly UiEventArgs None = new UiEventArgs(UiEventType.None);
+ }
+
+ public class ControlMovedEventArgs : UiEventArgs
+ {
+ public Vector2 OldPosition { get; }
+ public Vector2 NewPosition { get; }
+
+ public ControlMovedEventArgs(Vector2 oldPosition, Vector2 newPosition) : base(UiEventType.ControlMoved)
+ {
+ OldPosition = oldPosition;
+ NewPosition = newPosition;
+ }
+ }
+
+ public class ControlResizedEventArgs
+ {
+
+ }
+}
diff --git a/Dashboard.Common/Windowing/ICompositor.cs b/Dashboard.Common/Windowing/ICompositor.cs
new file mode 100644
index 0000000..1a61ca1
--- /dev/null
+++ b/Dashboard.Common/Windowing/ICompositor.cs
@@ -0,0 +1,43 @@
+namespace Dashboard.Windowing
+{
+ ///
+ /// Interface for a class that composites multiple windows together.
+ ///
+ public interface ICompositor : IDisposable
+ {
+ void Composite(IPhysicalWindow window, IEnumerable windows);
+ }
+
+ ///
+ /// Interface for classes that implement a window manager.
+ ///
+ public interface IWindowManager : IEnumerable, IEventListener, IPaintable, IDisposable
+ {
+ ///
+ /// The physical window that this window manager is associated with.
+ ///
+ IPhysicalWindow PhysicalWindow { get; }
+
+ ///
+ /// The compositor that will composite all virtual windows.
+ ///
+ ICompositor Compositor { get; set; }
+
+ ///
+ /// The window that is currently focused.
+ ///
+ IVirtualWindow? FocusedWindow { get; }
+
+ ///
+ /// Create a virtual window.
+ ///
+ /// Virtual window handle.
+ IVirtualWindow CreateWindow();
+
+ ///
+ /// Focus a virtual window, if it is owned by this window manager.
+ ///
+ /// The window to focus.
+ void Focus(IVirtualWindow window);
+ }
+}
diff --git a/Dashboard.Common/Windowing/IDeviceContext.cs b/Dashboard.Common/Windowing/IDeviceContext.cs
new file mode 100644
index 0000000..64d7e1c
--- /dev/null
+++ b/Dashboard.Common/Windowing/IDeviceContext.cs
@@ -0,0 +1,20 @@
+using System.Drawing;
+
+namespace Dashboard.Windowing
+{
+ ///
+ /// Generic interface for the rendering system present in a
+ ///
+ public interface IDeviceContext
+ {
+ ///
+ /// The swap group for this device context.
+ ///
+ ISwapGroup SwapGroup { get; }
+
+ ///
+ /// The size of the window framebuffer in pixels.
+ ///
+ Size FramebufferSize { get; }
+ }
+}
\ No newline at end of file
diff --git a/Dashboard.Common/Windowing/IEventListener.cs b/Dashboard.Common/Windowing/IEventListener.cs
new file mode 100644
index 0000000..773fe74
--- /dev/null
+++ b/Dashboard.Common/Windowing/IEventListener.cs
@@ -0,0 +1,11 @@
+namespace Dashboard.Windowing
+{
+ public interface IEventListener
+ {
+ ///
+ /// Send an event to this windowing object.
+ ///
+ /// The event arguments sent.
+ void SendEvent(EventArgs args);
+ }
+}
diff --git a/Dashboard.Common/Windowing/IPaintable.cs b/Dashboard.Common/Windowing/IPaintable.cs
new file mode 100644
index 0000000..ba676bb
--- /dev/null
+++ b/Dashboard.Common/Windowing/IPaintable.cs
@@ -0,0 +1,12 @@
+namespace Dashboard.Windowing
+{
+ public interface IPaintable
+ {
+ event EventHandler Painting;
+
+ ///
+ /// Paint this paintable object.
+ ///
+ void Paint();
+ }
+}
diff --git a/Dashboard.Common/Windowing/ISwapGroup.cs b/Dashboard.Common/Windowing/ISwapGroup.cs
new file mode 100644
index 0000000..b4f4e3a
--- /dev/null
+++ b/Dashboard.Common/Windowing/ISwapGroup.cs
@@ -0,0 +1,18 @@
+namespace Dashboard.Windowing
+{
+ ///
+ /// Interface that is used to swap the buffers of windows
+ ///
+ public interface ISwapGroup
+ {
+ ///
+ /// The swap interval for this swap group.
+ ///
+ public int SwapInterval { get; set; }
+
+ ///
+ /// Swap buffers.
+ ///
+ void Swap();
+ }
+}
\ No newline at end of file
diff --git a/Dashboard.Common/IWindow.cs b/Dashboard.Common/Windowing/IWindow.cs
similarity index 58%
rename from Dashboard.Common/IWindow.cs
rename to Dashboard.Common/Windowing/IWindow.cs
index 565c88d..9b56ce2 100644
--- a/Dashboard.Common/IWindow.cs
+++ b/Dashboard.Common/Windowing/IWindow.cs
@@ -1,11 +1,16 @@
using System.Drawing;
+using Dashboard.Events;
-namespace Dashboard
+namespace Dashboard.Windowing
{
///
/// Base class of all Dashboard windows.
///
- public interface IWindow : IDisposable
+ public interface IWindow :
+ IPaintable,
+ IDisposable,
+ IAnimationTickEvent,
+ IMouseEvents
{
///
/// Name of the window.
@@ -42,46 +47,8 @@ namespace Dashboard
///
/// An object that represents a window in a virtual space, usually another window or a rendering system.
///
- public interface IVirtualWindow : IWindow
+ public interface IVirtualWindow : IWindow, IEventListener
{
- ///
- /// Send an event to this window.
- ///
- /// The event arguments
- /// TODO:
- void SendEvent(EventArgs args);
- }
-
- ///
- /// Generic interface for the rendering system present in a
- ///
- public interface IDeviceContext
- {
- ///
- /// The swap group for this device context.
- ///
- ISwapGroup SwapGroup { get; }
-
- ///
- /// The size of the window framebuffer in pixels.
- ///
- Size FramebufferSize { get; }
- }
-
- ///
- /// Interface that is used to swap the buffers of
- ///
- public interface ISwapGroup
- {
- ///
- /// The swap interval for this swap group.
- ///
- public int SwapInterval { get; set; }
-
- ///
- /// Swap buffers.
- ///
- void Swap();
}
///
@@ -98,5 +65,10 @@ namespace Dashboard
/// True if the window is double buffered.
///
public bool DoubleBuffered { get; }
+
+ ///
+ /// The window manager for this physical window.
+ ///
+ public IWindowManager? WindowManager { get; set; }
}
}
diff --git a/Dashboard.Drawing/IDrawQueuePaintable.cs b/Dashboard.Drawing/IDrawQueuePaintable.cs
new file mode 100644
index 0000000..a719b7d
--- /dev/null
+++ b/Dashboard.Drawing/IDrawQueuePaintable.cs
@@ -0,0 +1,9 @@
+using Dashboard.Windowing;
+
+namespace Dashboard.Drawing
+{
+ public interface IDrawQueuePaintable : IPaintable
+ {
+ DrawQueue DrawQueue { get; }
+ }
+}
diff --git a/Dashboard.OpenGL/IGLContext.cs b/Dashboard.OpenGL/IGLContext.cs
index 072ba62..00f4605 100644
--- a/Dashboard.OpenGL/IGLContext.cs
+++ b/Dashboard.OpenGL/IGLContext.cs
@@ -1,4 +1,5 @@
using System.Drawing;
+using Dashboard.Windowing;
namespace Dashboard.OpenGL
{
diff --git a/Dashboard.OpenTK/PAL2/EventConverter.cs b/Dashboard.OpenTK/PAL2/EventConverter.cs
new file mode 100644
index 0000000..d67f7cd
--- /dev/null
+++ b/Dashboard.OpenTK/PAL2/EventConverter.cs
@@ -0,0 +1,18 @@
+using Dashboard.Events;
+using OpenTK.Platform;
+
+namespace Dashboard.OpenTK.PAL2
+{
+ public static class EventConverter
+ {
+ public static UiEventArgs? Convert(PlatformEventType type, EventArgs ea)
+ {
+ switch (type)
+ {
+ case PlatformEventType.KeyDown:
+ default:
+ return null;
+ }
+ }
+ }
+}
diff --git a/Dashboard.OpenTK/PAL2/OpenGLDeviceContext.cs b/Dashboard.OpenTK/PAL2/OpenGLDeviceContext.cs
index c279092..6d20797 100644
--- a/Dashboard.OpenTK/PAL2/OpenGLDeviceContext.cs
+++ b/Dashboard.OpenTK/PAL2/OpenGLDeviceContext.cs
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Drawing;
using Dashboard.OpenGL;
+using Dashboard.Windowing;
using OpenTK.Mathematics;
using OpenTK.Platform;
using TK = OpenTK.Platform.Toolkit;
diff --git a/Dashboard.OpenTK/PAL2/OpenTKEventExtensions.cs b/Dashboard.OpenTK/PAL2/OpenTKEventExtensions.cs
new file mode 100644
index 0000000..ed1007c
--- /dev/null
+++ b/Dashboard.OpenTK/PAL2/OpenTKEventExtensions.cs
@@ -0,0 +1,7 @@
+namespace Dashboard.OpenTK.PAL2
+{
+ public static class OpenTKEventExtensions
+ {
+ // public static EventArgs ToDashboardEvent(this EventArgs)
+ }
+}
diff --git a/Dashboard.OpenTK/PAL2/Pal2DashboardBackend.cs b/Dashboard.OpenTK/PAL2/Pal2DashboardBackend.cs
new file mode 100644
index 0000000..61b13b4
--- /dev/null
+++ b/Dashboard.OpenTK/PAL2/Pal2DashboardBackend.cs
@@ -0,0 +1,50 @@
+using Dashboard.Windowing;
+using OpenTK.Graphics;
+using OpenTK.Platform;
+using TK = OpenTK.Platform.Toolkit;
+
+namespace Dashboard.OpenTK.PAL2
+{
+ public class Pal2DashboardBackend : IDashboardBackend
+ {
+ public GraphicsApiHints GraphicsApiHints { get; set; } = new OpenGLGraphicsApiHints();
+ public bool OpenGLBindingsInitialized { get; set; } = false;
+
+ public IPhysicalWindow CreatePhysicalWindow()
+ {
+ PhysicalWindow window = new PhysicalWindow(GraphicsApiHints);
+
+ if (!OpenGLBindingsInitialized)
+ {
+ OpenGLBindingsInitialized = true;
+ GLLoader.LoadBindings(
+ new Pal2BindingsContext(TK.OpenGL,
+ ((OpenGLDeviceContext)window.DeviceContext).ContextHandle));
+ }
+
+ return window;
+ }
+
+ public virtual IWindow CreateWindow()
+ {
+ return CreatePhysicalWindow();
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public void Initialize()
+ {
+ }
+
+ public void Leave()
+ {
+ }
+
+ public void RunEvents(bool wait)
+ {
+ TK.Window.ProcessEvents(wait);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Dashboard.OpenTK/PAL2/PhysicalWindow.cs b/Dashboard.OpenTK/PAL2/PhysicalWindow.cs
index 8c0943a..569f63f 100644
--- a/Dashboard.OpenTK/PAL2/PhysicalWindow.cs
+++ b/Dashboard.OpenTK/PAL2/PhysicalWindow.cs
@@ -1,16 +1,24 @@
-using System.Drawing;
+using System.Collections.Concurrent;
+using System.Drawing;
+using Dashboard.Drawing;
+using Dashboard.Events;
+using Dashboard.Windowing;
using OpenTK.Mathematics;
using OpenTK.Platform;
+using MouseMoveEventArgs = Dashboard.Events.MouseMoveEventArgs;
using TK = OpenTK.Platform.Toolkit;
namespace Dashboard.OpenTK.PAL2
{
- public class PhysicalWindow : IPhysicalWindow
+ public class PhysicalWindow : IPhysicalWindow, IDrawQueuePaintable, IEventListener
{
+ public DrawQueue DrawQueue { get; } = new DrawQueue();
public WindowHandle WindowHandle { get; }
public IDeviceContext DeviceContext { get; }
public bool DoubleBuffered => true; // Always true for OpenTK windows.
+ public IWindowManager? WindowManager { get; set; }
+
public string Title
{
get => TK.Window.GetTitle(WindowHandle);
@@ -37,22 +45,32 @@ namespace Dashboard.OpenTK.PAL2
set => TK.Window.SetClientSize(WindowHandle, new Vector2i((int)value.Width, (int)value.Height));
}
+ public event EventHandler? Painting;
+ public event EventHandler? AnimationTimerEvent;
+ public event EventHandler? MouseMoved;
+ public event EventHandler? MouseButtonDown;
+ public event EventHandler? MouseButtonUp;
+ public event EventHandler? MouseScroll;
+
public PhysicalWindow(WindowHandle window, IDeviceContext dc)
{
WindowHandle = window;
DeviceContext = dc;
+ AddWindow(this);
}
public PhysicalWindow(WindowHandle window, OpenGLContextHandle context, ISwapGroup? swapGroup = null)
{
WindowHandle = window;
DeviceContext = new OpenGLDeviceContext(window, context, swapGroup);
+ AddWindow(this);
}
public PhysicalWindow(GraphicsApiHints hints)
{
WindowHandle = TK.Window.Create(hints);
DeviceContext = CreateDeviceContext(WindowHandle, hints);
+ AddWindow(this);
}
private static IDeviceContext CreateDeviceContext(WindowHandle window, GraphicsApiHints hints)
@@ -74,8 +92,73 @@ namespace Dashboard.OpenTK.PAL2
if (_isDisposed) return;
_isDisposed = true;
+ RemoveWindow(this);
+
(DeviceContext as IDisposable)?.Dispose();
TK.Window.Destroy(WindowHandle);
}
+
+ protected virtual void OnPaint()
+ {
+ WindowManager?.Paint();
+ Painting?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void Paint()
+ {
+ DrawQueue.Clear();
+ OnPaint();
+ }
+
+ protected virtual void OnAnimationTimerEvent(AnimationTickEventArgs ea) =>
+ AnimationTimerEvent?.Invoke(this, ea);
+
+ protected virtual void OnMouseMoved(MouseMoveEventArgs ea) => MouseMoved?.Invoke(this, ea);
+
+ protected virtual void OnMouseButtonDown(MouseButtonEventArgs ea) => MouseButtonDown?.Invoke(this, ea);
+
+ protected virtual void OnMouseButtonUp(MouseButtonEventArgs ea) => MouseButtonUp?.Invoke(this, ea);
+
+ protected virtual void OnMouseScroll(MouseScrollEventArgs ea) => MouseScroll?.Invoke(this, ea);
+
+ public void SendEvent(EventArgs args)
+ {
+ }
+
+ private static readonly ConcurrentDictionary _windows =
+ new ConcurrentDictionary();
+
+ static PhysicalWindow()
+ {
+ EventQueue.EventRaised += EventQueueOnEventRaised;
+ }
+
+ private static void AddWindow(PhysicalWindow window)
+ {
+ _windows.TryAdd(window.WindowHandle, window);
+ }
+
+ private static void RemoveWindow(PhysicalWindow window)
+ {
+ _windows.TryRemove(window.WindowHandle, out _);
+ }
+
+ private static void EventQueueOnEventRaised(PalHandle? handle, PlatformEventType type, EventArgs args)
+ {
+ if (handle is not WindowHandle windowHandle)
+ return;
+
+ if (!_windows.TryGetValue(windowHandle, out PhysicalWindow? window))
+ return;
+
+ switch (type)
+ {
+ case PlatformEventType.MouseMove:
+ case PlatformEventType.Scroll:
+ case PlatformEventType.MouseUp:
+ case PlatformEventType.MouseDown:
+ break;
+ }
+ }
}
}
diff --git a/Dashboard/Application.cs b/Dashboard/Application.cs
new file mode 100644
index 0000000..c9c1bd0
--- /dev/null
+++ b/Dashboard/Application.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Dashboard.Windowing;
+
+namespace Dashboard
+{
+ public class Application : IDisposable
+ {
+ public IDashboardBackend Backend { get; }
+ public IWindowFactory WindowFactory { get; set; }
+
+ public string Name { get; } = "Dashboard Application";
+
+ protected bool IsInitialized { get; private set; } = false;
+ protected bool IsDisposed { get; private set; } = false;
+
+ private readonly List _physicalWindows = new List();
+
+ public event EventHandler? PreInitializing;
+ public event EventHandler? Initializing;
+ public event EventHandler? PostInitializing;
+ public event EventHandler? Leaving;
+
+ public Application(IDashboardBackend backend)
+ {
+ Backend = backend;
+ WindowFactory = backend;
+ }
+
+ protected virtual void PreInitialize()
+ {
+ PreInitializing?.Invoke(this, EventArgs.Empty);
+ }
+
+ public virtual void Initialize()
+ {
+ Backend.Initialize();
+ Initializing?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected virtual void PostInitialize()
+ {
+ PostInitializing?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void InitializeInternal()
+ {
+ if (IsInitialized)
+ return;
+
+ IsInitialized = true;
+
+ PreInitialize();
+ Initialize();
+ PostInitialize();
+ }
+
+ public virtual void RunEvents(bool wait)
+ {
+ if (!IsInitialized)
+ throw new InvalidOperationException("The application is not initialized. Cannot run events at this time.");
+
+ Backend.RunEvents(wait);
+ }
+
+ public virtual void Leave()
+ {
+ Backend.Leave();
+ }
+
+ public void Run() => Run(true, CancellationToken.None);
+ public void Run(bool wait) => Run(wait, CancellationToken.None);
+
+ public void Run(bool waitForEvents, CancellationToken token)
+ {
+ InitializeInternal();
+
+ while (!token.IsCancellationRequested)
+ {
+ RunEvents(waitForEvents);
+
+ foreach (IPhysicalWindow window in _physicalWindows)
+ {
+ window.Paint();
+ }
+ }
+
+ Leave();
+ }
+
+ ///
+ public IWindow CreateWindow()
+ {
+ IWindow window = WindowFactory.CreateWindow();
+
+ if (window is IPhysicalWindow physical)
+ _physicalWindows.Add(physical);
+
+ return window;
+ }
+
+ ///
+ public IPhysicalWindow CreatePhysicalWindow()
+ {
+ IPhysicalWindow window = WindowFactory.CreatePhysicalWindow();
+ _physicalWindows.Add(window);
+ return window;
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ protected void InvokeDispose(bool disposing)
+ {
+ if (IsDisposed)
+ return;
+
+ IsDisposed = true;
+
+ Dispose(disposing);
+
+ if (disposing)
+ GC.SuppressFinalize(this);
+ }
+
+ public void Dispose() => InvokeDispose(true);
+ }
+}
\ No newline at end of file
diff --git a/Dashboard/Controls/ClassSet.cs b/Dashboard/Controls/ClassSet.cs
new file mode 100644
index 0000000..019cc93
--- /dev/null
+++ b/Dashboard/Controls/ClassSet.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Dashboard.Controls
+{
+ public enum ClassChangeType
+ {
+ Added,
+ Removed,
+ }
+
+ public record struct ClassChanged(object? Owner, string ClassName, ClassChangeType Type);
+
+ public class ClassSet : ICollection
+ {
+ public int Count => _classes.Count;
+ public bool IsReadOnly => false;
+ public object? Owner { get; }
+ public event EventHandler? ClassChanged;
+
+ private readonly HashSet _classes = new HashSet();
+
+ public ClassSet(object? owner)
+ {
+ Owner = owner;
+ }
+
+ public IEnumerator GetEnumerator() => _classes.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ public void Add(string item)
+ {
+ if (_classes.Add(item))
+ {
+ OnClassAdded(item);
+ }
+ }
+
+ public void Clear()
+ {
+ foreach (string @class in _classes)
+ OnClassRemoved(@class);
+
+ _classes.Clear();
+ }
+
+ public bool Contains(string item) => _classes.Contains(item);
+
+ public void CopyTo(string[] array, int arrayIndex) => _classes.CopyTo(array, arrayIndex);
+
+ public bool Remove(string item)
+ {
+ if (_classes.Remove(item))
+ {
+ OnClassRemoved(item);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void OnClassAdded(string @class)
+ {
+ ClassChanged?.Invoke(this, new ClassChanged(Owner, @class, ClassChangeType.Added));
+ }
+
+ private void OnClassRemoved(string @class)
+ {
+ ClassChanged?.Invoke(this, new ClassChanged(Owner, @class, ClassChangeType.Removed));
+ }
+ }
+}
diff --git a/Dashboard/Controls/Control.cs b/Dashboard/Controls/Control.cs
new file mode 100644
index 0000000..72c1a78
--- /dev/null
+++ b/Dashboard/Controls/Control.cs
@@ -0,0 +1,68 @@
+using System;
+using Dashboard.Drawing;
+using Dashboard.Windowing;
+
+namespace Dashboard.Controls
+{
+ public class Control : IEventListener, IDrawQueuePaintable, IDisposable
+ {
+ public string? Id { get; set; }
+ public ClassSet Classes { get; }
+ public Form? Owner { get; protected set; } = null;
+ public Control? Parent { get; private set; } = null;
+ public bool Disposed { get; private set; }
+ public virtual DrawQueue DrawQueue => Owner?.DrawQueue ?? throw NoOwnerException;
+ public virtual Box2d ClientArea { get; set; }
+
+ public event EventHandler? Painting;
+ public event EventHandler? OwnerChanged;
+ public event EventHandler? ParentChanged;
+ public event EventHandler? Disposing;
+ public event EventHandler? Resized;
+
+ public Control()
+ {
+ Classes = new ClassSet(this);
+ }
+
+ public virtual void OnPaint()
+ {
+ Painting?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void Paint()
+ {
+ OnPaint();
+ }
+
+ protected void InvokeDispose(bool disposing)
+ {
+ if (Disposed)
+ return;
+ Disposed = true;
+
+ Dispose(disposing);
+
+ if (disposing)
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ }
+
+ public void Dispose() => InvokeDispose(true);
+
+ public void SendEvent(EventArgs args)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal static void SetParent(Control parent, Control child)
+ {
+ child.Parent = parent;
+ }
+
+ protected static Exception NoOwnerException => new Exception("No form owns this control");
+ }
+}
diff --git a/Dashboard/Controls/Form.cs b/Dashboard/Controls/Form.cs
new file mode 100644
index 0000000..cc39426
--- /dev/null
+++ b/Dashboard/Controls/Form.cs
@@ -0,0 +1,23 @@
+using Dashboard.Drawing;
+using Dashboard.Windowing;
+
+namespace Dashboard.Controls
+{
+ public class Form : Control
+ {
+ public IWindow Window { get; }
+ public override DrawQueue DrawQueue { get; }
+
+ public override Box2d ClientArea
+ {
+ get => new Box2d(0, 0, Window.ClientSize.Width, Window.ClientSize.Height);
+ set { }
+ }
+
+ public Form(IWindow window)
+ {
+ Window = window;
+ DrawQueue = (window as IDrawQueuePaintable)?.DrawQueue ?? new DrawQueue();
+ }
+ }
+}
diff --git a/Dashboard/Controls/Label.cs b/Dashboard/Controls/Label.cs
new file mode 100644
index 0000000..f03339c
--- /dev/null
+++ b/Dashboard/Controls/Label.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Drawing;
+using System.Numerics;
+using Dashboard.Drawing;
+
+namespace Dashboard.Controls
+{
+ public class Label : Control
+ {
+ public bool AutoSize { get; set; } = true;
+ public string Text { get; set; } = "";
+
+ public event EventHandler? TextChanged;
+
+ protected IBrush TextBrush => throw new NotImplementedException();
+ protected IFont Font => throw new NotImplementedException();
+
+ protected virtual void OnTextChanged(string oldValue, string newValue)
+ {
+ if (AutoSize)
+ CalculateSize();
+ }
+
+ protected void CalculateSize()
+ {
+ SizeF sz = Typesetter.MeasureString(Font, Text);
+ ClientArea = new Box2d(ClientArea.Min, ClientArea.Min + (Vector2)sz);
+ }
+
+
+ public override void OnPaint()
+ {
+ base.OnPaint();
+ DrawQueue.Text(new Vector3(ClientArea.Min, 0), TextBrush, Text, Font);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Dashboard/IDashboardBackend.cs b/Dashboard/IDashboardBackend.cs
new file mode 100644
index 0000000..f56ecb4
--- /dev/null
+++ b/Dashboard/IDashboardBackend.cs
@@ -0,0 +1,29 @@
+using System;
+using Dashboard.Windowing;
+
+namespace Dashboard
+{
+ public interface IWindowFactory
+ {
+ ///
+ /// Creates a window. It could be a virtual window, or a physical window.
+ ///
+ /// A window.
+ IWindow CreateWindow();
+
+ ///
+ /// Always creates a physical window.
+ ///
+ /// A physical window.
+ IPhysicalWindow CreatePhysicalWindow();
+ }
+
+ public interface IDashboardBackend : IDisposable, IWindowFactory
+ {
+ void Initialize();
+
+ void RunEvents(bool wait);
+
+ void Leave();
+ }
+}
\ No newline at end of file
diff --git a/tests/Dashboard.TestApplication/Program.cs b/tests/Dashboard.TestApplication/Program.cs
index a63bd3e..17a80ea 100644
--- a/tests/Dashboard.TestApplication/Program.cs
+++ b/tests/Dashboard.TestApplication/Program.cs
@@ -10,6 +10,7 @@ using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using Box2d = Dashboard.Box2d;
using TK = OpenTK.Platform.Toolkit;
+using Dashboard;
TK.Init(new ToolkitOptions()
{
@@ -21,149 +22,156 @@ TK.Init(new ToolkitOptions()
}
});
-PhysicalWindow window = new PhysicalWindow(new OpenGLGraphicsApiHints()
-{
- Version = new Version(3, 2),
- ForwardCompatibleFlag = true,
- DebugFlag = true,
- Profile = OpenGLProfile.Core,
- sRGBFramebuffer = true,
+Application app = new Application(new Pal2DashboardBackend()
+{
+ GraphicsApiHints = new OpenGLGraphicsApiHints()
+ {
+ Version = new Version(3, 2),
+ ForwardCompatibleFlag = true,
+ DebugFlag = true,
+ Profile = OpenGLProfile.Core,
+ sRGBFramebuffer = true,
- SwapMethod = ContextSwapMethod.Undefined,
+ SwapMethod = ContextSwapMethod.Undefined,
- RedColorBits = 8,
- GreenColorBits = 8,
- BlueColorBits = 8,
- AlphaColorBits = 8,
- Multisamples = 0,
+ RedColorBits = 8,
+ GreenColorBits = 8,
+ BlueColorBits = 8,
+ AlphaColorBits = 8,
+ Multisamples = 0,
- SupportTransparentFramebufferX11 = true,
+ SupportTransparentFramebufferX11 = true,
+ }
});
-window.Title = "DashTerm";
-TK.Window.SetMinClientSize(window.WindowHandle, 300, 200);
-TK.Window.SetClientSize(window.WindowHandle, new Vector2i(320, 240));
-TK.Window.SetBorderStyle(window.WindowHandle, WindowBorderStyle.ResizableBorder);
-// TK.Window.SetTransparencyMode(wnd, WindowTransparencyMode.TransparentFramebuffer, 0.1f);
-
-OpenGLDeviceContext context = (OpenGLDeviceContext)window.DeviceContext;
-
-context.MakeCurrent();
-context.SwapGroup.SwapInterval = 1;
-
-GLLoader.LoadBindings(new Pal2BindingsContext(TK.OpenGL, context.ContextHandle));
-
-DrawQueue queue = new DrawQueue();
+PhysicalWindow window;
SolidBrush fg = new SolidBrush(Color.FromArgb(0, 0, 0, 0));
SolidBrush bg = new SolidBrush(Color.Black);
-bool shouldExit = false;
-
-GLEngine engine = new GLEngine();
-engine.Initialize();
-
-ContextExecutor executor = engine.GetExecutor(context);
-DimUI dimUI = new DimUI(new DimUIConfig()
-{
- Font = new NamedFont("Noto Sans", 9f),
-});
-
+CancellationTokenSource source = new CancellationTokenSource();
+GLEngine engine;
+ContextExecutor executor;
+DimUI dimUI;
Vector2 mousePos = Vector2.Zero;
Random r = new Random();
-EventQueue.EventRaised += (handle, type, eventArgs) =>
-{
- if (handle != window.WindowHandle)
- return;
-
- switch (type)
- {
- case PlatformEventType.Close:
- shouldExit = true;
- break;
- case PlatformEventType.MouseMove:
- mousePos = ((MouseMoveEventArgs)eventArgs).ClientPosition;
- break;
- }
-};
-
-TK.Window.SetMode(window.WindowHandle, WindowMode.Normal);
-
List points = new List();
-
-IFont font = Typesetter.LoadFont("Nimbus Mono", 12f);
+IFont font;
StringBuilder builder = new StringBuilder();
-while (!shouldExit)
-{
- TK.Window.ProcessEvents(true);
- TK.Window.GetFramebufferSize(context.WindowHandle, out Vector2i framebufferSize);
- executor.BeginFrame();
+app.PostInitializing += (sender, ea) => {
+ window = (PhysicalWindow)app.CreatePhysicalWindow();
- dimUI.Begin(new Box2d(0, 0, framebufferSize.X, framebufferSize.Y), queue);
- dimUI.Text("Hello World!");
- dimUI.Button("Cancel"); dimUI.SameLine();
- dimUI.Button("OK");
+ window.Title = "DashTerm";
+ TK.Window.SetMinClientSize(window.WindowHandle, 300, 200);
+ TK.Window.SetClientSize(window.WindowHandle, new Vector2i(320, 240));
+ TK.Window.SetBorderStyle(window.WindowHandle, WindowBorderStyle.ResizableBorder);
+ // TK.Window.SetTransparencyMode(wnd, WindowTransparencyMode.TransparentFramebuffer, 0.1f);
- dimUI.Input("type me!", builder);
+ OpenGLDeviceContext context = (OpenGLDeviceContext)window.DeviceContext;
- dimUI.BeginMenu();
+ context.MakeCurrent();
+ context.SwapGroup.SwapInterval = 1;
- if (dimUI.MenuItem("File"))
+ engine = new GLEngine();
+ engine.Initialize();
+
+ executor = engine.GetExecutor(context);
+
+ dimUI = new DimUI(new DimUIConfig()
{
- dimUI.BeginMenu();
- dimUI.MenuItem("New Window");
- dimUI.MenuItem("Preferences");
- dimUI.MenuItem("Exit");
- dimUI.EndMenu();
- }
+ Font = new NamedFont("Noto Sans", 9f),
+ });
- if (dimUI.MenuItem("Edit"))
+ EventQueue.EventRaised += (handle, type, eventArgs) =>
{
- dimUI.BeginMenu();
- dimUI.MenuItem("Cut");
- dimUI.MenuItem("Copy");
- dimUI.MenuItem("Paste");
+ if (handle != window.WindowHandle)
+ return;
- if (dimUI.MenuItem("Send Char"))
+ switch (type)
{
- dimUI.BeginMenu();
- dimUI.EndMenu();
+ case PlatformEventType.Close:
+ source.Cancel();
+ break;
+ case PlatformEventType.MouseMove:
+ mousePos = ((MouseMoveEventArgs)eventArgs).ClientPosition;
+ break;
}
+ };
- dimUI.EndMenu();
- }
+ TK.Window.SetMode(window.WindowHandle, WindowMode.Normal);
+ font = Typesetter.LoadFont("Nimbus Mono", 12f);
- if (dimUI.MenuItem("View"))
- {
- dimUI.BeginMenu();
- dimUI.MenuItem("Clear");
+ window.Painting += (sender, ea) => {
+ TK.Window.GetFramebufferSize(context.WindowHandle, out Vector2i framebufferSize);
+ executor.BeginFrame();
+
+ dimUI.Begin(new Box2d(0, 0, framebufferSize.X, framebufferSize.Y), window.DrawQueue);
+ dimUI.Text("Hello World!");
+ dimUI.Button("Cancel"); dimUI.SameLine();
+ dimUI.Button("OK");
+
+ dimUI.Input("type me!", builder);
- if (dimUI.MenuItem("Set Size"))
- {
dimUI.BeginMenu();
- dimUI.MenuItem("24 x 40");
- dimUI.MenuItem("25 x 40");
- dimUI.MenuItem("24 x 80");
- dimUI.MenuItem("25 x 80");
- dimUI.MenuItem("25 x 120");
- dimUI.EndMenu();
- }
- dimUI.EndMenu();
- }
+ if (dimUI.MenuItem("File"))
+ {
+ dimUI.BeginMenu();
+ dimUI.MenuItem("New Window");
+ dimUI.MenuItem("Preferences");
+ dimUI.MenuItem("Exit");
+ dimUI.EndMenu();
+ }
- dimUI.Finish();
+ if (dimUI.MenuItem("Edit"))
+ {
+ dimUI.BeginMenu();
+ dimUI.MenuItem("Cut");
+ dimUI.MenuItem("Copy");
+ dimUI.MenuItem("Paste");
- GL.Viewport(0, 0, framebufferSize.X, framebufferSize.Y);
- GL.ClearColor(0.3f, 0.3f, 0.3f, 1.0f);
- GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
- GL.Disable(EnableCap.DepthTest);
- GL.Enable(EnableCap.Blend);
- GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
- GL.ColorMask(true, true, true, true);
+ if (dimUI.MenuItem("Send Char"))
+ {
+ dimUI.BeginMenu();
+ dimUI.EndMenu();
+ }
- executor.Draw(queue);
- executor.EndFrame();
- queue.Clear();
+ dimUI.EndMenu();
+ }
- context.SwapGroup.Swap();
-}
+ if (dimUI.MenuItem("View"))
+ {
+ dimUI.BeginMenu();
+ dimUI.MenuItem("Clear");
+
+ if (dimUI.MenuItem("Set Size"))
+ {
+ dimUI.BeginMenu();
+ dimUI.MenuItem("24 x 40");
+ dimUI.MenuItem("25 x 40");
+ dimUI.MenuItem("24 x 80");
+ dimUI.MenuItem("25 x 80");
+ dimUI.MenuItem("25 x 120");
+ dimUI.EndMenu();
+ }
+
+ dimUI.EndMenu();
+ }
+
+ dimUI.Finish();
+
+ GL.Viewport(0, 0, framebufferSize.X, framebufferSize.Y);
+ GL.ClearColor(0.3f, 0.3f, 0.3f, 1.0f);
+ GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+ GL.Disable(EnableCap.DepthTest);
+ GL.Enable(EnableCap.Blend);
+ GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+ GL.ColorMask(true, true, true, true);
+
+ executor.Draw(window.DrawQueue);
+ executor.EndFrame();
+
+ context.SwapGroup.Swap();
+ };
+};
+
+app.Run(true, source.Token);