38 Commits

Author SHA1 Message Date
c8946673c3 Fix layoutorder on file entries spazzing out in non-testbed apps. 2025-07-08 14:33:35 -05:00
d0a246bffe Update file information display in FileDialog. 2025-07-08 13:31:50 -05:00
e084e1120f Trying to solve for arrow angles. 2025-06-28 00:36:53 -05:00
238a7934b3 Adding Anchors and Decorator Widget types. 2025-06-27 19:55:02 -05:00
558f17fc94 Make ScrollingRect key-binds only work when mouse is inside. (This is subject to change) 2025-06-27 15:30:11 -05:00
4acd91b078 ContextMenu now opens on mouse-release versus initial mouse-press. TODO: Make it an enum configuration. 2025-06-27 15:29:17 -05:00
944bd66b43 Refactor CommandLine to not naiively override Window::Open, Close() 2025-06-27 15:28:06 -05:00
d935608e05 Fixed Window close button simply setting the element to invisible and not properly calling Open()/Close() procedure. 2025-06-27 15:27:29 -05:00
3b973c2c45 Added input-history feature to TextInputForm. Control via up/down arrows. 2025-06-27 15:26:33 -05:00
ce0c69190a Testing more of the filesystem API 2025-06-26 21:45:35 -05:00
dca15511ea Other edits. 2025-06-26 06:03:35 -05:00
beb5c97c13 Add UDim::FromPixels and UDim::FromScale constructors 2025-06-26 06:03:27 -05:00
505c2c70e6 Fixed Separator class spacing behavior 2025-06-26 06:03:04 -05:00
9b07a8ea01 Test of FileDialog.hpp 2025-06-26 06:02:43 -05:00
393ad1b2b3 Somewhat Fixed Checkbox drawing. 2025-06-26 06:02:30 -05:00
67416540d4 Typo 2025-06-26 05:36:27 -05:00
d9d92c2a28 Adjusting demo app to take screenshots. 2025-06-25 17:40:56 -05:00
086a3f226b Writing documentation in widgets.md, will deploy doxygen API reference tomorrow. 2025-06-25 02:46:48 -05:00
a2088b086b Add Tooltip::PopupDelay 2025-06-24 15:08:35 -05:00
86fb0cb2e6 Implementing hack for tooltip. 2025-06-24 13:46:33 -05:00
3a0743e787 Add FpsGraphWindow::DockGraph and UndockGraph 2025-06-24 02:52:48 -05:00
f46bf097ac Documentate Slider 2025-06-24 02:51:49 -05:00
b80fa9cd94 Add Window::HideTitlebar and ShowTitlebar 2025-06-24 02:51:20 -05:00
f229698971 Enhanced demo program. 2025-06-24 01:58:02 -05:00
96239db592 Redesign ColorPicker widget with the new slider capabilities in mind. 2025-06-24 01:57:53 -05:00
9d901c6300 Implement static draw routines to RectBase to unify JUI's rectangle motifs. 2025-06-24 01:57:38 -05:00
d5d6703ee9 Redesigned slider to support customizing the size of the Scrubber. 2025-06-24 01:57:14 -05:00
72e21451a6 Spastic Edits 2025-06-23 22:47:27 -05:00
8af6030d1a Refactored Slider to use an internal range of 0-1 and map it to a set range for the end-user. 2025-06-23 21:05:40 -05:00
0e62fcd5f6 Developing documentation. 2025-06-22 22:01:11 -05:00
938be6f7ca A ton of edits.. I don't even know 2025-06-16 12:02:10 -05:00
1c523dafa2 More supplemental docs. 2025-06-16 01:58:45 -05:00
80682e5fee Test of links to supplemental documentation. 2025-06-16 01:35:54 -05:00
fa5b9e23cf Documenting Link Widget. 2025-06-15 23:50:07 -05:00
3a0901693e Add LabeledSlider class. 2025-06-13 13:57:28 -05:00
4415e3c6b4 Test of Link Widget, brings me back around to several old issues that still need resolved. 2025-06-12 03:45:37 -05:00
1bd581a5d8 FPSGraphWindow 2.0 2025-06-09 20:48:12 -05:00
31d5074ed0 FPSGraphWindow 2025-06-09 20:16:05 -05:00
58 changed files with 5003 additions and 549 deletions

View File

@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.18..3.27)
project(JUI
VERSION 1.1
VERSION 4.0
LANGUAGES CXX)
if (PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR)

