Update zwidget

* Moved subtree scripts to tools, updated bash script for granularity

* Squashed 'libraries/ZWidget/' changes from 96501b6ef1..02da8cdc0e

02da8cdc0e Added ListView::GetPreferredWidth, ListView::GetPreferredHeight, ListView::GetMinHeight
04bf153d9d Added event subscriber system
1ce55e5da2 Added Dropdown Known issues: does not hide when parent widget's visibility changes

git-subtree-dir: libraries/ZWidget
git-subtree-split: 02da8cdc0e0ca14443245e0aa5a25b3f8e84928b
This commit is contained in:
Marcus Minhorst 2025-08-09 17:28:36 -07:00 committed by GitHub
commit 2ef2243f7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 514 additions and 12 deletions

View file

@ -46,6 +46,7 @@ set(ZWIDGET_SOURCES
src/widgets/textlabel/textlabel.cpp
src/widgets/pushbutton/pushbutton.cpp
src/widgets/checkboxlabel/checkboxlabel.cpp
src/widgets/dropdown/dropdown.cpp
src/widgets/listview/listview.cpp
src/widgets/tabwidget/tabwidget.cpp
src/window/window.cpp
@ -164,6 +165,7 @@ source_group("src\\widgets\\imagebox" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE
source_group("src\\widgets\\textlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/textlabel/.+")
source_group("src\\widgets\\pushbutton" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/pushbutton/.+")
source_group("src\\widgets\\checkboxlabel" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/checkboxlabel/.+")
source_group("src\\widgets\\dropdown" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/dropdown/.+")
source_group("src\\widgets\\listview" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/listview/.+")
source_group("src\\widgets\\tabwidget" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/widgets/tabwidget/.+")
source_group("src\\window" REGULAR_EXPRESSION "${CMAKE_CURRENT_SOURCE_DIR}/src/window/.+")

View file

@ -1,8 +1,10 @@
#include <iostream>
#include <zwidget/core/widget.h>
#include <zwidget/core/resourcedata.h>
#include <zwidget/core/image.h>
#include <zwidget/core/theme.h>
#include <zwidget/window/window.h>
#include <zwidget/widgets/dropdown/dropdown.h>
#include <zwidget/widgets/textedit/textedit.h>
#include <zwidget/widgets/mainwindow/mainwindow.h>
#include <zwidget/widgets/listview/listview.h>
@ -34,6 +36,7 @@ public:
PlayButton = new PushButton(this);
ExitButton = new PushButton(this);
GamesList = new ListView(this);
Choices = new Dropdown(this);
SetWindowBackground(Colorf::fromRgba8(51, 51, 51));
SetWindowBorderColor(Colorf::fromRgba8(51, 51, 51));
@ -60,6 +63,10 @@ public:
BrightmapsCheckbox->SetText("Brightmaps");
WidescreenCheckbox->SetText("Widescreen");
Choices->AddItem("First");
Choices->AddItem("Second");
Choices->AddItem("Third");
try
{
auto filedata = ReadAllBytes("banner.png");
@ -93,9 +100,11 @@ public:
y += 10.0;
SelectLabel->SetFrameGeometry(20.0, y, GetWidth() - 40.0, SelectLabel->GetPreferredHeight());
SelectLabel->SetFrameGeometry(20.0, y, GetWidth() - 40.0 - Choices->GetPreferredWidth(), SelectLabel->GetPreferredHeight());
y += SelectLabel->GetPreferredHeight();
Choices->SetFrameGeometry(GetWidth() - 20.0 - Choices->GetPreferredWidth(), y-Choices->GetPreferredHeight(), Choices->GetPreferredWidth(), Choices->GetPreferredHeight());
double listViewTop = y + 10.0;
y = GetHeight() - 15.0 - PlayButton->GetPreferredHeight();
@ -140,6 +149,7 @@ public:
PushButton* PlayButton = nullptr;
PushButton* ExitButton = nullptr;
ListView* GamesList = nullptr;
Dropdown* Choices = nullptr;
};
static std::vector<uint8_t> ReadAllBytes(const std::string& filename);
@ -156,13 +166,23 @@ std::vector<uint8_t> LoadWidgetData(const std::string& name)
return ReadAllBytes(name);
}
int example()
enum class Backend {
Default, Win32, SDL2, X11, Wayland
};
int example(Backend backend = Backend::Default)
{
WidgetTheme::SetTheme(std::make_unique<DarkWidgetTheme>());
// DisplayBackend::Set(DisplayBackend::TryCreateWin32());
// DisplayBackend::Set(DisplayBackend::TryCreateSDL2());
DisplayBackend::Set(DisplayBackend::TryCreateBackend());
// just for testing backends
switch (backend)
{
case Backend::Default: DisplayBackend::Set(DisplayBackend::TryCreateBackend()); break;
case Backend::Win32: DisplayBackend::Set(DisplayBackend::TryCreateWin32()); break;
case Backend::SDL2: DisplayBackend::Set(DisplayBackend::TryCreateSDL2()); break;
case Backend::X11: DisplayBackend::Set(DisplayBackend::TryCreateX11()); break;
case Backend::Wayland: DisplayBackend::Set(DisplayBackend::TryCreateWayland()); break;
}
#if 1
auto launcher = new LauncherWindow();
@ -275,9 +295,20 @@ static std::vector<uint8_t> ReadAllBytes(const std::string& filename)
return buffer;
}
int main()
int main(int argc, const char** argv)
{
example();
std::string backendStr = argc > 1? argv[1]: "";
std::transform(backendStr.begin(), backendStr.end(), backendStr.begin(),
[](unsigned char c){ return std::tolower(c); });
Backend backend = Backend::Default;
if (backendStr == "sdl2") backend = Backend::SDL2;
if (backendStr == "x11") backend = Backend::X11;
if (backendStr == "wayland") backend = Backend::Wayland;
if (backendStr == "win32") backend = Backend::Win32; // lol
example(backend);
}
#endif

View file

@ -4,6 +4,7 @@
#include <memory>
#include <variant>
#include <unordered_map>
#include <unordered_set>
#include "canvas.h"
#include "rect.h"
#include "colorf.h"
@ -11,6 +12,7 @@
class Canvas;
class Timer;
class Dropdown;
enum class WidgetType
{
@ -19,6 +21,13 @@ enum class WidgetType
Popup
};
enum class WidgetEvent
{
VisibilityChange,
// TODO: add more events
};
class Widget : DisplayWindowHost
{
public:
@ -93,6 +102,9 @@ public:
void ShowNormal();
void Hide();
void Subscribe(Widget* subscriber);
void Unsubscribe(Widget* subscriber);
void ActivateWindow();
void Close();
@ -178,6 +190,8 @@ protected:
virtual void OnLostFocus() { }
virtual void OnEnableChanged() { }
virtual void Notify(Widget* source, const WidgetEvent type) { };
private:
void DetachFromParent();
@ -201,6 +215,8 @@ private:
void OnWindowDeactivated() override;
void OnWindowDpiScaleChanged() override;
void NotifySubscribers(const WidgetEvent type);
WidgetType Type = {};
Widget* ParentObj = nullptr;
@ -234,8 +250,12 @@ private:
Widget(const Widget&) = delete;
Widget& operator=(const Widget&) = delete;
std::unordered_set<Widget*> Subscribers;
std::unordered_set<Widget*> Subscriptions;
friend class Timer;
friend class OpenFileDialog;
friend class OpenFolderDialog;
friend class SaveFileDialog;
friend class Dropdown;
};

View file

@ -0,0 +1,58 @@
#pragma once
#include <string>
#include <vector>
#include <functional>
#include "../../core/widget.h"
#include "../../widgets/listview/listview.h"
class Dropdown : public Widget
{
public:
Dropdown(Widget* parent);
void AddItem(const std::string& text);
void RemoveItem(int index);
void ClearItems();
void SetMaxDisplayItems(int items);
void SetDropdownDirection(bool below);
int GetSelectedItem() const { return selectedItem; }
void SetSelectedItem(int index);
std::string GetSelectedText() const;
std::string GetText() const { return text; }
double GetPreferredHeight() const;
double GetPreferredWidth() const;
std::function<void(int)> OnChanged;
protected:
void OnPaint(Canvas* canvas) override;
bool OnMouseDown(const Point& pos, InputKey key) override;
void OnKeyDown(InputKey key) override;
void OnGeometryChanged() override;
void OnLostFocus() override;
void Notify(Widget* source, const WidgetEvent type) override;
private:
void OpenDropdown();
void CloseDropdown();
void OnDropdownActivated();
size_t GetDisplayItems();
std::vector<std::string> items;
int selectedItem = -1;
std::string text;
bool dropdownOpen = false;
Widget* dropdown = nullptr;
ListView* listView = nullptr;
int maxDisplayItems = 0;
bool dropdownDirection = true;
};

View file

@ -22,6 +22,10 @@ public:
void SetSelectedItem(int index);
void ScrollToItem(int index);
double GetPreferredWidth();
double GetPreferredHeight();
double GetMinimumHeight() const;
void Activate();
std::function<void(int)> OnChanged;

View file

@ -151,6 +151,7 @@ DarkWidgetTheme::DarkWidgetTheme()
auto lineedit = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "lineedit");
auto textedit = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "textedit");
auto listview = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "listview");
auto dropdown = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "dropdown");
auto scrollbar = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "scrollbar");
auto tabbar = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "tabbar");
auto tabbar_tab = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "tabbar-tab");
@ -217,6 +218,18 @@ DarkWidgetTheme::DarkWidgetTheme()
listview->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100));
listview->SetColor("selection-color", Colorf::fromRgba8(100, 100, 100));
dropdown->SetDouble("noncontent-left", 5.0);
dropdown->SetDouble("noncontent-top", 5.0);
dropdown->SetDouble("noncontent-right", 5.0);
dropdown->SetDouble("noncontent-bottom", 5.0);
dropdown->SetColor("background-color", Colorf::fromRgba8(38, 38, 38));
dropdown->SetColor("border-left-color", Colorf::fromRgba8(100, 100, 100));
dropdown->SetColor("border-top-color", Colorf::fromRgba8(100, 100, 100));
dropdown->SetColor("border-right-color", Colorf::fromRgba8(100, 100, 100));
dropdown->SetColor("border-bottom-color", Colorf::fromRgba8(100, 100, 100));
dropdown->SetColor("selection-color", Colorf::fromRgba8(100, 100, 100));
dropdown->SetColor("arrow-color", Colorf::fromRgba8(100, 100, 100));
scrollbar->SetColor("track-color", Colorf::fromRgba8(33, 33, 33));
scrollbar->SetColor("thumb-color", Colorf::fromRgba8(58, 58, 58));
@ -290,6 +303,7 @@ LightWidgetTheme::LightWidgetTheme()
auto lineedit = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "lineedit");
auto textedit = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "textedit");
auto listview = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "listview");
auto dropdown = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "dropdown");
auto scrollbar = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "scrollbar");
auto tabbar = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "tabbar");
auto tabbar_tab = RegisterStyle(std::make_unique<BasicWidgetStyle>(widget), "tabbar-tab");
@ -353,6 +367,18 @@ LightWidgetTheme::LightWidgetTheme()
listview->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155));
listview->SetColor("selection-color", Colorf::fromRgba8(200, 200, 200));
dropdown->SetDouble("noncontent-left", 5.0);
dropdown->SetDouble("noncontent-top", 5.0);
dropdown->SetDouble("noncontent-right", 5.0);
dropdown->SetDouble("noncontent-bottom", 5.0);
dropdown->SetColor("background-color", Colorf::fromRgba8(230, 230, 230));
dropdown->SetColor("border-left-color", Colorf::fromRgba8(155, 155, 155));
dropdown->SetColor("border-top-color", Colorf::fromRgba8(155, 155, 155));
dropdown->SetColor("border-right-color", Colorf::fromRgba8(155, 155, 155));
dropdown->SetColor("border-bottom-color", Colorf::fromRgba8(155, 155, 155));
dropdown->SetColor("selection-color", Colorf::fromRgba8(200, 200, 200));
dropdown->SetColor("arrow-color", Colorf::fromRgba8(200, 200, 200));
scrollbar->SetColor("track-color", Colorf::fromRgba8(210, 210, 220));
scrollbar->SetColor("thumb-color", Colorf::fromRgba8(180, 180, 180));

