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

@ -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
}