2782
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -15,24 +15,7 @@ It is expressly built with our Redacted3D engine in mind, but steps have been ta
JUI provides a set of objects that we term Widgets, which can be styled and laid out on-screen in relation to each other. Each widget has a single parent, and a list of child elements. Your root widget should be a Scene object.
## Features
* Comprehensive list of common UI widgets:
* ![Scene](https://git.redacted.cc/josh/ReJUI/wiki/Scene)
* ![Rect](https://git.redacted.cc/josh/ReJUI/wiki/Rect)
* ![Text](https://git.redacted.cc/josh/ReJUI/wiki/Text)
* ![TextRect](https://git.redacted.cc/josh/ReJUI/wiki/TextRect)
* ![Button](https://git.redacted.cc/josh/ReJUI/wiki/Button)
* ![TextButton](https://git.redacted.cc/josh/ReJUI/wiki/TextButton)
* ![TextInputForm](https://git.redacted.cc/josh/ReJUI/wiki/TextInputForm)
* ![Slider](https://git.redacted.cc/josh/ReJUI/wiki/Slider)
* ![Image](https://git.redacted.cc/josh/ReJUI/wiki/Image)
* ![ImageRect](https://git.redacted.cc/josh/ReJUI/wiki/ImageRect)
* ![RadioButton](https://git.redacted.cc/josh/ReJUI/wiki/RadioButton)
* ![Window](https://git.redacted.cc/josh/ReJUI/wiki/Window)
* ![Checkbox](https://git.redacted.cc/josh/ReJUI/wiki/Checkbox)
* ![NineSliceRect](https://git.redacted.cc/josh/ReJUI/wiki/NineSliceRect)
* Vertical and Horizontal ListLayout, GridLayout
* Separator
* ScrollRect
* Flexible and comprehensive set of Widgets. [Full List Here](docs/widgets.md)
* Extendable - Widgets can be extended via class derivation, and even combined to create complex behavior.
* Low-overhead stateful GUI elements.
* Easy integration with your project. Simply provide update, draw, and user-input callbacks to your scene.
@@ -58,6 +41,10 @@ Currently, the package is also integrated with ![J3ML](https://git.redacted.cc/j
Documentation is automatically generated from latest commit and is hosted at https://doc.redacted.cc/jui .
[An overview of the library is available here.](docs/overview.md)
[A full list of widgets is available here.](docs/widgets.md)
## Contributing
Contributions to JUI are welcome! Feel free to file bug reports or feature requests by creating an Issue. Pull requests are also very welcome!

0
docs/animation.md Normal file
View File

0
docs/layout.md Normal file
View File

29
docs/overview.md Normal file
View File

@@ -0,0 +1,29 @@
<details>
<summary>Data Types</summary>
<br/>
<details>
<summary>UDim - Universal Dimension</summary>
This is some information about the Widget class.
</details>
<br/>
<details>
<summary>UDim2 Universal Dimension 2D </summary>
This is some information about the Widget class.
</details>
<br/>
<details>
<summary>Event Event Signalling API</summary>
This is some information about the Widget class.
</details>
<br/>
<details>
<summary>mcolor::Color3, mcolor::Color4 - Color Representation API </summary>
This is some information about the Widget class.
</details>
<br/>
</details>
<details><summary>Widgets</summary>
This is some information about the Widget class.
</details>

0
docs/styling.md Normal file
View File

300
docs/widgets.md Normal file
View File

@@ -0,0 +1,300 @@
## JUI Widget Directory
Updated on 2025-06-25 / Release-6.4 [^1]
See also:
[Animation](animation.md) <br/>
[Layout](layout.md) <br/>
[Overview](overview.md) <br/>
[Styling](styling.md) <br/>
[Full API Reference](https://doc.redacted.cc/jui) <br/>
[Repository](https://git.redacted.cc/josh/ReJUI)
<details><summary>Widget</summary>
* The [Widget](doc.redacted.cc/jui/) is the basis of all GUI elements in JUI.
* It is an abstract class, and you should not directly instantiate `Widget` for use in your UI.
* Instead, create its concrete derived types (`Button`, `Text`, `Slider`) or your own custom derivation.
* `class Widget` serves as the starting point for creating custom widgets.
* Widgets are organized in a tree-like hierarchy, where each widget can have multiple **children** and a single **parent**.
* This structure dictates rendering order, event propagation, and coordinate systems.
* Operations like updating, drawing, and event dispatching typically traverse this hierarchy **recursively**.
* A Widget's on-screen position is always relative to its parent. This simplifies layout management, as moving a parent automatically moves all its children. Sizing is also handled in relation to the parent.
* Widgets can be given a custom string identifier which can be used to search for it in the hierarchy later.
* ```Widget::Name("unique_element_4");```
```cpp
// Dispatch a "MouseMovement" event until a widget indicates that it has "observed" the event.
for (auto* widget : my_element->GetChildren()) {
if (widget->ObserveMouseMovement({420, 420}) return;
}
// Listens for an event of a child widget being added.
// These event-hooks are provided to make customization easier without requiring you to derive the class.
my_element->ChildAdded += [] (Widget* child) {
child->Name("New Child Element!");
};
```
</details>
<details><summary>Scene</summary>
* The [Scene]() widget acts as the **root container** for all your JUI elements within a window or application.
* It's the top-level `Widget` that manages the entire hierarchy of your UI.
* You'll typically have one `Scene` per window, serving as the canvas upon which all other widgets are placed and rendered.
* Every JUI application or window requires a `Scene` to host its user interface.
* All other widgets become descendants of the `Scene`.
* A scene is a **mandatory** component for a JUI application. You cannot display widgets without placing them in a `Scene`.
* Unlike other `Widget` types, a `Scene` does not have a parent itself. It's the **root** of the hierarchy.
* Event Sink - Events from the OS / Framework (mouse clicks, key presses) are first received by the `Scene`, which then initiates the recursive dispatch process down its tree.
* Z-Ordering: While individual widgets use Z-Index for sibling ordering, the `Scene` ensures that its entire content is rendered correctly, typically on top of the OpenGL scene if applicable.
* A scene's viewport (aka canvas size) can be set to the window viewport, or an arbitrary value.
* The scene provides a GlobalUIScale property, that can be used to scale an entire GUI uniformly.
```cpp
auto* scene = new JUI::Scene();
auto* text = new JUI::Text(scene);
text->Content("Hello, World!");
void app_key_press(Key key) {
// Returns whether any widget should have "consumed" the event.
bool sunk = scene->ObserveKeyInput(key, true);
}
void app_key_release(Key key) {
// Returns whether any widget should have "consumed" the event.
bool sunk = scene->ObserveKeyInput(key, false);
}
```
</details>
<details>
<summary>BindMenu</summary>
### The BindMenu Widget is a work-in-progress. Check back in Prerelease 7.
</details>
<details>
<summary>Button</summary>
This is some information about the Widget class.
</details>
<details><summary>Checkbox</summary>
* The `Checkbox` widget provides a simple binary toggle input, allowing users to select or deselect an option.
* Typically Consists of a small square box.
* When clicked, the box's state flips, indicating whether the corresponding option is enabled or disabled.
* Also see LabeledCheckbox for a Checkbox with integrated text.
* Usages:
* Enabling / Disabling Features
* Opt-in / Opt-out Choices:
* Multiple Selections - When presented in a group, checkboxes allow users to select zero, one or multiple independent options simultaneously.
* Contrast with Radio Buttons, which allow only one selection from a group.
* Boolean Settings
</details>
<details><summary>Collapsible</summary>
* The `Collapsible` widget provides a way to **show or hide** a section of UI content with a single click, typically on a header or title bar.
* It's a space-saving component that allows users to expand details when needed and collapse them to reduce visual clutter.
* It's ideal for settings panels, property inspectors, or long lists of categorized information.
* Usages:
* Settings Panel - Group related settings that can be expanded individually.
* Accordions - Create a series of Collapsibles where only one can be open at a time, by combining them with custom logic on the open/close events.
* Property Inspectors - Display object properties that can be shown or hidden.
* Information Hiding - Presenting advanced options or less frequently used details that don't need to be visible by default.
* Debug Overlays - Containing logs or diagnostic tools that can be toggled open.
* Notes
* Content Container
* Toggle Mechanism
* Layout Impact
* Initial State
</details>
<details><summary>ColorPicker</summary>
This is some information about the Widget class.
</details>
<details><summary>ComboBox</summary>
This is some information about the Widget class.
</details>
<details><summary>CommandLine</summary>
This is some information about the Widget class.
</details>
<details><summary>ContextMenu</summary>
This is some information about the Widget class.
</details>
<details><summary>DialogWindow</summary>
This is some information about the Widget class.
</details>
<details><summary>FileDialog</summary>
This is some information about the Widget class.
</details>
<details><summary>FpsGraph</summary>
This is some information about the Widget class.
</details>
<details><summary>Graph</summary>
This is some information about the Widget class.
</details>
<details><summary>GridLayout</summary>
This is some information about the Widget class.
</details>
<details><summary>Image</summary>
This is some information about the Widget class.
</details>
<details><summary>ImageButton</summary>
This is some information about the Widget class.
</details>
<details><summary>ImageRect</summary>
This is some information about the Widget class.
</details>
<details><summary>LabeledCheckbox</summary>
This is some information about the Widget class.
</details>
<details><summary>LabeledSlider</summary>
This is some information about the Widget class.
</details>
<details><summary>Link</summary>
This is some information about the Widget class.
</details>
<details><summary>ListBox</summary>
This is some information about the Widget class.
</details>
<details><summary>VerticalListLayout</summary>
This is some information about the Widget class.
</details>
<details><summary>HorizontalListLayout</summary>
This is some information about the Widget class.
</details>
<details><summary>NineSlice</summary>
This is some information about the Widget class.
</details>
<details><summary>ProgressBar</summary>
This is some information about the Widget class.
</details>
<details><summary>RadioButton</summary>
This is some information about the Widget class.
</details>
<details><summary>Rect</summary>
This is some information about the Widget class.
</details>
<details><summary>RichText</summary>
This is some information about the Widget class.
</details>
<details><summary>ScrollingRect</summary>
This is some information about the Widget class.
</details>
<details><summary>Separator</summary>
This is some information about the Widget class.
</details>
<details><summary>Slider</summary>
* The `Slider` Widget allows users to select a value from a continuous or discrete range by moving a **scrubber** (or thumb, dragger, etc.) along a **track**.
* `Slider` is a specialization of the `Rect` widget.
* Usages:
* Volume Control
* Brightess/Contrast
* Color Picker (JUI actually has [one](ColorPicker) built-in that uses `Sliders`!)
* Progress Indicator (Just kidding, use a `ProgressBar` for this!);
* Game Settings
* Field-of-view.
* Sensitivity
* Spell power, physics parameters.
* Numerical Input (Range-Bound): An alternative to a text input field when the allowed values are within a known, limited range.
* Notes:
* Orientation: ===Currently, JUI does not **yet** have vertical sliders.===
* Range and Step: You can define a minimum and maximum value for the slider;
* As well as a "step" quantity, forcing the slider to snap to discrete increments (ie. multiples of 5, or increments of 0.25)
* Event-Driven - The "value changed" event fires when the user moves the scrubber or the value is set programmatically.
```cpp
auto* sfx_volume_slider = new JUI::Slider(my_scene);
sfx_volume_slider->Minimum(0);
sfx_volume_slider->Maximum(100);
sfx_volume_slider->Interval(1);
sfx_volume_slider->CurrentValue(50);
sfx_volume_slider->ValueChanged += [](float value) {
// set audio engine SFX volume.
};
```
* Also see `LabeledSlider` for a slider with text.
</details>
<details><summary>Table</summary>
This is some information about the Widget class.
</details>
<details><summary>TabView</summary>
This is some information about the Widget class.
</details>
<details><summary>Text</summary>
This is some information about the Widget class.
</details>
<details><summary>TextButton</summary>
This is some information about the Widget class.
</details>
<details><summary>TextInputForm</summary>
This is some information about the Widget class.
</details>
<details><summary>TextRect</summary>
This is some information about the Widget class.
</details>
<details><summary>ToggleButton</summary>
This is some information about the Widget class.
</details>
<details><summary>Tooltip</summary>
This is some information about the Widget class.
</details>
<details><summary>UtilityBar</summary>
This is some information about the Widget class.
</details>
<details><summary>Viewport</summary>
This is some information about the Widget class.
</details>
<details><summary>Window</summary>
This is some information about the Widget class.
</details>

View File

@@ -18,6 +18,8 @@
#include "JGL/types/RenderTarget.h"
#include <JUI/DefaultStyle.hpp>
#include "JGL/JGL.h"
namespace JUI
{
enum class BorderMode
@@ -27,6 +29,10 @@ namespace JUI
Inset /// As BorderWidth increases, the border grows inward only. The dimensions of the widget's contents are reduced at a 1:2 ratio.
};
enum class CornerRoundingMode {
None, Rounded, Chamfer,
};
/// Base implementation for rendering rectangles with decorations.
class RectBase {
public:
@@ -40,6 +46,7 @@ namespace JUI
virtual void CornerRounding(float radius);
[[nodiscard]] float CornerRounding() const;
enum CornerRoundingMode CornerRoundingMode() const;
// TODO: Implement per-corner rounding in JGL::Outline/FillRect
//void CornerRounding(float tlRadius, float trRadius, float blRadius, float brRadius);
@@ -67,9 +74,29 @@ namespace JUI
void Draw(const Vector2& pos, const Vector2& size);
void Draw(const Color4& bgColor, const Color4& fgColor, const Vector2& pos, const Vector2& size);
/// Core routine for drawing outline rects.
static void DrawOutline(const Color4& color,
const Vector2& pos, const Vector2& size,
float rounding, float borderWidth,
enum BorderMode borderMode, enum CornerRoundingMode cornerRoundingMode);
/// Core routine for drawing background rects.
static void DrawBG(const Color4& color,
const Vector2& pos, const Vector2& size,
float rounding,
enum CornerRoundingMode rounding_mode);
/// Core routine for drawing rect boxes, with background and outline.
static void Draw(const Color4& bgColor, const Color4& fgColor,
const Vector2& pos, const Vector2& size,
float rounding, float borderWidth,
enum BorderMode borderMode, enum CornerRoundingMode rounding_mode);
protected:
enum BorderMode border_mode = BorderMode::Middle;
enum CornerRoundingMode corner_mode = CornerRoundingMode::Rounded;
bool mouse_press_debounce{};
bool mouse_inside_debounce{};
float border_width = Style::BorderLineWidth;

View File

@@ -11,6 +11,16 @@
#pragma once
/// Ideal refactor:
/// * Completely separate Component for a given behavior / area of concern
/// * XYZStyler class, stored as a pointer in the widget.
/// * Smart pointers
/// * Intelligent layout.
/// *
#include <Event.h>
#include <string>
#include <vector>
@@ -31,17 +41,43 @@ namespace JUI {
using namespace J3ML::Math;
using namespace J3ML::LinearAlgebra;
struct PaddedElementStyle {
};
struct EventArgs {
};
struct FocusedArgs {};
struct UnfocusedEventArgs {};
struct ChildAddedEventArgs {};
struct ChildRemovedEventArgs {};
/// TODO: Refactor the member functions providing the parent-child hierarchy into the Node mixin.
template <class TNode>
class Node {
public:
protected:
std::vector<TNode*> children;
TNode* parent;
};
/// Widget is the base class for all JUI elements and represents any object that can be in the tree hierarchy.
/// Some widgets are expressly used for layout, and therefore do not strictly speaking 'Appear' on screen.
/// Some widgets are expressly used for layout, and therefore do not strictly-speaking 'Appear' on screen.
/// Widgets exist in a tree hierarchy where each object has one parent, and an arbitrary number of children. No circular references are allowed.
class Widget {
public:
/// The default constructor for Widget. Generally, JUI widgets will initialize member data,
/// and apply a default style in this constructor. Be aware of this, as this is not "idiomatic C++".
Widget();
/// Construct a new widget by specifying it's parent,
/// Construct a new widget by specifying its parent,
explicit Widget(Widget* parent);
virtual ~Widget() {}
virtual ~Widget() = default;
public:
#pragma region Events
/// An event that triggers when the widget is in-focus, generally when the mouse is hovering it.
@@ -93,6 +129,8 @@ namespace JUI {
[[nodiscard]] virtual Vector2 GetAbsolutePosition() const;
[[nodiscard]] virtual float GetAbsoluteRotation() const;
[[nodiscard]] virtual Vector2 GetAbsoluteCentroid() const;
/// Returns the parent of this widget, or a nullptr if there is no parent.
/// @see GetFamilyTreeRoot().
Widget* GetParent() const;
@@ -111,6 +149,8 @@ namespace JUI {
/// In a well-formed JUI menu, this **should** always be a Scene.
Widget* GetFamilyTreeRoot() const;
#pragma endregion
#pragma region Layout
@@ -391,6 +431,8 @@ namespace JUI {
int layout_order = 0;
Vector2 viewport_size{0,0};
bool is_mouse_inside = false;
/// Returns the amount of pixels this widget will be padded by from the top-left.
/// Generally, the widget will be shrunk and moved over by this amount, relative to the parent.
Vector2 GetAbsolutePaddingTopLeft() const;
@@ -401,5 +443,8 @@ namespace JUI {
Vector2 GetAbsoluteMarginTopLeft();
Vector2 GetAbsoluteMarginBottomRight();
void UpdateTweens(float elapsed);
/// Calculate if mouse is inside this element at the start of it's update frame.
bool ComputeIsMouseInside() const;
};
}

View File

@@ -14,10 +14,27 @@
#include <J3ML/LinearAlgebra/Vector2.hpp>
#include <JUI/Base/Widget.hpp>
#include "Hoverable.hpp"
namespace JUI {
struct ClickEventArgs {
public:
protected:
private:
};
struct ReleaseEventArgs {
public:
protected:
private:
};
/// A mixin helper class that provides behavior and events for clickable widgets.
class Clickable {
public:
Event<std::optional<ClickEventArgs>> Clicked;
Event<ReleaseEventArgs> Released;
Event<Vector2, MouseButton> OnClickEvent;
Event<Vector2, MouseButton, bool> OnReleaseEvent;
@@ -34,4 +51,4 @@ namespace JUI {
bool clicked = false;
bool click_debounce = false;
};
}
}

View File

@@ -0,0 +1,62 @@
/// Josh's User Interface Library
/// A C++20 Library for creating, styling, and rendering of a UI/UX widgets.
/// Developed and Maintained by Josh O'Leary @ Redacted Software.
/// Special Thanks to William Tomasine II and Maxine Hayes.
/// (c) 2024 Redacted Software
/// This work is dedicated to the public domain.
/// @file Resizable.hpp
/// @desc Resizable Mixin Helper class
/// @edit 2024-10-11
#pragma once
#include <Event.h>
#include <J3ML/LinearAlgebra/Vector2.hpp>
namespace JUI
{
/// A mixin helper class that enables a widget to be resized by the mouse.
class Focusable {
public:
Event<> OnGrabFocus;
Event<> OnDropFocus;
[[nodiscard]] bool Focused() const;
[[nodiscard]] bool IsFocused() const { return focused;}
[[nodiscard]] bool HasFocus() const { return focused;}
virtual void GrabFocus() { SetFocused(true);}
virtual void DropFocus(const Vector2& point) {
if (do_focus_cooldown_hack && focus_hack_cooldown > time_focused)
return;
SetFocused(false);
}
virtual void SetFocused(bool value) {
focused = value;
if (do_focus_cooldown_hack && value) time_focused = 0;
}
virtual void Update(float delta) {
if (do_focus_cooldown_hack)
time_focused += delta;
}
void DoFocusHackCooldown(bool value) { do_focus_cooldown_hack = value; }
bool DoFocusHackCooldown() const { return do_focus_cooldown_hack; }
float FocusHackCooldown() const { return focus_hack_cooldown; }
void FocusHackCooldown(float value) {
focus_hack_cooldown = value;
}
protected:
bool focused = false;
float time_focused = 0;
bool do_focus_cooldown_hack = false;
float focus_hack_cooldown = 0.1f;
};
}

View File

@@ -18,6 +18,14 @@
namespace JUI
{
/// Interface for anything that can report mouse presence.
class IMouseAware {
public:
virtual ~IMouseAware() = default;
virtual bool IsMouseAware() const = 0;
};
/// A mixin helper class that provides behavior for hoverable objects.
/// A hoverable object pays attention to when the mouse enters and leaves it's bounds.
class Hoverable {
@@ -39,7 +47,10 @@ namespace JUI
virtual void OnExitChildren(const Vector2& mouse);
virtual void OnExitDescendants(const Vector2& mouse);
void Update(const Vector2& m_pos, float delta);
void Update(bool mouse_inside, float delta);
/// @note This member function **must** be implemented for classes that implement Hoverable.
//[[nodiscard]] virtual bool IsMouseInside() const = 0;
//virtual void SetTooltip(const std::string& content, float delay) {}
protected:
//Tooltip* tooltip = nullptr;

View File

@@ -0,0 +1,14 @@
#pragma once
namespace JUI {
struct ButtonStyle {
RectStyle base_style;
RectStyle hovered_style;
RectStyle pressed_style;
RectStyle disabled_style;
};
class ButtonStyler {
};
}

View File

@@ -0,0 +1,15 @@
#pragma once
namespace JUI {
struct CheckboxStyle {
RectStyle unchecked_style;
RectStyle checked_color;
RectStyle disabled_style;
};
class CheckboxStyler {
public:
protected:
private:
};
}

View File

@@ -0,0 +1,45 @@
#pragma once
#include <Color4.hpp>
#include <Colors.hpp>
namespace JUI {
struct RectStyle {
float border_width = 1.f;
Color4 border_color {192, 192, 192};
Color4 bg_color {64, 64, 64};
float corner_rounding = 0.f;
enum BorderMode border_mode;
enum CornerRoundingMode corner_mode;
};
class RectStyler {
public:
#pragma region Getters
virtual Color4 BGColor() const = 0;
virtual Color4 BorderColor() const = 0;
virtual enum BorderMode BorderMode() const = 0;
virtual float BorderWidth() const = 0;
virtual float CornerRounding() const = 0;
#pragma endregion
#pragma region Setters
virtual void BGColor(const Color4& color) = 0;
virtual void BorderColor(const Color4& color) = 0;
virtual void BorderMode(const enum BorderMode& borderMode) = 0;
virtual void BorderWidth(float width) = 0;
#pragma endregion
void Style(const RectStyle& value) {
style = value;
}
protected:
float border_width = 1.f;
Color4 border_color {192, 192, 192};
enum BorderMode border_mode;
float corner_rounding = 0.f;
RectStyle style;
private:
};
}

View File

@@ -26,6 +26,9 @@ namespace JUI
UDim() : Pixels(0), Scale(0.f) {}
UDim(int pixels, float scale) : Pixels(pixels), Scale(scale) {}
UDim(const UDim& u) = default;
static UDim FromPixels(int pixels);
static UDim FromScale(float scale);
public:
UDim operator + (const UDim& rhs) const;

View File

@@ -17,7 +17,6 @@
namespace JUI
{
using namespace J3ML::LinearAlgebra;
/// Funky Coordinate system purpose-built for screen positioning
@@ -26,6 +25,17 @@ namespace JUI
/// @see UDim.hpp
class UDim2
{
public: // Constants
static const UDim2 Center;
static const UDim2 TopLeft;
static const UDim2 BottomRight;
static const UDim2 BottomLeft;
static const UDim2 TopRight;
static const UDim2 TopCenter;
static const UDim2 LeftCenter;
static const UDim2 BottomCenter;
static const UDim2 RightCenter;
public: // Properties
UDim X;
UDim Y;

View File

@@ -0,0 +1,94 @@
#pragma once
#include <JUI/Base/Widget.hpp>
namespace JUI {
/// An invisible, point-like widget which is used to attach decorator-type widgets at a given point.
/// For example: The CurveSegment Widget uses two Anchors for it's endpoints.
class Anchor : public Widget {
public:
Anchor() : Widget() {
}
explicit Anchor(Widget* parent) : Anchor() {
Parent(parent);
};
explicit Anchor(Widget* parent, const UDim2& position) : Anchor(parent) {
Position(position);
Size({0_px, 0_px});
}
void Update(float elapsed) override {
}
void Draw() override {
Color4 debug_color = Colors::Red;
float debug_size = 1;
J2D::FillCircle(debug_color, GetAbsolutePosition(), debug_size, 5);
}
Vector2 Point() { return GetAbsolutePosition(); }
protected:
private:
};
class Decorator : public Widget {
public:
Decorator() : Widget(){
}
explicit Decorator(Widget* parent) : Decorator() {
Parent(parent);
}
};
class ArrowDecorator : public Decorator {
public:
explicit ArrowDecorator(Widget* parent, Anchor* a, Anchor* b) : Decorator() {
Parent(parent);
origin = a;
goal = b;
}
void Draw() override {
auto goal_point = goal->Point();
auto origin_point = origin->Point();
using namespace J3ML;
J2D::DrawLine(color, origin->Point(), goal->Point(), line_width);
Vector2 direction = ( origin->Point() - goal->Point()).Normalized();
Vector2 triangle_base_pt = goal->Point() + (direction*arrowhead_size);
//J2D::DrawPoint(Colors::Yellow, triangle_base_pt, 4);
Vector2 tri_left = triangle_base_pt + (direction.Rotated90CW() *arrowhead_size);
Vector2 tri_right = triangle_base_pt + (direction.Rotated90CCW()*arrowhead_size);
Vector2 tri_top = goal->Point();
J2D::DrawLine(color, tri_top, tri_left, arrowhead_size);
J2D::DrawLine(color, tri_top, tri_right, arrowhead_size);
}
protected:
float arrowhead_size;
float line_width;
Color4 color;
Anchor* origin;
Anchor* goal;
private:
};
}

View File

@@ -32,6 +32,7 @@ namespace JUI {
protected:
bool checked = false;
Color4 check_color = Style::Checkbox::CheckmarkColor;
Color4 uncheck_color = Style::BackgroundColor;
};
class Checkbox : public Button, public CheckboxBase {

View File

@@ -8,152 +8,53 @@
#include "../Style/TextStyler.hpp"
#include <JUI/Widgets/Slider.hpp>
namespace JUI {
#include "Window.hpp"
std::string rgb2hex(int r, int g, int b, bool with_head)
{
std::stringstream ss;
if (with_head)
ss << "#";
ss << std::hex << (r << 16 | g << 8 | b );
return ss.str();
}
/// TODO: Move to a utility library or something...
/// Converts a set of R,G,B values to it's hexadecimal string representation.
std::string rgb2hex(int r, int g, int b, bool with_head);
namespace JUI {
/// A JUI Widget that displays a HSV color input dialog.
class ColorPicker : public Rect, public TextStyler {
public:
ColorPicker();
void Font(const JGL::Font &value) override {
TextStyler::Font(value);
hue_label->Font(value);
sat_label->Font(value);
bri_label->Font(value);
hex_label->Font(value);
}
void TextSize(int size) override {
TextStyler::TextSize(size);
hue_label->TextSize(size);
sat_label->TextSize(size);
bri_label->TextSize(size);
hex_label->TextSize(size);
}
void TextColor(const Color4 &color) override {
TextStyler::TextColor(color);
hue_label->TextColor(color);
sat_label->TextColor(color);
bri_label->TextColor(color);
hex_label->TextColor(color);
}
explicit ColorPicker(Widget* parent);
/// Invoked when the color value is changed by the user.
/// @param Color4 The new color value.
Event<Color4> OnColorValueChanged;
/// Sets the font of the text elements contained in this widget.
void Font(const JGL::Font &value) override;
/// Sets the point size of the text elements contained in this widget.
void TextSize(int size) override;
/// Sets the point size of the text elements contained in this widget.
void TextColor(const Color4 &color) override;
void SetColorValue(const Color4& color);
Color4 GetColorValue() const;
float hue = 0;
float sat = 1.f;
float bri = 1.f; // AKA val
ColorPicker() : Rect() {
Name("ColorPicker");
Size({100_percent, 100_percent});
auto* row_layout = new JUI::VerticalListLayout(this);
row_layout->Padding(5_px);
hue_slider = new JUI::Slider(row_layout);
hue_slider->Size({100_percent, 28_px});
hue_slider->Minimum(0.f); hue_slider->Maximum(1.f);
hue_slider->Interval(1e-3f);
hue_slider->ValueChanged += [this] (float val) mutable {
hue_label->Content(std::format("Hue: {}", Math::FloorInt(val*360)));
hue = val * 360;
hue_slider->ScrubberColor(Color4::FromHSV(hue, 1.f, 1.f));
OnColorValueChanged.Invoke(Color4::FromHSV(hue, sat, bri));
float Hue() const { return hue;}
float Sat() const { return sat;}
float Val() const { return bri;}
Color4 computed = Color4::FromHSV(hue, sat, bri);
hex_label->TextColor(computed);
hex_label->Content(std::format("{}", rgb2hex(computed.r, computed.g, computed.b, true)));
};
hue_label = new JUI::Text(hue_slider);
hue_label->AlignCenterHorizontally();
hue_label->AlignTop();
hue_label->TextColor(Colors::Black);
hue_label->Content("Hue: 0");
sat_slider = new JUI::Slider(row_layout);
sat_slider->Size({100_percent, 28_px});
sat_slider->Minimum(0); sat_slider->Maximum(1);
sat_slider->Interval(1e-3f);
sat_slider->ValueChanged += [this] (float val) mutable {
sat_label->Content(std::format("Saturation: {}%", Math::Floor(val*100.f)));
sat = val;
OnColorValueChanged.Invoke(Color4::FromHSV(hue, sat, bri));
Color4 computed = Color4::FromHSV(hue, sat, bri);
hex_label->TextColor(computed);
hex_label->Content(std::format("{}", rgb2hex(computed.r, computed.g, computed.b, true)));
};
sat_label = new JUI::Text(sat_slider);
sat_label->AlignCenterHorizontally();
sat_label->AlignTop();
sat_label->TextColor(Colors::Black);
sat_label->Content("Saturation: 100%");
bri_slider = new JUI::Slider(row_layout);
bri_slider->Size({100_percent, 28_px});
bri_slider->Minimum(0); bri_slider->Maximum(1);
bri_slider->Interval(1e-3f);
bri_slider->ValueChanged += [this] (float val) mutable {
bri_label->Content(std::format("Brightness: {}%", Math::Floor(val*100.f)));
bri = val;
OnColorValueChanged.Invoke(Color4::FromHSV(hue, sat, bri));
Color4 computed = Color4::FromHSV(hue, sat, bri);
hex_label->TextColor(computed);
hex_label->Content(std::format("{}", rgb2hex(computed.r, computed.g, computed.b, true)));
};
bri_label = new JUI::Text(bri_slider);
bri_label->AlignCenterHorizontally();
bri_label->AlignTop();
bri_label->TextColor(Colors::Black);
bri_label->Content("Brightness: 100%");
//auto* hue_box = new JUI::Rect();
hex_label = new JUI::TextRect(row_layout);
hex_label->AlignCenterHorizontally();
hex_label->Size({100_percent, 28_px});
hex_label->Content("#FFZZGG");
}
explicit ColorPicker(Widget* parent) : ColorPicker() {
Parent(parent);
}
public: /// Subcomponents.
/*JUI::Text* HueLabel() { return hue_label; }
JUI::Text* SatLabel() { return sat_label; }
JUI::Text* BriLabel() { return bri_label; }
JUI::Slider* HueSlider() { return hue_slider; }
JUI::Slider* SatSlider() { return sat_slider; }
JUI::Slider* BriSlider() { return bri_slider; }
JUI::TextRect* HexLabel() { return hex_label; }*/
protected:
/// Calculates the colors of the label, and the inverse color for the text.
void RecomputeVisuals();
/// Applies this widget's specific styling to it's slider sub-components.
void StyleSlider(Slider* slider);
JUI::Slider* hue_slider = nullptr;
JUI::Slider* sat_slider = nullptr;
JUI::Slider* bri_slider = nullptr;
@@ -161,7 +62,18 @@ namespace JUI {
JUI::Text* sat_label = nullptr;
JUI::Text* bri_label = nullptr;
JUI::TextRect* hex_label = nullptr;
float hue = 0;
float sat = 1.f;
float bri = 1.f; // AKA val
private:
};
class ColorPickerDialog : public Window {
public:
protected:
private:
};
}

View File

@@ -24,10 +24,6 @@ namespace JUI {
/// This event is invoked when the user has sent a message to the command line.
Event<std::string> OnInput;
/// This event is invoked when the CommandLine window is opened.
Event<> OnOpen;
/// This event is invoked when the CommandLine window is closed.
Event<> OnClose;
/// The default constructor initializes the layout and events of child elements.
/// This constructor is a delegate called by the explicit constructor with parent parameter.
@@ -42,18 +38,8 @@ namespace JUI {
/// @param color
void Log(const std::string& message, const Color4& color = Colors::White);
/// @return True if the window widget is currently open and visible. Input will be ignored if not.
[[nodiscard]] bool Opened() const;
/// @return True if the window widget is currently closed and invisible. Input will be ignored if so.
[[nodiscard]] bool Closed() const;
/// Opens the window widget.
void Open(bool openVal = true);
/// Closes the window widget.
void Close(bool openVal = false);
/// Sets the "open"-ness of the window widget. The widget will be invisible and ignore input if it is not open.
void SetOpen(bool openVal);
void SetOpen(bool openVal) override;
/// Passes input events down the widget hierarchy.
/// @return True if this widget will "consume" the input.
@@ -70,7 +56,6 @@ namespace JUI {
int index = 0;
int input_form_height = 20;
int message_height = 16;
bool open = false;
std::vector<std::string> msg_history;
private:
};

View File

@@ -32,19 +32,7 @@ namespace JUI {
virtual Separator* AddSeparator(const UDim& size = 5_px) = 0;
};
class RectStyleInterface {
public:
virtual Color4 BGColor() const = 0;
virtual Color4 BorderColor() const = 0;
virtual enum BorderMode BorderMode() const = 0;
virtual float BorderWidth() const = 0;
virtual float CornerRounding() const = 0;
virtual void BGColor(const Color4& color) = 0;
virtual void BorderColor(const Color4& color) = 0;
virtual void BorderMode(const enum BorderMode& borderMode) = 0;
};

View File

@@ -7,6 +7,7 @@
/// @file FileDialog.hpp
/// @desc A dialog window that shows a selection of files on disk, and a form for naming, used for file loading and saving.
/// @edit 2024-5-29
#pragma once
@@ -17,30 +18,127 @@
namespace JUI
{
class FileListing : public JUI::Widget { };
// TODO: Implement SortBy enumeration - Date, Size, Name, Type
class FileDialogWindow : public JUI::Window
{
public:
Event<> UserCompleted;
Event<> UserConfirmed;
Event<> UserCancelled;
Event<std::string> FileSelected;
FileDialogWindow() : Window() {
auto* toolbar = new JUI::UtilityBar(this->Content());
Title("File Dialog");
//auto* toolbar = new JUI::UtilityBar(this->Content());
auto* file_listing = new JUI::ScrollingRect(this->Content());
file_listing->Size({100_percent, 100_percent-20_px});
file_layout = new JUI::VerticalListLayout(file_listing);
}
/// Tree system
std::string format_perms(const std::filesystem::perms& p) const {
using std::filesystem::perms;
auto parse = [=](char op, perms perm) {
return (perms::none == (perm & p) ? '-' : op);
};
std::string perm_string = "";
perm_string += parse('r', perms::owner_read);
perm_string += parse('w', perms::owner_write);
perm_string += parse('x', perms::owner_exec);
perm_string += parse('r', perms::group_read);
perm_string += parse('w', perms::group_write);
perm_string += parse('x', perms::group_exec);
perm_string += parse('r', perms::others_read);
perm_string += parse('w', perms::others_write);
perm_string += parse('x', perms::others_exec);
return perm_string;
}
std::string format_size(uintmax_t size) const {
bool addSpace = false;
int precision = 2;
std::vector<std::string> units = {"B", "KB", "GB", "TB", "PB", "EB", "ZB", "YB"};
if (Math::Abs(size) < 1) return std::to_string(size) + (addSpace ? " " : "") + units[0];
auto exponent = Math::Min<uintmax_t>(Math::Floor(Math::Log10(size < 0 ? -size : size) / 3), units.size()-1);
auto n = (size < 0 ? -size : size) / std::pow(1000, exponent);
return (size < 0 ? "-" : "") + std::format("{}", n) + (addSpace ? " ": "") + units[exponent];
}
JUI::Rect* CreateFileEntry(const std::filesystem::directory_entry& entry) {
auto* rect = new JUI::TextButton(file_layout);
rect->LayoutOrder(index);
rect->Size({100_percent, 20_px});
std::string entry_type = entry.is_directory() ? "directory" : "file";
std::string perms = format_perms(entry.status().permissions());
auto file_size = (entry.is_directory() ? 0 : entry.file_size());
auto file_size_str = format_size(file_size);
std::filesystem::file_time_type timestamp = (entry.is_directory() ? std::filesystem::file_time_type{} : entry.last_write_time());
std::string label = std::format("{} {} {} {} {}", entry.path().filename().string(), file_size_str, entry_type, perms, timestamp);
rect->Content(label);
rect->BaseBGColor(Colors::Gray);
index++;
rect->Clicked += [this, entry] (auto ev) mutable {
FileSelected(entry.path().string());
};
return rect;
}
FileDialogWindow(const std::filesystem::path& root) : FileDialogWindow() {
std::cout << root.relative_path() << std::endl;
std::cout << root.root_directory() << std::endl;
std::cout << root.filename() << std::endl;
std::cout << root.parent_path() << std::endl;
std::cout << root.string() << std::endl;
std::cout << std::filesystem::current_path() << std::endl;
Title(std::format("'{}' in '{}' - File Dialog", root.relative_path().string(), std::filesystem::current_path().string()));
for (const auto& entry: std::filesystem::directory_iterator(root)) {
CreateFileEntry(entry);
}
}
FileDialogWindow(const std::filesystem::path& root) : FileDialogWindow() {}
explicit FileDialogWindow(Widget* parent, const std::filesystem::path& root) : FileDialogWindow(root)
{
Parent(parent);
}
void SetSelectedFile(const std::string& filename);
void SetConfirmOverwrite(bool confirm);
void SetBlockingWhileOpen(bool blocking);
bool GetBlockingWhileOpen() const;
protected:
int directories_indexed = 0;
int index = 0;
VerticalListLayout* file_layout;
std::vector<Rect*> file_entry_widgets;
private:
};

View File

@@ -1,114 +1,119 @@
#pragma once
#include "Rect.hpp"
#include "ImageRect.hpp"
#include <JUI/Widgets/Rect.hpp>
#include <JUI/Widgets/ImageRect.hpp>
#include <JUI/Widgets/FpsGraph.hpp>
#include "Window.hpp"
namespace JUI {
///
struct DataPoint {
Vector2 pos;
Color4 color;
};
///
class FpsGraph : public JUI::ImageRect
{
public:
void RenderDataPoints() {
J2D::Begin(canvas, nullptr, true);
/// Plots all stored data-points onto the render target.
void RenderDataPoints();
/// The default constructor initializes the basic layout of the FpsGraph class. Member variables are also zero-initialized.
FpsGraph();
DataPoint prev = data[0];
for (auto& data_pt : data) {
/// Constructs an FpsGraph by explicitly specifying its parent element.
explicit FpsGraph(Widget* parent);
//J2D::DrawGradientLine(prev.color, data_pt.color, prev.pos, data_pt.pos, 1);
J2D::DrawLine(data_pt.color, data_pt.pos, {data_pt.pos.x, graph_height }, 1);
/// Inserts a new data point into the graph's data set.
void Plot(const Vector2& pt, const Color4& col);
void Plot(const DataPoint& data_pt);
prev = data_pt;
}
float target_line_height = (1.f / 60.f) * y_axis_scale;
J2D::DrawString(Colors::Black, "60 FPS / 16.7ms (Target)", 0, graph_height - target_line_height, 10, 1.f);
J2D::DrawLine(Colors::White, {0, graph_height - target_line_height}, {GetAbsoluteSize().x, graph_height - target_line_height}, 1);
J2D::End();
}
/// Performs update logic on the state and appearance of this graph widget.
void Update(float delta) override;
FpsGraph() : JUI::ImageRect() {
for (int i = 0; i < sample_history; i++) {
Plot({i / 1.f, 0.f}, Colors::Black);
}
/// A builtin preset for style and layout that appears docked at the bottom of the screen. It effectively behaves as an overlaid frame-graph.
/// TODO: Better name.
void SetupAsPseudoDockedElementAtBottomOfScreen();
canvas = new JGL::RenderTarget(Vector2i(graph_width, graph_height));
void ShowTargetFPS(bool show);
RenderDataPoints();
bool ShowTargetFPS() const;
Content(canvas);
};
explicit FpsGraph(Widget* parent) : FpsGraph() {
Parent(parent);
}
void Plot(const Vector2& pt, const Color4& col) {
data.push_back(DataPoint{ pt, col });
}
void TargetFPS(float fps);
void Update(float delta) override {
ImageRect::Update(delta);
data.erase(data.begin());
for (auto& data_pt : data) {
data_pt.pos.x -= 1;
}
Color4 color = Colors::Green;
if (delta > 1.f/120.f)
color = Colors::Blue;
if (delta > 1.f/60.f)
color = Colors::Yellow;
if (delta > 1.f/30.f)
color = Colors::Red;
if (delta > 1.f/15.f)
color = Colors::Purples::Magenta;
if (delta > 1.f/5.f)
color = Colors::Black;
Plot({(data.size()-1.f), graph_height - (delta*y_axis_scale)}, color);
RenderDataPoints();
Content(canvas);
}
float TargetFPS() const;
std::vector<DataPoint> data;
JGL::RenderTarget* canvas;
/// If the current FPS is lower than this value, use high_fps_color.
float HighFPSRange() const;
/// If the current FPS is lower than this value, and higher than SubparFPSRange(), use target_fps_color.
float TargetFPSRange() const;
/// If the current FPS is lower than this value, and higher than LowFPSRange(), use subpar_fps_color.
float SubparFPSRange() const;
/// If the current FPS is lower than this value, and higher than UnplayableFPSRange()
float LowFPSRange() const;
float UnplayableFPSRange() const;
/// Any FPS lower than this value will use slideshow_fps_color.
float SlideshowFPSRange() const;
protected:
float graph_height = 50;
float graph_width = 500;
float sample_history = 500;
float y_axis_scale = 600;
float target_fps = 60.f;
bool show_target_fps_bar = true;
Color4 target_fps_bar_color = Colors::White;
Color4 target_fps_label_color = Colors::Black;
protected:
Color4 high_fps_color = Colors::Blue;
Color4 target_fps_color = Colors::Green;
Color4 subpar_fps_color = Colors::Yellow;
Color4 low_fps_color = Colors::Red;
Color4 unplayable_fps_color = Colors::Purples::Magenta;
Color4 slideshow_fps_color = Colors::Black;
float high_fps_range_factor = 2.f;
float target_fps_range_factor = 1.f;
float subpar_fps_range_factor = 0.5f;
float low_fps_range_factor = 0.25f;
float unplayable_fps_range_factor = 0.125f;
float slideshow_fps_range_factor = 0.0625f;
private:
};
class FpsGraphWindow : public JUI::Window
class FpsGraphWindow : public Window
{
public:
FpsGraphWindow() : Window()
{
Name("FPSGraphWindow");
Title("FPS Graph");
data_graph = new FpsGraph(this->Content());
FpsGraphWindow();
explicit FpsGraphWindow(Widget* parent);
FpsGraph* GraphWidget() {return data_graph; }
void DockGraph() {
this->Close();
// TODO:: unsafe.
data_graph->Parent(this->GetParent());
data_graph->SetupAsPseudoDockedElementAtBottomOfScreen();
}
explicit FpsGraphWindow(Widget* parent) : FpsGraphWindow()
{
this->Parent(parent);
void UndockGraph() {
this->Open();
data_graph->Parent(this->Content());
}
protected:
FpsGraph* data_graph;
private:
};
}
}

View File

@@ -2,8 +2,35 @@
#include "Rect.hpp"
#include "ImageRect.hpp"
class Graph : public JUI::ImageRect {
public:
protected:
private:
};
namespace JUI {
/// A widget that plots and displays a data set in the form of various graphs.
class Graph : public ImageRect {
public:
/// The default constructor sets sensible defaults for style properties and zero-initializes other members.
Graph()
{
}
/// Constructs a Graph by explicitly specifying its parent element.
explicit Graph(Widget* parent) : Graph() {
this->Parent(parent);
}
protected:
private:
};
class PieChart : public Graph {
public:
protected:
private:
};
class LineGraph : public Graph {
public:
protected:
private:
};
}

View File

@@ -0,0 +1,19 @@
#include <JUI/Widgets/Slider.hpp>
#include <JUI/Base/TextBase.hpp>
namespace JUI {
/// A combination widget of a Slider and Text element, which is drawn over the slider elements.
class LabeledSlider : public Slider, public TextBase {
public:
LabeledSlider();
explicit LabeledSlider(Widget* parent);
void Draw() override;
void Update(float elapsed) override;
protected:
private:
};
}

View File

@@ -0,0 +1,113 @@
/// Josh's User Interface Library
/// A C++20 Library for creating, styling, and rendering of a UI/UX widgets.
/// Developed and Maintained by Josh O'Leary @ Redacted Software.
/// Special Thanks to William Tomasine II and Maxine Hayes.
/// (c) 2025 Redacted Software
/// This work is dedicated to the public domain.
/// @file Link.hpp
/// @desc Special category of text-objects that model a clickable link in HTML.
/// @edit 2025-06-11
#pragma once
#include <JUI/Widgets/Text.hpp>
#include "JUI/Mixins/Clickable.hpp"
namespace JUI {
struct LinkInvokedEventArgs {};
/// Controls when Link widgets fire their Cicked callback. It can fire right when the widget is clicked, or when it is is released.
enum class LinkClickMode { Press, Release };
/// The Link widget is a specialization of the Text widget that has the appearance and behavior of a clickable link, such as in HTML.
class Link : public Text, public Clickable, public Hoverable {
public:
enum LinkClickMode ClickMode = LinkClickMode::Release;
Event<std::optional<LinkInvokedEventArgs>> Invoked;
/// This event is fired when the user clicks/releases the link.
Link() : Text(), Clickable(), Hoverable() {}
explicit Link(const std::string& content) : Link() {
this->Content(content);
}
explicit Link(Widget* parent) : Link() {
this->Parent(parent);
}
void OnHover(const Vector2 &MousePos) override {
TextColor(Colors::Blues::CornflowerBlue);
}
void OnExit(const Vector2 &MousePos) override {
TextColor(Colors::Blues::DeepSkyBlue);
}
void OnClick(const Vector2& mouse_pos, const MouseButton& btn) override {
if (disabled)
return;
Clickable::OnClick(mouse_pos, btn);
TextColor(Colors::White);
if (!fire_on_release) {
Clicked.Invoke(std::nullopt);
already_clicked = true;
}
}
void OnRelease(const J3ML::LinearAlgebra::Vector2 &mouse_pos, const JUI::MouseButton &btn,
bool still_hovering) override {
if (disabled)
return;
Clickable::OnRelease(mouse_pos, btn, still_hovering);
TextColor(Colors::Black);
if (fire_on_release) {
Clicked.Invoke(std::nullopt);
already_clicked = true;
}
}
void Update(float delta) override {
Text::Update(delta);
// TODO: Why does hovered not handle this?
Hoverable::Update(IsMouseInside(), delta);
// TODO: This is duplicated between here and Window.cpp
// Will be abstracted away into Clickable shortly.
// OMFG
if (IsHovered() && mb_state && !prev_mb_state)
{
OnClick(last_known_mouse_pos, mbtn);
}
if (IsClicked() && !mb_state)
{
OnRelease(last_known_mouse_pos, mbtn, IsHovered());
}
}
protected:
Color4 already_clicked_color;
Color4 hovered_color;
Color4 clicked_color;
bool disabled = false;
bool already_clicked = false;
bool fire_on_release;
private:
};
}

View File

@@ -5,20 +5,36 @@
namespace JUI {
/// The Separator Widget Class.
/// Fills space, and renders a single line through it, based on the given orientation.
/// Dashed, dotted, and solid lines of varying thickness are supported.
class Separator : public Widget {
public:
Separator();
explicit Separator(Widget* parent);
/// Constructs a Separator widget by specifying it's parent. Additional style parameters can also be specified.
/// @param parent
/// @param orientation
/// @param spacing
/// @param line_mode
/// @param line_color
/// @param line_thickness
Separator(Widget* parent,
const enum Orientation& orientation = Orientation::Horizontal,
const UDim& spacing = 5_px,
const enum LineFillMode& line_mode = LineFillMode::Solid,
const Color4& line_color = Colors::Black,
float line_thickness = 1.f
);
enum Orientation Orientation() const;
/// Sets whether the separator is laid out as a horizontal or vertical line.
/// Horizontal orientation draws a horizontal line, for separating elements in a vertical list.
/// Vertical orientation draws a vertical line, for separating elements in a horizontal list.
void Orient(const enum Orientation& orientation);
enum LineFillMode LineFillMode() const;
@@ -29,18 +45,43 @@ namespace JUI {
void LineColor(const Color4& color);
float GetAbsoluteSpacing() const;
void InnerDraw() override;
float Spacing() const;
//float Spacing() const;
void Spacing(float spacing);
//void Spacing(float spacing);
/// @return The size of the separator object.
UDim Spacing() const { return spacing;}
/// Sets the size of the separator object.
void Spacing(const UDim& value) {
spacing = value;
UpdateSize();
}
/// @returns The spacing between
float LineSpacing() const { return line_spacing;}
void LineSpacing(float value) {
line_spacing = value;
}
protected:
/// The default constructor zero-initializes all members.
Separator();
/// Sets the widgets size based on the spacing and orientation.
void UpdateSize();
enum LineFillMode line_mode = LineFillMode::Solid;
enum Orientation orientation = Orientation::Horizontal;
Color4 line_color = Colors::Black;
float line_thickness = 1.f;
float spacing = 2.f;
float line_spacing = 2.f;
UDim spacing = 2_px;
private:
};

View File

@@ -18,56 +18,88 @@
namespace JUI
{
template<typename T>
T roundMultiple( T value, T multiple )
{
if (multiple == 0) return value;
return static_cast<T>(std::round(static_cast<double>(value)/static_cast<double>(multiple))*static_cast<double>(multiple));
}
/// A slider is a widget with a handle which can be pulled back and forth to change the value.
class Slider : public Rect, public Clickable, public Hoverable
{
public:
/// Invoked when the value of the slider is changed, usually by the user interacting with it.
Event<float> ValueChanged;
/// The default constructor initializes member variables to reasonable defaults.
Slider();
/// Constructs a slider by specifying it's parent widget.
explicit Slider(JUI::Widget* parent);
/// @return The minimum value allowed by the slider.
[[nodiscard]] float Minimum() const;
/// @return The maximum value allowed by the slider.
[[nodiscard]] float Maximum() const;
/// @return The increments the slider moves in.
[[nodiscard]] float Interval() const;
/// Returns the current stored value
[[nodiscard]] float CurrentValue() const;
[[nodiscard]] Color4 ScrubberColor() const;
[[nodiscard]] float ScrubberWidth() const;
/// @note Deprecated in favor of Slider::ScrubberSize().
[[deprecated]] [[nodiscard]] float ScrubberWidth() const;
[[nodiscard]] UDim2 ScrubberSize() const;
[[nodiscard]] float ScrubberRounding() const;
/// Returns whether the slider is currently being dragged by the user.
[[nodiscard]] bool Dragging() const;
[[nodiscard]] float Range() const;
/// Sets the minimum value allowed by the slider.
void Minimum(float min);
void Maximum(float max);
void Interval(float inter);
void CurrentValue(float value);
/// @return The percentage of the slider, in the range [0, 1].
float Percentage() const;
/// Sets the percentage of the slider, in the range [0, 1]. The percentage is the underlying representation used interally.
void Percentage(float value);
void ScrubberColor(const Color4& color);
void ScrubberWidth(float width);
/// @note Deprecated in favor of Slider::ScrubberSize().
[[deprecated]] void ScrubberWidth(float width);
void ScrubberSize(const UDim2& size);
void ScrubberRounding(float rounding);
void SetDragging(bool value);
void OnClick(const J3ML::LinearAlgebra::Vector2 &MousePos, const JUI::MouseButton &MouseButton) override;
void OnRelease(const J3ML::LinearAlgebra::Vector2 &MousePos, const JUI::MouseButton &MouseButton, bool MouseStillOver) override;
void OnHover(const J3ML::LinearAlgebra::Vector2 &MousePos) override;
void OnExit(const J3ML::LinearAlgebra::Vector2 &MousePos) override;
void Update(float delta) override;
Vector2 GetScrubberAbsolutePosition() const;
Vector2 GetScrubberAbsoluteSize() const;
void Draw() override;
void InnerDraw() override;
protected:
float minimum = 0;
float maximum = 1;
float interval = 0.1;
float current;
float percentage = 0.f;
bool dragging = false;
float scrubber_width = 20;
UDim2 scrubber_size {20_px, 100_percent};
Color4 scrubber_color = Colors::White;
float scrubber_rounding = 0.f;
private:
};
class VerticalSlider {
public:
protected:
private:
};
}

View File

@@ -27,18 +27,32 @@ namespace JUI {
class TextInputForm : public TextRect, public Clickable {
public:
#pragma region Events
Event<> OnSelect;
Event<> OnDeselect;
Event<std::string> OnReturn;
#pragma endregion
public:
#pragma region Constructors
TextInputForm();
explicit TextInputForm(Widget* parent);
#pragma endregion
public:
#pragma region Methods
void Update(float elapsed) override;
void InnerDraw() override;
void Draw() override;
/// @return The number of characters the cursor is actually moved to the left by.
int MoveCursorLeft(int chars = 1);
/// @note The amount of
/// @return The number of characters the cursor is actually moved to the right by.
int MoveCursorRight(int chars = 1);
#pragma region Getters
void MoveCursorLeft();
#pragma endregion
#pragma region Setters
void MoveCursorRight();
#pragma endregion
/// Returns the maximum position of the cursor, which is determined by the input buffer's length.
[[nodiscard]] unsigned int CursorMaxPosition() const;
@@ -64,6 +78,11 @@ namespace JUI {
void PushKeyToCurrentPlaceInInputBuffer(const Key &key);
bool ObserveKeyInput(Key key, bool pressed) override;
void ShowNextHistory();
void ShowPrevHistory();
bool ObserveMouseInput(MouseButton btn, bool pressed) override;
bool ObserveMouseMovement(const Vector2 &latest_known_pos) override;
[[nodiscard]] std::string GetAutocompleteText() const;
@@ -103,22 +122,50 @@ namespace JUI {
void SetSelectionColor(const Color4& color);
bool HasSelection() const;
std::string GetSelectedText() const;
[[nodiscard]] bool KeepInputHistory() const;
void KeepInputHistory(bool value);
std::string InputHistory(int index) const;
#pragma endregion
protected:
#pragma region Properties
bool keep_input_history = false;
bool clear_text_on_return = true;
bool drop_focus_on_return = false;
bool focused = false;
bool selection_enabled;
bool selection_active;
int selection_start_index;
int selection_end_index;
unsigned int cursor_position = 0;
std::string input_buffer;
float cursor_blink_time = 0.f;
Color4 autocomplete_color = Style::InputForm::AutocompleteTextColor;
std::string autocomplete_text = "Hello World";
bool hide_autocomplete_on_select = true;
bool autocomplete_text_enabled = true;
std::set<std::string> blacklist;
bool selection_enabled;
#pragma endregion
#pragma region Working Variables
std::vector<std::string> history;
int history_index = -1;
bool selection_active;
int selection_start_index;
int selection_end_index;
unsigned int cursor_position = 0;
std::string input_buffer;
std::string saved_input_buffer;
/// Tracks the time (in seconds) since the TextInputForm was last opened.
/// @note This is used to circumvent a behavioral bug caused by how the input code is structured:
/// 1. User clicks a button that opens an InputForm.
/// 2. Grab InputForm Focus.
/// 3. User releases the button.
/// 4. The input form interprets this as "I am focused, but something else was just clicked".
/// 5. The input form drops its focus instantly, and it appears as if it was never focused.
float time_focused = 0.f;
#pragma endregion
private:
};
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "Button.hpp"
namespace JUI {
/// The
class ToggleButton : public Button {
};
}

View File

@@ -12,32 +12,24 @@ namespace JUI
Tooltip() : TextRect() {
Name("Tooltip");
}
explicit Tooltip(Widget* parent) : Tooltip()
{
this->Parent(parent);
explicit Tooltip(Widget* parent) : Tooltip() {
attachment = parent;
this->Parent(parent->GetFamilyTreeRoot());
this->AutoFitSizeToText(true);
this->ZIndex(10);
this->Visible(false);
this->Size({100_px, 20_px});
}
void Update(float delta) override
{
void Update(float delta) override;
if (parent && parent->IsMouseInside() || IsMouseInside())
{
auto coords = InputService::GetMousePosition();
Position(UDim2::FromPixels(coords.x, coords.y));
Visible(true);
} else
Visible(false);
TextRect::Update(delta);
}
~Tooltip() override {};
Widget* attachment = nullptr;
float PopupDelay() const { return 0;}
protected:
float popup_delay = 0.f;
};
}

View File

@@ -34,6 +34,9 @@ namespace JUI {
/// which can be dragged around, resized, and docked into other applicable widgets.
class Window : public Widget, public RectBase, public Clickable, public Hoverable, public Draggable, public Resizable, public Dockable {
public:
Event<> OnOpen;
Event<> OnClose;
/// The default constructor sets a default style for this Window.
Window();
/// Construct a window widget by specifying it's parent.
@@ -55,18 +58,35 @@ namespace JUI {
// TODO: Decide if this will auto-scale with the titlebar's text height, or the other way around.
[[nodiscard]] int TitlebarHeight() const;
void HideTitlebar() {
titlebar_hidden = true;
Topbar->Visible(false);
}
void ShowTitlebar() {
titlebar_hidden = false;
Topbar->Visible(true);
}
/// @return True if this window widget is on-top of all other window-widgets with the same parent.
bool OnTop() const;
/// Brings this window widget to the top of the window-widget stack.
void BringToTop();
/// Moves this window up in the window-widget stack.
void MoveUp() const;
void MoveUp() {
zindex++;
}
/// Moves this window down in the window-widget stack.
void MoveDown() const;
void MoveDown() {
zindex--;
}
void TitlebarHeight(int height);
@@ -156,8 +176,13 @@ namespace JUI {
{
// TODO: Consider how this plays with Clickable::OnClick and Clickable::OnRelease
if (this->visible && this->focused)
return Widget::ObserveMouseInput(btn, pressed);
if (visible) {
if (!pressed)
return Widget::ObserveMouseInput(btn, false);
if (this->focused)
return Widget::ObserveMouseInput(btn, pressed);
}
// Special case to allow drop of "Resizing"
if (btn == MouseButton::Right && pressed == false)
@@ -167,10 +192,12 @@ namespace JUI {
}
[[nodiscard]] bool IsOpen() const;
void Open();
void Close();
void SetOpen(bool value);
void Toggle();
[[nodiscard]] bool IsClosed() const;
virtual void Open();
virtual void Close();
virtual void SetOpen(bool value);
virtual void Toggle();
protected:
void UpdateInternalWidgetsTitlebarHeight();
@@ -187,6 +214,7 @@ namespace JUI {
bool open = false;
//bool resizing = false;
bool draggable = true;
bool titlebar_hidden = false;
bool dockable = false;
int titlebar_height = Style::Window::TitlebarHeight;
int title_font_size = 16;

364
main.cpp
View File

@@ -34,12 +34,15 @@
#include <JUI/Widgets/Collapsible.hpp>
#include <JUI/Widgets/CommandLine.hpp>
#include "JUI/Widgets/Anchor.hpp"
#include "JUI/Widgets/ColorPicker.hpp"
#include "JUI/Widgets/FileDialog.hpp"
#include "JUI/Widgets/FpsGraph.hpp"
#include "JUI/Widgets/LabeledSlider.hpp"
#include "JUI/Widgets/Link.hpp"
using namespace JUI;
float ui_scale = 1.f;
float accum = 0;
int iter = 0;
@@ -54,6 +57,7 @@ JUI::TextRect* widget_count = nullptr;
JUI::CommandLine* console = nullptr;
JUI::Window* nineslice_demo_window = nullptr;
JUI::UtilityBar* topbar = nullptr;
JUI::FpsGraphWindow* fps_graph = nullptr;
/// Returns the sum total of widgets that are considered "descendnats" of the given widget.
/// A descendant is classified as being a child, or child of a child, and so on, of a given widget.
@@ -61,6 +65,10 @@ int count_descendants(JUI::Widget* w) {
return w->GetDescendants().size();
}
void CreateFileDialog() {
auto* dialog = new JUI::FileDialogWindow(scene, ".");
}
/// Creates the window that provides a "Welcome" dialog to the user.
/// Project metadata and copyright information is presented.
JUI::Window* CreateInfoboxWindow(JUI::Widget* root) {
@@ -153,7 +161,11 @@ JUI::UtilityBar* CreateUtilityBar(JUI::Widget* root) {
demos->AddButton("Scroll Widget Demo", [&] {});
demos->AddButton("Command Line", [&]{ console->Toggle();});
demos->AddButton("Command Line", [&] {
console->Toggle();
});
demos->AddButton("File Dialog", [&] {CreateFileDialog();});
auto* test_sub = demos->AddSubmenu("Fruit"); {
test_sub->AddButton("Apples");
@@ -167,6 +179,13 @@ JUI::UtilityBar* CreateUtilityBar(JUI::Widget* root) {
berries->AddButton("Blueberries");
}
}
auto* ptA = new JUI::Anchor(topbar, {30_px, 40_px});
auto* ptB = new JUI::Anchor(demos, UDim2::BottomRight);
auto* line = new JUI::ArrowDecorator(topbar, ptB, ptA);
}
auto* edit = topbar->AddButton("Edit");
@@ -177,8 +196,19 @@ JUI::UtilityBar* CreateUtilityBar(JUI::Widget* root) {
view->AddButton("Decrease UI Scale 5%", [&] {
ui_scale -= 0.05f;
});
}
view->AddSeparator();
view->AddButton("Toggle Docked FPSGraph", [&] mutable {
static bool docked = false;
if (docked) {
fps_graph->UndockGraph();
docked = false;
} else {
fps_graph->DockGraph();
docked = true;
}
});
}
auto* help = topbar->AddButton("About", [&] {
@@ -201,102 +231,244 @@ JUI::UtilityBar* CreateUtilityBar(JUI::Widget* root) {
/// Constructs, styles, and returns a JUI::Rect which contains a sample of each widget in action.
JUI::Rect* CreateWidgetList(JUI::Widget* root) {
auto* widget_list = new Rect(root);
auto* window = new Window(root);
window->Title("Widgets");
window->Size({300_px, 80_percent});
auto* widget_list = new Rect(window->Content());
//column_rect->ZIndex(4);
widget_list->Size({0, -24, 0.2f, 1.f});
widget_list->Position({0, 24, 0, 0});
widget_list->Size({0, 0, 1.f, 1.f});
widget_list->Position({0, 0, 0, 0});
widget_list->BGColor(Colors::Grays::Gainsboro);
auto* column_layout = new VerticalListLayout(widget_list);
auto* btn_container1 = new Rect(column_layout);
btn_container1->Size({100_percent, 24_px});
auto* button_section = new Collapsible(column_layout);
{
button_section->Title("Button Styles");
button_section->Size({100_percent, 100_px});
auto* button_section_layout = new VerticalListLayout(button_section->ContentBox());
auto* btn_h_layout1 = new HorizontalListLayout(btn_container1);
btn_h_layout1->Padding({2_px});
auto* btn_container1 = new Rect(button_section_layout);
{
btn_container1->Size({100_percent, 32_px});
auto* button = new TextButton(btn_h_layout1);
//button->Position({5, 105, 0, 0});
button->Size({0, 20, 0.32f, 0});
button->TextColor(Colors::Black);
button->Content("Button");
button->AlignLeft();
//button->Padding(5_px);
auto* tt2 = new JUI::Tooltip(button);
tt2->Content("Test 123");
auto* button = new TextButton(btn_container1);
//button->Position({5, 105, 0, 0});
button->Size({30_percent, 100_percent-10_px});
button->Position({10_px, 50_percent});
button->AnchorPoint({0, 0.5});
button->TextColor(Colors::Black);
button->Content("Left");
button->AlignLeft();
auto* button2 = new TextButton(btn_h_layout1);
//button2->Position({5, 105, 0, 0});
button2->Size({0, 20, 0.32f, 0});
button2->TextColor(Colors::Black);
button2->Content("Button");
button2->AlignCenterHorizontally();
auto* tt2 = new JUI::Tooltip(button);
tt2->Content("Test 123");
auto* button3 = new TextButton(btn_h_layout1);
//button2->Position({5, 105, 0, 0});
button3->Size({0, 20, 0.32f, 0});
button3->TextColor(Colors::Black);
button3->Content("Button");
button3->AlignRight();
auto* button2 = new TextButton(btn_container1);
button2->Size({30_percent, 100_percent-10_px});
button2->Position({50_percent, 50_percent});
button2->AnchorPoint({0.5f, .5f});
button2->TextColor(Colors::Black);
button2->Content("Center");
button2->AlignCenterHorizontally();
auto* btn_container2 = new Rect(column_layout);
btn_container2->Size({100_percent, 24_px});
btn_container2->BGColor(Colors::DarkGray);
auto* button3 = new TextButton(btn_container1);
//button2->Position({5, 105, 0, 0});
button3->Size({30_percent, 100_percent-10_px});
button3->TextColor(Colors::Black);
button3->Position({100_percent-10_px, 50_percent});
button3->AnchorPoint({1, 0.5});
button3->Content("Right");
button3->AlignRight();
}
auto* btn_h_layout2 = new HorizontalListLayout(btn_container2);
btn_h_layout2->Padding({2_px});
auto* button4 = new TextButton(btn_h_layout2);
//button->Position({5, 105, 0, 0});
button4->Size({0, 20, 0.32f, 0});
button4->TextColor(Colors::Black);
button4->Content("Button");
button4->AlignLeft();
//button4->CornerRounding(4);
auto* btn_container2 = new Rect(button_section_layout);
{
btn_container2->Size({100_percent, 32_px});
btn_container2->BGColor(Colors::DarkGray);
auto* button5 = new TextButton(btn_h_layout2);
//button2->Position({5, 105, 0, 0});
button5->Size({0, 20, 0.32f, 0});
button5->TextColor(Colors::Black);
button5->Content("Button");
button5->AlignCenterHorizontally();
//button5->CornerRounding(4);
auto* button6 = new TextButton(btn_h_layout2);
//button2->Position({5, 105, 0, 0});
button6->Size({0, 20, 0.32f, 0});
button6->TextColor(Colors::Black);
button6->Content("Button");
button6->AlignRight();
//button6->CornerRounding(4);
auto* button4 = new TextButton(btn_container2);
//button->Position({5, 105, 0, 0});
button4->Size({30_percent, 100_percent-10_px});
button4->Position({10_px, 50_percent});
button4->AnchorPoint({0, 0.5});
button4->TextColor(Colors::Black);
button4->Content("Left");
button4->AlignLeft();
button4->CornerRounding(8);
button4->BorderWidth(2);
button4->BorderColor(Colors::Cyans::Aqua);
auto* checkbox_container = new Rect(column_layout);
checkbox_container->Size({0, 24, 1, 0});
auto* button5 = new TextButton(btn_container2);
button5->Size({30_percent, 100_percent-10_px});
button5->Position({50_percent, 50_percent});
button5->AnchorPoint({0.5f, .5f});
button5->TextColor(Colors::Black);
button5->Content("Center");
button5->AlignCenterHorizontally();
button5->CornerRounding(8);
button5->BorderWidth(2);
button5->BorderColor(Colors::Cyans::Aqua);
auto* checkbox_horiz = new HorizontalListLayout(checkbox_container);
checkbox_horiz->Padding(2_px);
auto* button6 = new TextButton(btn_container2);
button6->Size({30_percent, 100_percent-10_px});
button6->TextColor(Colors::Black);
button6->Position({100_percent-10_px, 50_percent});
button6->AnchorPoint({1, 0.5});
button6->Content("Right");
button6->AlignRight();
button6->CornerRounding(8);
button6->Disable();
}
auto* label = new TextRect(checkbox_horiz);
label->Content("Checkboxes");
label->BorderWidth(0);
label->AutoFitSizeToText(true);
auto* check1 = new Checkbox(checkbox_horiz);
check1->Size({20_px, 20_px});
auto* check2 = new Checkbox(checkbox_horiz);
check2->Size({20_px, 20_px});
check2->CheckedColor(Colors::Blue);
check2->CornerRounding(7);
auto* check3 = new Checkbox(checkbox_horiz);
check3->Size({20_px, 20_px});
check3->CheckedColor(Colors::Oranges::Coral);
check3->CornerRounding(7);
auto* input_form = new TextInputForm(column_layout);
input_form->Size({0,24, 1, 0});
input_form->Content("");
input_form->TextSize(14);
auto* link_container = new Rect(button_section_layout);
{
link_container->Size({100_percent, 20_px});
link_container->BGColor(Colors::DarkGray);
auto* link = new JUI::Link(link_container);
link->Content("Clickable Link");
link->Center();
link->Clicked += [](auto event) {
std::cout << "Link Released" << std::endl;
};
}
}
auto* checkbox_section = new Collapsible(column_layout);
{
checkbox_section->Title("Checkbox Styles");
checkbox_section->Size({100_percent, 60_px});
auto* checkbox_layout = new VerticalListLayout(checkbox_section->ContentBox());
{auto* set_1 = new Rect(checkbox_layout);
set_1->Size({100_percent, 24_px});
auto* set_1_layout = new HorizontalListLayout(set_1);
auto* label = new TextRect(set_1_layout);
label->Content("Standard");
label->BorderWidth(0);
label->AutoFitSizeToText(true);
//label->Size({50_px, 20_px});
auto* separator_1 = new Separator(set_1_layout, Orientation::Vertical, 10_px);
separator_1->Visible(false);
auto* check1 = new Checkbox(set_1_layout);
check1->Size({20_px, 20_px});
auto* separator_2 = new Separator(set_1_layout, Orientation::Vertical, 10_px);
separator_2->Visible(false);
auto* check2 = new Checkbox(set_1_layout);
check2->Size({20_px, 20_px});
check2->CheckedColor(Colors::Blue);
check2->CornerRounding(7);
auto* separator_3 = new Separator(set_1_layout, Orientation::Vertical, 10_px);
separator_3->Visible(false);
auto* check3 = new Checkbox(set_1_layout);
check3->Size({20_px, 20_px});
check3->CheckedColor(Colors::Oranges::Coral);
check3->CornerRounding(7);
auto* separator_4 = new Separator(set_1_layout, Orientation::Vertical, 10_px);
separator_4->Visible(false);
auto* check4 = new Checkbox(set_1_layout);
check4->Size({20_px, 20_px});
check4->DisabledBorderColor(Colors::Black);
check4->Disable();
};
{auto* set_2 = new Rect(checkbox_layout);
set_2->Size({100_percent, 24_px});
auto* layout = new HorizontalListLayout(set_2);
auto* label = new TextRect(layout);
label->Content("Customized:");
label->BorderWidth(0);
label->AutoFitSizeToText(true);
auto* separator_1 = new Separator(layout, Orientation::Vertical, 20_px);
separator_1->Visible(false);
auto* check1 = new Checkbox(layout);
check1->Size({20_px, 20_px});
auto* separator_2 = new Separator(layout, Orientation::Vertical, 10_px);
separator_2->Visible(false);
auto* check2 = new Checkbox(layout);
check2->Size({20_px, 20_px});
check2->CheckedColor(Colors::Blue);
check2->CornerRounding(7);
auto* separator_3 = new Separator(layout, Orientation::Vertical, 10_px);
separator_3->Visible(false);
auto* check3 = new Checkbox(layout);
check3->Size({20_px, 20_px});
check3->CheckedColor(Colors::Oranges::Coral);
check3->CornerRounding(10);
check3->BorderMode(BorderMode::Inset);
check3->BorderWidth(2);
}
}
auto* inputform_section = new Collapsible(column_layout);
{
inputform_section->Title("Input Forms");
inputform_section->Size({100_percent, 50_px});
auto* input_form = new TextInputForm(inputform_section->ContentBox());
input_form->Size({0,24, 1, 0});
input_form->Content("");
input_form->SetAutoCompleteText("Accepts text input!");
input_form->TextSize(14);
}
auto* slider_section = new Collapsible(column_layout);
{
slider_section->Title("Slider Styles");
slider_section->Size({100_percent, 50_px});
auto* slider = new LabeledSlider(slider_section->ContentBox());
slider->Size({90_percent, 12_px});
slider->Position({50_percent, 50_percent});
slider->AnchorPoint({0.5f, 0.5f});
slider->Content("Value: 0");
slider->Minimum(-1.f);
slider->Maximum(1.f);
slider->CurrentValue(0.f);
slider->Interval(1/5.f);
slider->ValueChanged += [slider](float value) mutable {
slider->Content(std::format("Value: {}", value));
};
slider->CornerRounding(6);
slider->ScrubberRounding(12);
slider->ScrubberColor(Colors::Black);
slider->ScrubberSize({24_px, 24_px});
//slider->SetClipsDescendants(true);
slider->BGColor(Colors::Blues::DarkSlateBlue);
slider->Center();
}
auto* collapsible = new Collapsible(column_layout);
collapsible->Size({100_percent, 200_px});
@@ -330,15 +502,6 @@ JUI::Rect* CreateWidgetList(JUI::Widget* root) {
auto* radio_c_btn = new Checkbox(radio_btn_set_layout);
radio_c_btn->Size({20_px, 20_px});
// What the FUCK?
/*auto* radio_c_label = new TextRect(radio_btn_set_layout);
radio_c_label->BorderWidth(0);
radio_c_label->Size({20_px, 20_px});
radio_c_label->AutoFitSizeToText(true);
radio_c_label->SetContent("C ");
radio_c_label->SetTextSize(12);*/
return widget_list;
}
@@ -373,19 +536,12 @@ JUI::Scene* CreateScene() {
auto *root = new Scene();
auto* graph_window = new JUI::Window(root);
graph_window->Name("Graph Window");
graph_window->Title("Graph Window");
graph_window->Size({500_px, 50_px});
auto* graph = new JUI::FpsGraph(graph_window->Content());
graph->Name("Graph");
graph->Size({500_px, 50_px});
fps_graph = new JUI::FpsGraphWindow(root);
auto* colorpicker_wnd = new JUI::Window(root);
colorpicker_wnd->Name("Slider ColorPicker");
colorpicker_wnd->Size({100_px, 150_px});
colorpicker_wnd->MinSize({100, 150});
colorpicker_wnd->Size({150_px, 100_px});
colorpicker_wnd->MinSize({50, 50});
auto* cpicker = new JUI::ColorPicker(colorpicker_wnd->Content());
topbar = CreateUtilityBar(root);
@@ -406,18 +562,6 @@ JUI::Scene* CreateScene() {
auto* widget_list = CreateWidgetList(root);
//auto* horizontal = new HorizontalListLayout(root);
//horizontal->ZIndex(1);
//auto* separator_a = new Rect(radio_btn_set_layout());
//nineslice_demo_window->Padding(1_px);
// End Window //
root->SetViewportSize({800, 600});

View File

@@ -34,32 +34,54 @@ namespace JUI {
void RectBase::Draw(const Color4& bgColor, const Color4& borderColor, const Vector2 &abs_pos, const Vector2 &abs_size) {
// Background rect
if (corner_rounding_radius > 0)
J2D::FillRoundedRect(bgColor, abs_pos, abs_size, corner_rounding_radius);
else
J2D::FillRect(bgColor, abs_pos, abs_size);
RectBase::Draw(bgColor, borderColor, abs_pos, abs_size, corner_rounding_radius, border_width, border_mode, corner_mode);
}
void RectBase::DrawOutline(const Color4 &color, const Vector2 &pos, const Vector2 &size, float rounding,
float borderWidth, enum JUI::BorderMode borderMode, enum JUI::CornerRoundingMode cornerRoundingMode) {
// Outline rect - compute the size change to fit the border accurately.
Vector2 border_offset = {0, 0};
if (border_mode == BorderMode::Inset)
border_offset = {-border_width/2.f, -border_width/2.f};
if (border_mode == BorderMode::Middle)
/// Border is too small, don't draw it.
if (borderWidth <= 0) return;
if (borderMode == BorderMode::Inset)
border_offset = {-borderWidth/2.f, -borderWidth/2.f};
if (borderMode == BorderMode::Middle)
border_offset = {0, 0};
if (border_mode == BorderMode::Outline)
border_offset = {border_width/2.f, border_width/2.f};
if (borderMode == BorderMode::Outline)
border_offset = {borderWidth/2.f, borderWidth/2.f};
// Draw the outline.
if (border_width > 0) {
if (corner_rounding_radius > 0)
J2D::OutlineRoundedRect(borderColor, abs_pos - border_offset, abs_size + (border_offset*2), corner_rounding_radius, border_width);
else
J2D::OutlineRect(borderColor, abs_pos - border_offset, abs_size + (border_offset*2), border_width);
}
if (cornerRoundingMode == CornerRoundingMode::None || rounding <= 0)
J2D::OutlineRect(color, pos - border_offset, size + (border_offset * 2), borderWidth);
else if (cornerRoundingMode == CornerRoundingMode::Rounded)
J2D::OutlineRoundedRect(color, pos - border_offset, size + (border_offset*2), rounding, borderWidth);
else if (cornerRoundingMode == CornerRoundingMode::Chamfer)
J2D::OutlineChamferRect(color, pos - border_offset, size + (border_offset*2), rounding, borderWidth);
}
void RectBase::DrawBG(const Color4 &color, const Vector2 &pos, const Vector2 &size, float rounding,
enum JUI::CornerRoundingMode rounding_mode) {
if (rounding_mode == CornerRoundingMode::None || rounding <= 0)
J2D::FillRect(color, pos, size);
else if (rounding_mode == CornerRoundingMode::Rounded)
J2D::FillRoundedRect(color, pos, size, rounding);
else if (rounding_mode == CornerRoundingMode::Chamfer)
J2D::FillChamferRect(color, pos, size, rounding);
}
void RectBase::Draw(const Color4 &bgColor, const Color4 &fgColor, const Vector2 &pos, const Vector2 &size,
float rounding, float borderWidth, enum JUI::BorderMode borderMode,
enum JUI::CornerRoundingMode rounding_mode) {
DrawBG(bgColor, pos, size, rounding, rounding_mode);
DrawOutline(fgColor, pos, size, rounding, borderWidth, borderMode, rounding_mode);
}
void RectBase::BorderMode(const enum BorderMode &mode) {
border_mode = mode;
}
@@ -70,6 +92,7 @@ namespace JUI {
corner_rounding_radius = radius;
}
float RectBase::CornerRounding() const { return corner_rounding_radius; }

View File

@@ -147,6 +147,10 @@ float Widget::GetAbsoluteRotation() const {
return rotation;
}
Vector2 Widget::GetAbsoluteCentroid() const {
return GetAbsolutePosition() + (GetAbsoluteSize() / 2.f);
}
std::vector<Widget *> Widget::GetChildren() { return children; }
std::vector<Widget *> Widget::GetDescendants() {
@@ -281,6 +285,9 @@ void Widget::UpdateTweens(float elapsed) {
}
void Widget::Update(float delta) {
is_mouse_inside = ComputeIsMouseInside();
UpdateChildWidgets(delta);
UpdateTweens(delta);
}
@@ -313,7 +320,8 @@ Widget* Widget::GetFamilyTreeRoot() const {
return parent->GetFamilyTreeRoot();
}
bool Widget::IsMouseInside() const {
bool Widget::ComputeIsMouseInside() const {
float x = last_known_mouse_pos.x;
float y = last_known_mouse_pos.y;
auto pos = GetAbsolutePosition();
@@ -325,6 +333,20 @@ bool Widget::IsMouseInside() const {
return false;
}
bool Widget::IsMouseInside() const {
return is_mouse_inside;
/*float x = last_known_mouse_pos.x;
float y = last_known_mouse_pos.y;
auto pos = GetAbsolutePosition();
auto size = GetAbsoluteSize();
if (x > pos.x && y > pos.y && x < pos.x + size.x && y < pos.y + size.y)
return true;
return false;*/
}
bool Widget::IsMouseInsideChildren(bool include_this) const {
if (include_this)
if (IsMouseInside())

View File

@@ -1,7 +1,7 @@
#include <JUI/Mixins/Clickable.hpp>
void JUI::Clickable::Update(const Vector2 &mpos, const JUI::MouseButton &btn, bool hovering) {
//Hoverable::Update(mpos, delta);
}
void JUI::Clickable::OnRelease(const Vector2 &MousePos, const JUI::MouseButton &MouseButton, bool MouseStillOver) {

View File

@@ -0,0 +1,3 @@
#include <JUI/Mixins/Focusable.hpp>
bool JUI::Focusable::Focused() const { return focused; }

View File

@@ -27,8 +27,9 @@ void JUI::Hoverable::OnExitDescendants(const Vector2& mouse) {
// TODO: Implement
}
void JUI::Hoverable::Update(const Vector2 &m_pos, float delta) {
void JUI::Hoverable::Update(bool mouse_inside, float delta) {
hovered = mouse_inside;
/*if (tooltip != nullptr)
{
if (IsHovered()) {
@@ -41,12 +42,12 @@ void JUI::Hoverable::Update(const Vector2 &m_pos, float delta) {
if (IsHovered() && !hover_debounce) {
OnHover(m_pos);
OnHover({0,0});
hover_debounce = true;
}
if (!IsHovered() && hover_debounce) {
OnExit(m_pos);
OnExit({0,0});
hover_debounce = false;
}
}

View File

@@ -16,6 +16,10 @@ JUI::UDim JUI::UDimLiterals::operator ""_px(unsigned long long px) {
return {static_cast<int>(px), 0.f};
}
JUI::UDim JUI::UDim::FromPixels(int pixels) { return {pixels, 0.f}; }
JUI::UDim JUI::UDim::FromScale(float scale) { return {0, scale}; }
JUI::UDim JUI::UDim::operator+(const UDim &rhs) const {
return {Pixels + rhs.Pixels, Scale + rhs.Scale};
}

View File

@@ -1,5 +1,18 @@
#include <JUI/UDim2.hpp>
const JUI::UDim2 JUI::UDim2::Center {50_percent, 50_percent};
const JUI::UDim2 JUI::UDim2::TopLeft {0_percent, 0_percent};
const JUI::UDim2 JUI::UDim2::BottomRight {100_percent, 100_percent};
const JUI::UDim2 JUI::UDim2::BottomLeft {0_percent, 100_percent};
const JUI::UDim2 JUI::UDim2::TopRight {100_percent, 0_percent};
const JUI::UDim2 JUI::UDim2::TopCenter {50_percent, 0_percent};
const JUI::UDim2 JUI::UDim2::LeftCenter {0_percent, 50_percent};
const JUI::UDim2 JUI::UDim2::BottomCenter {50_percent, 100_percent};
const JUI::UDim2 JUI::UDim2::RightCenter {100_percent, 50_percent};
JUI::UDim2::UDim2() {
//X = {0, 0.f};
//Y = {0, 0.f};

View File

@@ -46,9 +46,8 @@ namespace JUI {
}
void Button::Update(float delta) {
hovered = IsMouseInside();
Hoverable::Update(last_known_mouse_pos, delta);
Hoverable::Update(IsMouseInside(), delta);
// TODO: This is duplicated between here and Window.cpp
// Will be abstracted away into Clickable shortly.
@@ -161,7 +160,7 @@ namespace JUI {
BaseBGColor(base);
HoveredBGColor(hover);
PressedBGColor(pressed);
DisabledBGColor(disabled);\
DisabledBGColor(disabled);
UpdateVisualState();
}
}

View File

@@ -22,12 +22,14 @@ namespace JUI {
void Checkbox::InnerDraw() {
Rect::InnerDraw();
// TODO: Renders too large with BorderMode::Inset
if (checked) {
Vector2 pos_pad = ComputeElementPadding(GetAbsoluteSize(), {PaddingLeft(), PaddingTop()});
Vector2 size_pad = ComputeElementPadding(GetAbsoluteSize(), {PaddingLeft()+PaddingRight(), PaddingTop()+PaddingBottom()});
Vector2 padded_pos = GetAbsolutePosition() + pos_pad;
Vector2 padded_size = GetAbsoluteSize() - size_pad;
RectBase::Draw(check_color, check_color, padded_pos, padded_size);
RectBase::Draw(check_color, border_color, padded_pos, padded_size);
}
}
}

View File

@@ -1,3 +1,134 @@
//
// Created by dawsh on 5/1/25.
//
#include <JUI/Widgets/ColorPicker.hpp>
std::string rgb2hex(int r, int g, int b, bool with_head) {
std::stringstream ss;
if (with_head)
ss << "#";
ss << std::hex << (r << 16 | g << 8 | b );
return ss.str();
}
void JUI::ColorPicker::Font(const JGL::Font &value) {
TextStyler::Font(value);
hue_label->Font(value);
sat_label->Font(value);
bri_label->Font(value);
hex_label->Font(value);
}
void JUI::ColorPicker::TextSize(int size) {
TextStyler::TextSize(size);
hue_label->TextSize(size);
sat_label->TextSize(size);
bri_label->TextSize(size);
hex_label->TextSize(size);
}
void JUI::ColorPicker::TextColor(const Color4 &color) {
TextStyler::TextColor(color);
hue_label->TextColor(color);
sat_label->TextColor(color);
bri_label->TextColor(color);
hex_label->TextColor(color);
}
void JUI::ColorPicker::StyleSlider(Slider *slider) {
slider->ScrubberSize({16_px, 16_px});
slider->CornerRounding(8);
slider->BGColor(Colors::DarkGray);
slider->ScrubberRounding(8);
slider->Size({100_percent-10_px, 16_px});
}
JUI::ColorPicker::ColorPicker(): Rect() {
Name("ColorPicker");
Size({100_percent, 100_percent});
//auto* row_layout = new JUI::VerticalListLayout(this);
hue_slider = new JUI::Slider(this);
StyleSlider(hue_slider);
hue_slider->Position({50_percent, 4_px});
hue_slider->AnchorPoint({0.5f, 0.f});
hue_slider->Minimum(0.f);
hue_slider->Maximum(360);
hue_slider->Interval(1.f / 360.f);
hue_slider->ValueChanged += [this] (float val) mutable {
hue_label->Content(std::format("Hue: {}", Math::FloorInt(val)));
hue = val;
hue_slider->ScrubberColor(Color4::FromHSV(hue, 1.f, 1.f));
OnColorValueChanged.Invoke(Color4::FromHSV(hue, sat, bri));
RecomputeVisuals();
};
hue_label = new JUI::Text(hue_slider);
hue_label->AlignCenterHorizontally();
hue_label->AlignTop();
hue_label->TextColor(Colors::White);
hue_label->Content("Hue: 0");
sat_slider = new JUI::Slider(this);
StyleSlider(sat_slider);
sat_slider->Position({50_percent, 24_px});
sat_slider->AnchorPoint({0.5f, 0.f});
sat_slider->Minimum(0);
sat_slider->Maximum(1);
sat_slider->Interval(1e-3f);
sat_slider->ValueChanged += [this] (float val) mutable {
sat_label->Content(std::format("Saturation: {}%", Math::Floor(val*100.f)));
sat = val;
OnColorValueChanged.Invoke(Color4::FromHSV(hue, sat, bri));
RecomputeVisuals();
};
sat_label = new JUI::Text(sat_slider);
sat_label->AlignCenterHorizontally();
sat_label->AlignTop();
sat_label->TextColor(Colors::White);
sat_label->Content("Saturation: 100%");
bri_slider = new JUI::Slider(this);
StyleSlider(bri_slider);
bri_slider->Position({50_percent, 44_px});
bri_slider->AnchorPoint({0.5f, 0.f});
bri_slider->Minimum(0);
bri_slider->Maximum(1);
bri_slider->Interval(1e-3f);
bri_slider->ValueChanged += [this] (float val) mutable {
bri_label->Content(std::format("Brightness: {}%", Math::Floor(val*100.f)));
bri = val;
OnColorValueChanged.Invoke(Color4::FromHSV(hue, sat, bri));
RecomputeVisuals();
};
bri_label = new JUI::Text(bri_slider);
bri_label->AlignCenterHorizontally();
bri_label->AlignTop();
bri_label->TextColor(Colors::White);
bri_label->Content("Brightness: 100%");
//auto* hue_box = new JUI::Rect();
hex_label = new JUI::TextRect(this);
hex_label->AlignCenterHorizontally();
hex_label->Size({100_percent, 24_px});
hex_label->Position({0_px, 100_percent});
hex_label->Content("#FFZZGG");
}
JUI::ColorPicker::ColorPicker(Widget *parent): ColorPicker() {
Parent(parent);
}
void JUI::ColorPicker::RecomputeVisuals() {
Color4 computed = Color4::FromHSV(hue, sat, bri);
Color4 inverse = Color4(255 - computed.r, 255 - computed.g, 255 - computed.b);
hex_label->TextColor(inverse);
hex_label->BGColor(computed);
hex_label->Content(std::format("{}", rgb2hex(computed.r, computed.g, computed.b, true)));
}

View File

@@ -35,6 +35,7 @@ namespace JUI
input_box->TextSize(16);
input_box->TextColor(Colors::White);
input_box->SetAutocompleteTextColor(Colors::Gray);
input_box->KeepInputHistory(true);
input_box->OnReturn += [this] (const std::string& msg) {
OnInputBoxSend(msg);
@@ -46,17 +47,10 @@ namespace JUI
this->Parent(parent);
}
bool CommandLine::Opened() const { return this->open == true;}
bool CommandLine::Closed() const { return this->open == false;}
void CommandLine::Open(bool openVal) { SetOpen(openVal); }
void CommandLine::Close(bool openVal) { SetOpen(openVal);}
void CommandLine::SetOpen(bool openVal) {
this->Visible(openVal);
this->open = openVal;
Window::SetOpen(openVal);
this->input_box->SetFocused(openVal);
}
void CommandLine::Log(const std::string &message, const Color4 &color) {

View File

@@ -80,8 +80,8 @@ namespace JUI {
auto* btn = AddButton(name);
// TODO: Is this memory safe at all?
btn->OnClickEvent += [this, btn, callback] (auto a, auto b) mutable {
if (this->IsVisible() && btn->IsMouseInside())
btn->OnReleaseEvent += [this, btn, callback] (auto a, auto b, bool still_focus) mutable {
if (still_focus && this->IsVisible() && btn->IsMouseInside())
callback();
};
return btn;
@@ -109,8 +109,8 @@ namespace JUI {
submenu->Position({100_percent, 0_percent});
submenu->Visible(false);
btn->OnClickEvent += [this, submenu] (auto a, auto b) mutable {
if (this->IsVisible()) {
btn->OnReleaseEvent += [this, submenu] (auto a, auto b, bool still_there) mutable {
if (still_there && this->IsVisible()) {
this->Pin();
submenu->Visible(true);
}

View File

@@ -0,0 +1,126 @@
#include <JUI/Widgets/FpsGraph.hpp>
namespace JUI {
void FpsGraph::RenderDataPoints() {
J2D::Begin(canvas, nullptr, true);
DataPoint prev = data[0];
for (auto& data_pt : data) {
//J2D::DrawGradientLine(prev.color, data_pt.color, prev.pos, data_pt.pos, 1);
J2D::DrawLine(data_pt.color, data_pt.pos, {data_pt.pos.x, graph_height }, 1);
prev = data_pt;
}
float target_delta = 1.f / target_fps;
float target_line_height = target_delta * y_axis_scale;
float target_fps_rounded = Math::Floor(target_fps);
float target_delta_rounded = Math::Round(target_delta, 1);
std::string target_fps_label_text = std::format("{} FPS / {}ms (Target)", target_fps_rounded, target_delta_rounded);
J2D::DrawString(Colors::Black, target_fps_label_text, 0, graph_height - target_line_height, 10, 1.f);
J2D::DrawLine(Colors::White, {0, graph_height - target_line_height}, {GetAbsoluteSize().x, graph_height - target_line_height}, 1);
J2D::End();
}
FpsGraph::FpsGraph(): JUI::ImageRect() {
BGColor(Colors::Transparent);
BorderColor(Colors::Transparent);
for (int i = 0; i < sample_history; i++) {
Plot({i / 1.f, 0.f}, Colors::Black);
}
canvas = new JGL::RenderTarget(Vector2i(graph_width, graph_height));
RenderDataPoints();
Content(canvas);
}
FpsGraph::FpsGraph(Widget *parent): FpsGraph() {
Parent(parent);
}
void FpsGraph::Plot(const Vector2 &pt, const Color4 &col) {
data.push_back(DataPoint{ pt, col });
}
void FpsGraph::Plot(const DataPoint &data_pt) {
data.push_back(data_pt);
}
void FpsGraph::Update(float delta) {
ImageRect::Update(delta);
data.erase(data.begin());
for (auto& data_pt : data) {
data_pt.pos.x -= 1;
}
Color4 color = Colors::Green;
if (delta > 1.f/HighFPSRange())
color = Colors::Blue;
if (delta > 1.f/TargetFPSRange())
color = Colors::Yellow;
if (delta > 1.f/LowFPSRange())
color = Colors::Red;
if (delta > 1.f/UnplayableFPSRange())
color = Colors::Purples::Magenta;
if (delta > 1.f/5.f)
color = Colors::Black;
Plot({(data.size()-1.f), graph_height - (delta*y_axis_scale)}, color);
RenderDataPoints();
Content(canvas);
}
void FpsGraph::SetupAsPseudoDockedElementAtBottomOfScreen() {
this->Size({100_percent, 50_px});
this->AnchorPoint({1, 1});
this->Position({100_percent, 100_percent});
this->BGColor(Colors::Transparent);
this->BorderColor(Colors::Transparent);
}
void FpsGraph::ShowTargetFPS(bool show) { show_target_fps_bar = show;}
bool FpsGraph::ShowTargetFPS() const { return show_target_fps_bar;}
void FpsGraph::TargetFPS(float fps) {target_fps = fps;}
float FpsGraph::TargetFPS() const { return target_fps; }
float FpsGraph::HighFPSRange() const { return target_fps * high_fps_range_factor;}
float FpsGraph::TargetFPSRange() const { return target_fps * target_fps_range_factor;}
float FpsGraph::SubparFPSRange() const {return target_fps * subpar_fps_range_factor;}
float FpsGraph::LowFPSRange() const { return target_fps * low_fps_range_factor;}
float FpsGraph::UnplayableFPSRange() const { return target_fps * unplayable_fps_range_factor;}
float FpsGraph::SlideshowFPSRange() const { return target_fps * slideshow_fps_range_factor;}
FpsGraphWindow::FpsGraphWindow(): Window() {
Name("FPSGraphWindow");
Title("FPS Graph");
Size({500_px, 70_px});
this->SetResizable(false);
data_graph = new FpsGraph(this->Content());
data_graph->Size({500_px, 50_px});
}
FpsGraphWindow::FpsGraphWindow(Widget *parent): FpsGraphWindow() {
this->Parent(parent);
}
}

View File

@@ -0,0 +1,26 @@
#include <JUI/Widgets/LabeledSlider.hpp>
JUI::LabeledSlider::LabeledSlider(): Slider(), TextBase() {
}
JUI::LabeledSlider::LabeledSlider(Widget *parent): LabeledSlider() {
this->Parent(parent);
}
void JUI::LabeledSlider::Draw() {
Slider::Draw();
auto abs_pos = this->GetAbsolutePosition();
auto abs_size = this->GetAbsoluteSize();
auto pos_pad = GetAbsolutePaddingTopLeft();
auto size_pad = GetAbsolutePaddingBottomRight();
TextBase::Draw(abs_pos + pos_pad, abs_size - size_pad);
}
void JUI::LabeledSlider::Update(float elapsed) {
Slider::Update(elapsed);
TextBase::Update(elapsed);
}

View File

@@ -21,6 +21,7 @@ namespace JUI {
void Scene::Update(float delta) {
Widget::Update(delta);
}
void Scene::GlobalUIScale(const Vector2 &value) { ui_scale = value;}

View File

@@ -119,29 +119,30 @@ bool ScrollingRect::ObserveKeyInput(Key key, bool pressed) {
if (Widget::ObserveKeyInput(key, pressed))
return true;
if (IsMouseInside()) {
if (key == Keys::UpArrow && pressed)
{
Scroll(-scroll_amount);
return true;
}
if (key == Keys::DownArrow && pressed)
{
Scroll(scroll_amount);
return true;
}
if (key == Keys::PageDown && pressed) {
ScrollToBottom();
return true;
}
if (key == Keys::PageUp && pressed) {
ScrollToTop();
return true;
}
}
// TODO: Forego this in favor of mouse scrolling and clicking.
if (key == Keys::UpArrow && pressed)
{
Scroll(-scroll_amount);
return true;
}
if (key == Keys::DownArrow && pressed)
{
Scroll(scroll_amount);
return true;
}
if (key == Keys::PageDown && pressed) {
ScrollToBottom();
return true;
}
if (key == Keys::PageUp && pressed) {
ScrollToTop();
return true;
}
return false;
}

View File

@@ -6,12 +6,31 @@ namespace JUI {
Name("Separator");
}
Separator::Separator(Widget *parent): Separator() {
Parent(parent);
void Separator::UpdateSize() {
if (orientation == Orientation::Horizontal)
Size({100_percent, spacing});
if (orientation == Orientation::Vertical)
Size({spacing, 100_percent});
}
Separator::Separator(Widget *parent, const enum JUI::Orientation &orientation, const UDim &spacing,
const enum JUI::LineFillMode &line_mode, const Color4 &line_color, float line_thickness): Separator() {
this->orientation = orientation;
this->spacing = spacing;
this->line_mode = line_mode;
this->line_color = line_color;
this->line_thickness = line_thickness;
UpdateSize();
Parent(parent);
}
void Separator::Orient(const enum JUI::Orientation &orientation) {
this->orientation = orientation;
UpdateSize();
}
void Separator::LineFillMode(const enum JUI::LineFillMode &line_mode) {
@@ -24,6 +43,24 @@ namespace JUI {
line_color = color;
}
float Separator::GetAbsoluteSpacing() const {
if (parent == nullptr) {
// TODO: This function should in theory not be called in this case.
return 0.f;
}
auto abs_parent_size = parent->GetAbsoluteSize();
if (orientation == ::JUI::Orientation::Horizontal) {
return spacing.Pixels + (spacing.Scale * abs_parent_size.y);
}
if (orientation == Orientation::Vertical) {
return spacing.Pixels + (spacing.Scale * abs_parent_size.x);
}
// This control path should never be reached.
return 0.f;
}
void Separator::InnerDraw() {
Vector2 abs_pos = GetAbsolutePosition();
@@ -34,6 +71,8 @@ namespace JUI {
// TODO: Factor in padding.
float spacing = GetAbsoluteSpacing();
if (orientation == Orientation::Horizontal) {
start = abs_pos + Vector2(0, abs_size.y / 2.f);
end = start + Vector2(abs_size.x, 0);
@@ -44,22 +83,18 @@ namespace JUI {
end = start + Vector2(0, abs_size.y);
}
if (line_mode == LineFillMode::Dotted)
J2D::DrawDottedLine(line_color, start, end, spacing, line_thickness);
J2D::DrawDottedLine(line_color, start, end, line_spacing, line_thickness);
if (line_mode == LineFillMode::Dashed)
J2D::DrawDashedLine(line_color, start, end ,spacing, line_thickness);
J2D::DrawDashedLine(line_color, start, end, line_spacing, line_spacing, line_thickness);
if (line_mode == LineFillMode::Solid)
JGL::J2D::DrawLine(line_color, start, end, line_thickness);
}
float Separator::Spacing() const { return spacing; }
void Separator::Spacing(float spacing) {
this->spacing = spacing;
}
enum Orientation Separator::Orientation() const { return orientation;}
enum LineFillMode Separator::LineFillMode() const { return line_mode;}

View File

@@ -4,6 +4,29 @@
namespace JUI
{
template <typename T>
struct Range {
T min;
T max;
};
template <typename T>
T map(T value, T in_min, T in_max, T out_min, T out_max) {
return (value - in_min) / (in_max - in_min) * (out_max - out_min) + out_min;
}
template <typename T>
T map(T value, const Range<T>& in, const Range<T>& out) {
return map(value, in.min, in.max, out.min, out.max);
}
template<typename T>
T roundMultiple( T value, T multiple )
{
if (multiple == 0) return value;
return static_cast<T>(std::round(static_cast<double>(value)/static_cast<double>(multiple))*static_cast<double>(multiple));
}
Slider::Slider(JUI::Widget *parent) : Slider()
{
this->Parent(parent);
@@ -28,12 +51,8 @@ namespace JUI
}
void Slider::Update(float delta) {
bool selected = Rect::IsMouseInside();
hovered = IsMouseInside();
Hoverable::Update(last_known_mouse_pos, delta);
Hoverable::Update(Rect::IsMouseInside(), delta);
// TODO: This is duplicated between here and Window.cpp
// Will be abstracted away into Clickable shortly.
@@ -51,9 +70,10 @@ namespace JUI
if (dragging)
{
float range = maximum - minimum;
Vector2 mouse = last_known_mouse_pos;
float scrubber_width = GetScrubberAbsoluteSize().x;
float slider_abs_left = this->GetAbsolutePosition().x;
float slider_abs_right = (this->GetAbsolutePosition().x + this->GetAbsoluteSize().x) - scrubber_width;
float slider_total_width = this->GetAbsoluteSize().x - scrubber_width;
@@ -62,7 +82,7 @@ namespace JUI
float offset = scrubberP - slider_abs_left;
float percentage = offset / slider_total_width;
percentage = offset / slider_total_width;
float translated = Math::Clamp(roundMultiple(percentage * Range(), interval), 0.f, Range());
@@ -72,7 +92,6 @@ namespace JUI
ValueChanged(value);
current = value;
//std::cout << "slider value: " << value << std::endl;
}
// TODO there is probably a more elegant fix for this.
@@ -83,17 +102,46 @@ namespace JUI
Rect::Update(delta);
}
/// @note Calls GetScrubberAbsoluteSize() to compute box's offset.
Vector2 Slider::GetScrubberAbsolutePosition() const {
// TODO: Integrate padding.
float x_offset = percentage * (GetAbsoluteSize().x - GetScrubberAbsoluteSize().x);
float rect_half_height = (GetAbsoluteSize().y / 2.f);
float y_offset = rect_half_height - (GetScrubberAbsoluteSize().y / 2.f);
Vector2 scrub_rel_pos = {x_offset, y_offset};
return GetAbsolutePosition() + scrub_rel_pos;
}
Vector2 Slider::GetScrubberAbsoluteSize() const {
Vector2 abs_size = GetAbsoluteSize();
Vector2 scrub_size_px = scrubber_size.GetPixels();
Vector2 scrub_size_scale = scrubber_size.GetScale();
// TODO: Implement Slider::ScrubberPadding();
Vector2 padding = GetAbsolutePaddingBottomRight();
return scrub_size_px + (abs_size * scrub_size_scale) - padding;
}
void Slider::InnerDraw() {
Rect::InnerDraw();
Vector2 pos = GetScrubberAbsolutePosition();
Vector2 size = GetScrubberAbsoluteSize();
RectBase::Draw(scrubber_color, border_color, pos, size, scrubber_rounding, border_width, border_mode, corner_mode);
}
void Slider::Draw() {
//scrubber->Draw();
Rect::Draw();
Vector2 pos = {
current * (GetAbsoluteSize().x - scrubber_width) + GetAbsolutePosition().x
,GetAbsolutePosition().y
};
/// TODO: Implement internal padding on scrubber element?
//J2D::Begin();
JGL::J2D::FillRect(scrubber_color, pos, {scrubber_width, GetAbsoluteSize().y});
//J2D::End();
}
float Slider::Minimum() const { return minimum; }
@@ -102,11 +150,21 @@ namespace JUI
float Slider::Interval() const { return interval; }
float Slider::CurrentValue() const { return current; }
float Slider::CurrentValue() const {
return map(percentage, {0, 1}, {minimum, maximum});
}
Color4 Slider::ScrubberColor() const { return scrubber_color; }
float Slider::ScrubberWidth() const { return scrubber_width; }
float Slider::ScrubberWidth() const { return scrubber_size.X.Pixels; }
UDim2 Slider::ScrubberSize() const {
return scrubber_size;
}
float Slider::ScrubberRounding() const {
return scrubber_rounding;
}
float Slider::Range() const { return maximum - minimum; }
@@ -116,11 +174,22 @@ namespace JUI
void Slider::Interval(float inter) { interval = inter; }
void Slider::CurrentValue(float value) { current = value; }
void Slider::CurrentValue(float value) {
percentage = map(value, {minimum, maximum}, {0.f, 1.f});
}
float Slider::Percentage() const { return percentage;}
void Slider::Percentage(float value) { percentage = value; }
void Slider::ScrubberColor(const Color4 &color) { scrubber_color = color;}
void Slider::ScrubberWidth(float width) { scrubber_width = width;}
void Slider::ScrubberWidth(float width) { scrubber_size.X.Pixels = width;}
void Slider::ScrubberSize(const UDim2 &size) {
scrubber_size = size;
}
void Slider::ScrubberRounding(float rounding) { scrubber_rounding = rounding;}
Slider::Slider() {
Name("Slider");

View File

@@ -31,20 +31,18 @@ namespace JUI {
bool TextInputForm::ObserveMouseInput(MouseButton btn, bool pressed) {
if (pressed && btn == MouseButton::Left) {
if (!IsMouseInside() && focused && time_focused > 1.f) {
OnDeselect.Invoke();
focused = false;
}
if (IsMouseInside()) {
if (!focused) {
OnSelect.Invoke();
}
focused = true;
return true;
} else {
if (focused)
OnDeselect.Invoke();
focused = false;
if (IsMouseInside() && pressed && btn == MouseButton::Left) {
if (!focused) {
OnSelect.Invoke();
}
focused = true;
return true;
}
return Widget::ObserveMouseInput(btn, pressed);
}
@@ -87,6 +85,7 @@ namespace JUI {
// TODO: Make cursor actually blink
if (focused) {
time_focused += elapsed;
cursor_blink_time += elapsed;
if (cursor_blink_time > 1.f)
@@ -115,12 +114,27 @@ namespace JUI {
return s;
}
void TextInputForm::MoveCursorLeft() {
if (cursor_position > 0)
cursor_position--;
int TextInputForm::MoveCursorLeft(int chars) {
if (cursor_position <= 0) return 0; // TODO: This may be redundant?
int available_moves = Math::Min<int>(chars, cursor_position);
cursor_position -= available_moves;
return available_moves;
}
void TextInputForm::MoveCursorRight() {
int TextInputForm::MoveCursorRight(int chars) {
if (cursor_position >= CursorMaxPosition()) return 0;
int chars_until_end = CursorMaxPosition() - cursor_position;
int available_moves = Math::Min<int>(chars, chars_until_end);
cursor_position += available_moves;
return available_moves;
if (cursor_position < CursorMaxPosition()-1)
cursor_position++;
}
@@ -137,6 +151,15 @@ namespace JUI {
void TextInputForm::SendInput(bool clear_input) {
OnReturn.Invoke(input_buffer);
if (keep_input_history) {
if (history.size() == 0 || history[0] != input_buffer) {
history.insert(history.begin(), input_buffer);
}
}
history_index = -1;
saved_input_buffer = "";
if (clear_input) {
input_buffer = "";
cursor_position = 0;
@@ -159,11 +182,15 @@ namespace JUI {
if (cursor_position > 0) {
input_buffer = input_buffer.erase(cursor_position-1, 1);
cursor_position--;
saved_input_buffer = input_buffer;
}
}
void TextInputForm::PushStringToCurrentPlaceInInputBuffer(const std::string& snippet) {
input_buffer = input_buffer.insert(cursor_position, snippet);
cursor_position += snippet.length();
saved_input_buffer = input_buffer;
}
void TextInputForm::PushKeyToCurrentPlaceInInputBuffer(const Key& key) {
std::string insertion = lowercase(key.Mnemonic);
@@ -194,12 +221,30 @@ namespace JUI {
}
if (key == Keys::LeftArrow) {
MoveCursorLeft();
int qty = 1;
// TODO: Move Left until next space.
if (InputService::IsKeyDown(Keys::LeftShift)) qty = 5;
MoveCursorLeft(qty);
return true;
}
if (key == Keys::RightArrow) {
MoveCursorRight();
int qty = 1;
// TODO: Move Right until next space.
if (InputService::IsKeyDown(Keys::LeftShift)) qty = 5;
MoveCursorRight(qty);
return true;
}
if (key == Keys::DownArrow) {
if (keep_input_history)
ShowNextHistory();
return true;
}
if (key == Keys::UpArrow) {
if (keep_input_history)
ShowPrevHistory();
return true;
}
@@ -218,11 +263,45 @@ namespace JUI {
return true;
}
void TextInputForm::ShowNextHistory() {
if (keep_input_history) {
if (history_index > -1)
history_index--;
std::string result = "";
if (history_index == -1)
SetInputBuffer(saved_input_buffer);
else
SetInputBuffer(history[history_index]);
}
}
void TextInputForm::ShowPrevHistory() {
if (keep_input_history) {
if (history_index == -1 && !history.empty() || history.size()-1 > history_index) {
history_index++;
}
std::string result = "";
if (history_index == -1)
SetInputBuffer(saved_input_buffer);
else
SetInputBuffer(history[history_index]);
}
}
bool TextInputForm::HasFocus() const { return focused; }
void TextInputForm::SetFocused(bool focused) { this->focused = focused;}
void TextInputForm::SetFocused(bool focused) {
this->focused = focused;
if (focused) time_focused = 0;
}
void TextInputForm::GrabFocus() { SetFocused(true); }
void TextInputForm::GrabFocus() {
SetFocused(true);
}
void TextInputForm::DropFocus() { SetFocused(false); }
@@ -250,4 +329,13 @@ namespace JUI {
blacklist.insert(value);
}
bool TextInputForm::KeepInputHistory() const { return keep_input_history; }
void TextInputForm::KeepInputHistory(bool value) {
keep_input_history = value;
}
std::string TextInputForm::InputHistory(int index) const {
return history[index];
}
}

View File

@@ -30,9 +30,9 @@ namespace JUI {
Vector2 size = this->font.MeasureString(this->content, this->text_size);
//size += pad_shrinks_size_by*2.f;
if (abs_size.x < size.x)
//if (abs_size.x < size.x)
this->size.X.Pixels = size.x;
if (abs_size.y < size.y)
//if (abs_size.y < size.y)
this->size.Y.Pixels = size.y;
}

View File

@@ -1 +1,24 @@
#include <JUI/Widgets/Tooltip.hpp>
#include <JUI/Widgets/Tooltip.hpp>
void JUI::Tooltip::Update(float delta) {
if (attachment && attachment->IsMouseInside())
{
auto coords = InputService::GetMousePosition();
auto rel_pos = parent->GetAbsolutePosition();
auto rel_udim = UDim2::FromPixels(rel_pos.x, rel_pos.y);
auto anchor = AnchorPoint();
Position(UDim2::FromPixels(coords.x - rel_pos.x, coords.y - rel_pos.y - GetAbsoluteSize().y));
Visible(true);
} else
Visible(false);
TextRect::Update(delta);
}

View File

@@ -102,7 +102,7 @@ namespace JUI {
exit_btn->PressedImageColor(Colors::Reds::DarkRed);
exit_btn->OnReleaseEvent += [&] (...) {
this->Visible(false);
this->Close();
};
}
@@ -215,9 +215,8 @@ namespace JUI {
}
Widget::Update(delta);
hovered = IsMouseInside();
Hoverable::Update(last_known_mouse_pos, delta);
Hoverable::Update(IsMouseInside(), delta);
// TODO: This is duplicated between here and Window.cpp
// Will be abstracted away into Clickable shortly.
@@ -226,7 +225,6 @@ namespace JUI {
OnClick(last_known_mouse_pos, mbtn);
}
if (prev_mb_state && !mb_state)
{
OnRelease(last_known_mouse_pos, mbtn, IsHovered());
@@ -317,8 +315,18 @@ namespace JUI {
}
void Window::SetOpen(bool value) {
bool value_changed = value != open;
open = value;
Visible(open);
Visible(value);
if (value_changed) {
if (value)
OnOpen.Invoke();
else
OnClose.Invoke();
}
}
void Window::Toggle() {
@@ -348,4 +356,6 @@ namespace JUI {
}
bool Window::IsOpen() const { return open;}
}
bool Window::IsClosed() const { return !open;}
}