View file

@ -1,4 +1,3 @@
#include "core/widget.h"
#include "core/timer.h"
#include "core/colorf.h"
@ -34,6 +33,12 @@ Widget::Widget(Widget* parent, WidgetType type, RenderAPI renderAPI) : Type(type
Widget::~Widget()
{
for (auto subscription: Subscriptions)
subscription->Subscribers.erase(this);
for (auto subscriber: Subscribers)
subscriber->Subscriptions.erase(this);
if (DispCanvas)
DispCanvas->detach();
@ -46,6 +51,24 @@ Widget::~Widget()
DetachFromParent();
}
void Widget::Subscribe(Widget* subscriber)
{
Subscribers.insert(subscriber);
subscriber->Subscriptions.insert(this);
}
void Widget::Unsubscribe(Widget* subscriber)
{
Subscribers.erase(subscriber);
subscriber->Subscriptions.erase(this);
}
void Widget::NotifySubscribers(const WidgetEvent type)
{
for (auto subscriber: Subscribers)
subscriber->Notify(this, type);
}
void Widget::SetCanvas(std::unique_ptr<Canvas> canvas)
{
if (DispWindow)
@ -206,10 +229,12 @@ void Widget::Show()
if (Type != WidgetType::Child)
{
DispWindow->Show();
NotifySubscribers(WidgetEvent::VisibilityChange);
}
else if (HiddenFlag)
{
HiddenFlag = false;
NotifySubscribers(WidgetEvent::VisibilityChange);
Update();
}
}
@ -260,11 +285,15 @@ void Widget::Hide()
if (Type != WidgetType::Child)
{
if (DispWindow)
{
DispWindow->Hide();
NotifySubscribers(WidgetEvent::VisibilityChange);
}
}
else if (!HiddenFlag)
{
HiddenFlag = true;
NotifySubscribers(WidgetEvent::VisibilityChange);
Update();
}
}

