LibWebView+UI: Add structures to hold context menu and action data

We currently duplicate a lot of code to handle application/context menus
and actions. The goal here is to hold the data for the menus and actions
in LibWebView. Each UI will then be able to generate menus from the data
on-the-fly.

The structures added here are meant to support generic and checkable
actions, action groups, submenus, etc.
This commit is contained in:
Timothy Flynn 2025-08-30 13:11:21 -04:00 committed by Tim Flynn
commit a5be0f0a18
Notes: github-actions[bot] 2025-09-11 18:25:23 +00:00
10 changed files with 619 additions and 1 deletions

View file

@ -11,6 +11,7 @@ set(SOURCES
DOMNodeProperties.cpp DOMNodeProperties.cpp
HeadlessWebView.cpp HeadlessWebView.cpp
HelperProcess.cpp HelperProcess.cpp
Menu.cpp
Mutation.cpp Mutation.cpp
Plugins/FontPlugin.cpp Plugins/FontPlugin.cpp
Plugins/ImageCodecPlugin.cpp Plugins/ImageCodecPlugin.cpp

View file

@ -12,10 +12,12 @@
namespace WebView { namespace WebView {
class Action;
class Application; class Application;
class Autocomplete; class Autocomplete;
class CookieJar; class CookieJar;
class Database; class Database;
class Menu;
class OutOfProcessWebView; class OutOfProcessWebView;
class ProcessManager; class ProcessManager;
class Settings; class Settings;

View file

@ -0,0 +1,126 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWebView/Menu.h>
namespace WebView {
NonnullRefPtr<Action> Action::create(Variant<StringView, String> text, ActionID id, Function<void()> action)
{
return adopt_ref(*new Action { move(text), id, move(action) });
}
NonnullRefPtr<Action> Action::create_checkable(Variant<StringView, String> text, ActionID id, Function<void()> action)
{
auto checkable = create(move(text), id, move(action));
checkable->m_checked = false;
return checkable;
}
void Action::set_text(Variant<StringView, String> text)
{
if (text.visit([&](auto const& text) { return text == this->text(); }))
return;
m_text = move(text);
for (auto& observer : m_observers)
observer->on_text_changed(*this);
}
void Action::set_tooltip(StringView tooltip)
{
if (m_tooltip == tooltip)
return;
m_tooltip = tooltip;
for (auto& observer : m_observers)
observer->on_tooltip_changed(*this);
}
void Action::set_enabled(bool enabled)
{
if (m_enabled == enabled)
return;
m_enabled = enabled;
for (auto& observer : m_observers)
observer->on_enabled_state_changed(*this);
}
void Action::set_visible(bool visible)
{
if (m_visible == visible)
return;
m_visible = visible;
for (auto& observer : m_observers)
observer->on_visible_state_changed(*this);
}
void Action::set_checked(bool checked)
{
set_checked_internal(checked);
if (auto group = m_group.strong_ref()) {
group->for_each_action([&](Action& action) {
if (action.is_checkable() && &action != this)
action.set_checked_internal(false);
});
}
}
void Action::set_checked_internal(bool checked)
{
VERIFY(is_checkable());
if (m_checked == checked)
return;
m_checked = checked;
for (auto& observer : m_observers)
observer->on_checked_state_changed(*this);
}
void Action::add_observer(NonnullOwnPtr<Observer> observer)
{
observer->on_text_changed(*this);
if (m_tooltip.has_value())
observer->on_tooltip_changed(*this);
observer->on_enabled_state_changed(*this);
observer->on_visible_state_changed(*this);
if (is_checkable())
observer->on_checked_state_changed(*this);
m_observers.append(move(observer));
}
void Action::remove_observer(Observer const& observer)
{
m_observers.remove_first_matching([&](auto const& candidate) {
return candidate.ptr() == &observer;
});
}
NonnullRefPtr<Menu> Menu::create(StringView name)
{
return adopt_ref(*new Menu { name });
}
NonnullRefPtr<Menu> Menu::create_group(StringView name)
{
auto menu = create(name);
menu->m_is_group = true;
return menu;
}
void Menu::add_action(NonnullRefPtr<Action> action)
{
if (m_is_group)
action->set_group({}, *this);
m_items.append(move(action));
}
}

142
Libraries/LibWebView/Menu.h Normal file
View file

@ -0,0 +1,142 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <AK/RefCounted.h>
#include <AK/String.h>
#include <AK/StringView.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <AK/WeakPtr.h>
#include <AK/Weakable.h>
#include <LibGfx/Point.h>
#include <LibWebView/Forward.h>
namespace WebView {
enum class ActionID {
};
class WEBVIEW_API Action
: public RefCounted<Action>
, public Weakable<Action> {
public:
static NonnullRefPtr<Action> create(Variant<StringView, String> text, ActionID id, Function<void()> action);
static NonnullRefPtr<Action> create_checkable(Variant<StringView, String> text, ActionID id, Function<void()> action);
void activate() { m_action(); }
StringView text() const
{
return m_text.visit([](auto const& text) -> StringView { return text; });
}
void set_text(Variant<StringView, String>);
StringView tooltip() const { return *m_tooltip; }
void set_tooltip(StringView);
ActionID id() const { return m_id; }
bool enabled() const { return m_enabled; }
void set_enabled(bool);
bool visible() const { return m_visible; }
void set_visible(bool);
bool is_checkable() const { return m_checked.has_value(); }
bool checked() const { return *m_checked; }
void set_checked(bool);
struct Observer {
virtual ~Observer() = default;
virtual void on_text_changed(Action&) { }
virtual void on_tooltip_changed(Action&) { }
virtual void on_enabled_state_changed(Action&) { }
virtual void on_visible_state_changed(Action&) { }
virtual void on_checked_state_changed(Action&) { }
};
void add_observer(NonnullOwnPtr<Observer>);
void remove_observer(Observer const& observer);
void set_group(Badge<Menu>, Menu& group) { m_group = group; }
private:
Action(Variant<StringView, String> text, ActionID id, Function<void()> action)
: m_text(move(text))
, m_id(id)
, m_action(move(action))
{
}
void set_checked_internal(bool checked);
Variant<StringView, String> m_text;
Optional<StringView> m_tooltip;
ActionID m_id;
bool m_enabled { true };
bool m_visible { true };
Optional<bool> m_checked;
Function<void()> m_action;
Vector<NonnullOwnPtr<Observer>, 1> m_observers;
WeakPtr<Menu> m_group;
};
struct WEBVIEW_API Separator { };
class WEBVIEW_API Menu
: public RefCounted<Menu>
, public Weakable<Menu> {
public:
using MenuItem = Variant<NonnullRefPtr<Action>, NonnullRefPtr<Menu>, Separator>;
static NonnullRefPtr<Menu> create(StringView name);
static NonnullRefPtr<Menu> create_group(StringView name);
void add_action(NonnullRefPtr<Action> action);
void add_submenu(NonnullRefPtr<Menu> submenu) { m_items.append(move(submenu)); }
void add_separator() { m_items.append(Separator {}); }
StringView title() const { return m_title; }
Span<MenuItem> items() { return m_items; }
ReadonlySpan<MenuItem> items() const { return m_items; }
template<typename Callback>
void for_each_action(Callback const& callback)
{
for (auto& item : m_items) {
item.visit(
[&](NonnullRefPtr<Action>& action) { callback(*action); },
[&](NonnullRefPtr<Menu>& submenu) { submenu->for_each_action(callback); },
[&](Separator) {});
}
}
Function<void(Gfx::IntPoint)> on_activation;
private:
explicit Menu(StringView title)
: m_title(title)
{
}
StringView m_title;
Vector<MenuItem> m_items;
bool m_is_group { false };
};
}

View file

@ -7,6 +7,7 @@ add_library(ladybird_impl STATIC
Interface/LadybirdWebView.mm Interface/LadybirdWebView.mm
Interface/LadybirdWebViewBridge.cpp Interface/LadybirdWebViewBridge.cpp
Interface/LadybirdWebViewWindow.mm Interface/LadybirdWebViewWindow.mm
Interface/Menu.mm
Interface/Palette.mm Interface/Palette.mm
Interface/SearchPanel.mm Interface/SearchPanel.mm
Interface/Tab.mm Interface/Tab.mm

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <Interface/Menu.h>
#include <LibWebView/Menu.h>
#import <Cocoa/Cocoa.h>
@class LadybirdWebView;
namespace Ladybird {
NSMenu* create_application_menu(WebView::Menu&);
NSMenu* create_context_menu(LadybirdWebView*, WebView::Menu&);
NSMenuItem* create_application_menu_item(WebView::Action&);
NSButton* create_application_button(WebView::Action&, NSImageName);
}

168
UI/AppKit/Interface/Menu.mm Normal file
View file

@ -0,0 +1,168 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#import <Interface/Event.h>
#import <Interface/LadybirdWebView.h>
#import <Interface/Menu.h>
#import <Utilities/Conversions.h>
#import <objc/runtime.h>
@interface ActionExecutor : NSObject
{
WeakPtr<WebView::Action> m_action;
}
@end
@implementation ActionExecutor
+ (instancetype)attachToNativeControl:(WebView::Action const&)action
control:(id)control
{
auto* executor = [[ActionExecutor alloc] init];
[control setAction:@selector(execute:)];
[control setTarget:executor];
static char executor_key = 0;
objc_setAssociatedObject(control, &executor_key, executor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
executor->m_action = action.make_weak_ptr();
return executor;
}
- (void)execute:(id)sender
{
auto action = m_action.strong_ref();
if (!action)
return;
if (action->is_checkable())
action->set_checked(!action->checked());
action->activate();
}
@end
namespace Ladybird {
class ActionObserver final : public WebView::Action::Observer {
public:
static NonnullOwnPtr<ActionObserver> create(WebView::Action& action, id control)
{
return adopt_own(*new ActionObserver(action, control));
}
virtual void on_text_changed(WebView::Action& action) override
{
if ([m_control isKindOfClass:[NSButton class]] && [m_control image] != nil)
[m_control setToolTip:string_to_ns_string(action.text())];
else
[m_control setTitle:string_to_ns_string(action.text())];
}
virtual void on_tooltip_changed(WebView::Action& action) override
{
[m_control setToolTip:string_to_ns_string(action.tooltip())];
}
virtual void on_enabled_state_changed(WebView::Action& action) override
{
[m_control setEnabled:action.enabled()];
}
virtual void on_visible_state_changed(WebView::Action& action) override
{
[m_control setHidden:!action.visible()];
}
virtual void on_checked_state_changed(WebView::Action& action) override
{
[m_control setState:action.checked() ? NSControlStateValueOn : NSControlStateValueOff];
}
private:
ActionObserver(WebView::Action& action, id control)
: m_control(control)
{
[ActionExecutor attachToNativeControl:action control:control];
}
__weak id m_control { nil };
};
static void initialize_native_control(WebView::Action& action, id control)
{
action.add_observer(ActionObserver::create(action, control));
}
static void add_items_to_menu(NSMenu* menu, Span<WebView::Menu::MenuItem> menu_items)
{
for (auto& menu_item : menu_items) {
menu_item.visit(
[&](NonnullRefPtr<WebView::Action>& action) {
[menu addItem:create_application_menu_item(action)];
},
[&](NonnullRefPtr<WebView::Menu> const& submenu) {
auto* application_submenu = [[NSMenu alloc] init];
add_items_to_menu(application_submenu, submenu->items());
auto* item = [[NSMenuItem alloc] initWithTitle:string_to_ns_string(submenu->title())
action:nil
keyEquivalent:@""];
[item setSubmenu:application_submenu];
[menu addItem:item];
},
[&](WebView::Separator) {
[menu addItem:[NSMenuItem separatorItem]];
});
}
}
NSMenu* create_application_menu(WebView::Menu& menu)
{
auto* application_menu = [[NSMenu alloc] initWithTitle:string_to_ns_string(menu.title())];
add_items_to_menu(application_menu, menu.items());
return application_menu;
}
NSMenu* create_context_menu(LadybirdWebView* view, WebView::Menu& menu)
{
auto* application_menu = create_application_menu(menu);
__weak LadybirdWebView* weak_view = view;
__weak NSMenu* weak_application_menu = application_menu;
menu.on_activation = [weak_view, weak_application_menu](Gfx::IntPoint position) {
LadybirdWebView* view = weak_view;
NSMenu* application_menu = weak_application_menu;
if (view && application_menu) {
auto* event = create_context_menu_mouse_event(view, position);
[NSMenu popUpContextMenu:application_menu withEvent:event forView:view];
}
};
return application_menu;
}
NSMenuItem* create_application_menu_item(WebView::Action& action)
{
auto* item = [[NSMenuItem alloc] init];
initialize_native_control(action, item);
return item;
}
NSButton* create_application_button(WebView::Action& action, NSImageName image)
{
auto* button = [[NSButton alloc] init];
if (image)
[button setImage:[NSImage imageNamed:image]];
initialize_native_control(action, button);
return button;
}
}

View file

@ -6,11 +6,12 @@ target_sources(ladybird PRIVATE
FindInPageWidget.cpp FindInPageWidget.cpp
Icon.cpp Icon.cpp
LocationEdit.cpp LocationEdit.cpp
Menu.cpp
Settings.cpp Settings.cpp
StringUtils.cpp
Tab.cpp Tab.cpp
TabBar.cpp TabBar.cpp
TVGIconEngine.cpp TVGIconEngine.cpp
StringUtils.cpp
WebContentView.cpp WebContentView.cpp
ladybird.qrc ladybird.qrc
) )

130
UI/Qt/Menu.cpp Normal file
View file

@ -0,0 +1,130 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <UI/Qt/Icon.h>
#include <UI/Qt/Menu.h>
#include <UI/Qt/StringUtils.h>
#include <UI/Qt/WebContentView.h>
#include <QAction>
#include <QMenu>
#include <QPointer>
#include <QWidget>
namespace Ladybird {
class ActionObserver final : public WebView::Action::Observer {
public:
static NonnullOwnPtr<ActionObserver> create(WebView::Action& action, QAction& qaction)
{
return adopt_own(*new ActionObserver(action, qaction));
}
virtual void on_text_changed(WebView::Action& action) override
{
if (m_action)
m_action->setText(qstring_from_ak_string(action.text()));
}
virtual void on_tooltip_changed(WebView::Action& action) override
{
if (m_action)
m_action->setToolTip(qstring_from_ak_string(action.tooltip()));
}
virtual void on_enabled_state_changed(WebView::Action& action) override
{
if (m_action)
m_action->setEnabled(action.enabled());
}
virtual void on_visible_state_changed(WebView::Action& action) override
{
if (m_action)
m_action->setVisible(action.visible());
}
virtual void on_checked_state_changed(WebView::Action& action) override
{
if (m_action)
m_action->setChecked(action.checked());
}
private:
ActionObserver(WebView::Action& action, QAction& qaction)
: m_action(&qaction)
{
QObject::connect(m_action, &QAction::triggered, [weak_action = action.make_weak_ptr()](bool checked) {
if (auto action = weak_action.strong_ref()) {
if (action->is_checkable())
action->set_checked(checked);
action->activate();
}
});
QObject::connect(m_action->parent(), &QObject::destroyed, [this, weak_action = action.make_weak_ptr()]() {
if (auto action = weak_action.strong_ref())
action->remove_observer(*this);
});
}
QPointer<QAction> m_action;
};
static void initialize_native_control(WebView::Action& action, QAction& qaction)
{
if (action.is_checkable())
qaction.setCheckable(true);
action.add_observer(ActionObserver::create(action, qaction));
}
static void add_items_to_menu(QMenu& menu, QWidget& parent, Span<WebView::Menu::MenuItem> menu_items)
{
for (auto& menu_item : menu_items) {
menu_item.visit(
[&](NonnullRefPtr<WebView::Action>& action) {
auto* qaction = create_application_action(parent, action);
menu.addAction(qaction);
},
[&](NonnullRefPtr<WebView::Menu> const& submenu) {
auto* qsubmenu = new QMenu(qstring_from_ak_string(submenu->title()), &menu);
add_items_to_menu(*qsubmenu, parent, submenu->items());
menu.addMenu(qsubmenu);
},
[&](WebView::Separator) {
menu.addSeparator();
});
}
}
QMenu* create_application_menu(QWidget& parent, WebView::Menu& menu)
{
auto* application_menu = new QMenu(qstring_from_ak_string(menu.title()), &parent);
add_items_to_menu(*application_menu, parent, menu.items());
return application_menu;
}
QMenu* create_context_menu(QWidget& parent, WebContentView& view, WebView::Menu& menu)
{
auto* application_menu = create_application_menu(parent, menu);
menu.on_activation = [view = QPointer { &view }, application_menu = QPointer { application_menu }](Gfx::IntPoint position) {
if (view && application_menu)
application_menu->exec(view->map_point_to_global_position(position));
};
return application_menu;
}
QAction* create_application_action(QWidget& parent, WebView::Action& action)
{
auto* qaction = new QAction(&parent);
initialize_native_control(action, *qaction);
return qaction;
}
}

23
UI/Qt/Menu.h Normal file
View file

@ -0,0 +1,23 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWebView/Menu.h>
class QAction;
class QMenu;
class QWidget;
namespace Ladybird {
class WebContentView;
QMenu* create_application_menu(QWidget& parent, WebView::Menu&);
QMenu* create_context_menu(QWidget& parent, WebContentView&, WebView::Menu&);
QAction* create_application_action(QWidget& parent, WebView::Action&);
}