View file

@ -0,0 +1,283 @@
#include <algorithm>
#include "widgets/dropdown/dropdown.h"
#include "core/widget.h"
Dropdown::Dropdown(Widget* parent) : Widget(parent)
{
SetStyleClass("dropdown");
SetFocus();
// Subscribe to all parent widgets to receive notifications.
for (Widget* p = Parent(); p; p = p->Parent())
p->Subscribe(this);
}
void Dropdown::Notify(Widget* source, const WidgetEvent type)
{
if (type != WidgetEvent::VisibilityChange) return;
if (!dropdownOpen) return;
for (Widget* p = this; p != nullptr; p = p->Parent())
{
if (p->IsHidden())
{
CloseDropdown();
return;
}
}
}
void Dropdown::AddItem(const std::string& text)
{
items.push_back(text);
if (selectedItem == -1 && !items.empty())
{
SetSelectedItem(0);
}
Update();
}
void Dropdown::RemoveItem(int index)
{
if (index < 0 || (size_t)index >= items.size())
return;
items.erase(items.begin() + index);
if ((int)items.size() == 0)
{
SetSelectedItem(-1);
}
else if (selectedItem == index)
{
SetSelectedItem(0);
}
else if (selectedItem > index)
{
SetSelectedItem(selectedItem - 1);
}
Update();
}
void Dropdown::ClearItems()
{
items.clear();
SetSelectedItem(-1);
Update();
}
void Dropdown::SetSelectedItem(int index)
{
if (index >= (int)items.size())
index = (int)items.size() - 1;
if (selectedItem != index)
{
selectedItem = index;
text = (selectedItem != -1) ? items[selectedItem] : "";
if (OnChanged)
OnChanged(selectedItem);
Update();
}
}
std::string Dropdown::GetSelectedText() const
{
if (selectedItem >= 0 && (size_t)selectedItem < items.size())
return items[selectedItem];
return {};
}
double Dropdown::GetPreferredHeight() const
{
return 20.0 + 2*5.0; // Text plus top/bottom padding
}
double Dropdown::GetPreferredWidth() const
{
Canvas* canvas = GetCanvas();
double maxWidth = 0.0;
for (const auto& item : items)
{
auto width = canvas->measureText(item).width;
if (width > maxWidth)
{
maxWidth = width;
}
}
return maxWidth + 2*10.0 + 16.0; // content, pad l/r, scrollbar from dropdown
}
void Dropdown::OnPaint(Canvas* canvas)
{
Colorf textColor = GetStyleColor("color");
Colorf arrowColor = GetStyleColor("arrow-color");
double w = GetWidth();
double h = GetHeight();
auto vtp = canvas->verticalTextAlign();
double textY = (h-(vtp.bottom-vtp.top))/2.0 + vtp.baseline;
canvas->drawText(Point(7.0, textY), textColor, text);
double arrowS = 8.0;
double arrowX = w - 3.0; // rightmost point, aligned with scrollbar
double arrowY = (h-arrowS)/2;
Point p1(arrowX, arrowY);
Point p2(arrowX - arrowS, arrowY);
Point p3(arrowX - arrowS/2, arrowY + arrowS);
canvas->line(p1, p2, arrowColor);
canvas->line(p2, p3, arrowColor);
canvas->line(p3, p1, arrowColor);
}
bool Dropdown::OnMouseDown(const Point& pos, InputKey key)
{
if (key == InputKey::LeftMouse)
{
if (dropdownOpen)
{
// this set focus call will close the popup
SetFocus();
return true;
}
else
{
SetFocus();
OpenDropdown();
return true;
}
}
return false;
}
void Dropdown::OnKeyDown(InputKey key)
{
if (key == InputKey::Down)
{
if (selectedItem + 1 < (int)items.size())
SetSelectedItem(selectedItem + 1);
}
else if (key == InputKey::Up)
{
if (selectedItem > 0)
SetSelectedItem(selectedItem - 1);
}
else if (key == InputKey::Enter || key == InputKey::Space)
{
if (dropdownOpen)
CloseDropdown();
else
OpenDropdown();
}
}
void Dropdown::SetMaxDisplayItems(int items)
{
maxDisplayItems = items;
}
size_t Dropdown::GetDisplayItems()
{
return (maxDisplayItems < 1)
? std::max<size_t>(items.size(), 1ul)
: std::min<size_t>(items.size(), (size_t)maxDisplayItems);
}
void Dropdown::SetDropdownDirection(bool below)
{
dropdownDirection = below;
}
void Dropdown::OnGeometryChanged()
{
if (dropdownOpen)
{
Point pos = MapToGlobal(Point(0.0, 0.0));
double width = GetWidth() + GetNoncontentLeft() + GetNoncontentRight();
double innerH = GetDisplayItems() * 25.0 + 10.0;
double outerH = GetHeight();
pos.x -= GetNoncontentLeft();
// adjust to be above/below box
if (dropdownDirection)
pos.y += outerH + GetNoncontentBottom() - 1.0;
else
pos.y -= innerH + GetNoncontentTop() - 1.0;
dropdown->SetFrameGeometry(pos.x, pos.y, width, innerH);
}
}
void Dropdown::OnLostFocus()
{
if (!dropdownOpen) return;
// Check if the mouse is over the dropdown itself or one of its children
auto hover = Window()->HoverWidget;
if (hover && (hover == dropdown || dropdown->IsParent(hover))) return;
CloseDropdown();
}
void Dropdown::OpenDropdown()
{
if (dropdownOpen || items.empty()) return;
dropdownOpen = true;
dropdown = new Widget(Window());
listView = new ListView(dropdown);
for (const auto& item : items)
{
listView->AddItem(item);
}
listView->SetSelectedItem(selectedItem);
listView->OnActivated = [=]() { OnDropdownActivated(); };
listView->OnChanged = [=](int index) { OnDropdownActivated(); };
listView->SetFrameGeometry(
0,
0,
GetWidth() + GetNoncontentLeft() + GetNoncontentRight(),
GetDisplayItems() * 25.0 + 10.0
);
OnGeometryChanged();
dropdown->Show();
listView->ScrollToItem(selectedItem);
}
void Dropdown::CloseDropdown()
{
if (!dropdownOpen || !dropdown) return;
dropdown->Close();
dropdown = nullptr;
listView = nullptr;
dropdownOpen = false;
Update();
}
void Dropdown::OnDropdownActivated()
{
if (listView)
{
SetSelectedItem(listView->GetSelectedItem());
}
CloseDropdown();
SetFocus();
}

View file

@ -1,4 +1,3 @@
#include "widgets/listview/listview.h"
#include "widgets/scrollbar/scrollbar.h"
@ -284,3 +283,40 @@ void ListView::OnKeyDown(InputKey key)
}
}
}
double ListView::GetPreferredWidth()
{
double total = 0.0;
for (double width : columnwidths)
{
if (width > 0.0) total += width;
}
if (total == 0.0)
{
auto canvas = GetCanvas();
for (const auto& row : items)
{
double wRow = 0.0;
for (const auto& cell : row)
{
wRow += canvas->measureText(cell).width;
}
if (wRow > total) total = wRow;
}
}
return total + 10.0*2 + scrollbar->GetPreferredWidth();
}
double ListView::GetPreferredHeight()
{
return items.size()*20.0 + 10.0*2; // Items plus top/bottom padding
}
double ListView::GetMinimumHeight() const
{
return 20.0 + 10.0*2; // One item plus top/bottom padding
}

16
tools/update-subtrees.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/bash
pushd "$(git rev-parse --show-toplevel)" >/dev/null
pull() {
if [[ "${1}" == "${2}" || "${1}" == "all" ]]
then
echo "fetching ${2}"
git subtree pull --prefix="${3}" "https://github.com/${4}" "${5}" --squash || exit
else
echo "${2} not specified, skipping"
fi
}
pull "$1" 'zwidget' 'libraries/ZWidget' 'dpjudas/ZWidget' 'master'
pull "$1" 'zmusic' 'libraries/ZMusic' 'ZDoom/ZMusic' 'master'

View file

@ -1,3 +0,0 @@
#!/bin/bash
git subtree pull --prefix=libraries/ZWidget https://github.com/dpjudas/ZWidget master --squash
git subtree pull --prefix=libraries/ZMusic https://github.com/ZDoom/ZMusic master --squash