LibWebView+UI: Generate the zoom menu

This commit is contained in:
Timothy Flynn 2025-09-10 07:23:15 -04:00 committed by Tim Flynn
commit 9684e6dbc5
Notes: github-actions[bot] 2025-09-11 18:24:52 +00:00
20 changed files with 104 additions and 210 deletions

View file

@ -668,6 +668,22 @@ void Application::initialize_actions()
view->get_source();
});
m_zoom_menu = Menu::create_group("Zoom"sv);
m_zoom_menu->add_action(Action::create("Zoom In"sv, ActionID::ZoomIn, [this]() {
if (auto view = active_web_view(); view.has_value())
view->zoom_in();
}));
m_zoom_menu->add_action(Action::create("Zoom Out"sv, ActionID::ZoomOut, [this]() {
if (auto view = active_web_view(); view.has_value())
view->zoom_out();
}));
m_reset_zoom_action = Action::create("Reset Zoom"sv, ActionID::ResetZoom, [this]() {
if (auto view = active_web_view(); view.has_value())
view->reset_zoom();
});
m_zoom_menu->add_action(*m_reset_zoom_action);
auto set_color_scheme = [this](auto color_scheme) {
return [this, color_scheme]() {
m_color_scheme = color_scheme;

View file

@ -77,6 +77,9 @@ public:
Action& select_all_action() { return *m_select_all_action; }
Action& view_source_action() { return *m_view_source_action; }
Menu& zoom_menu() { return *m_zoom_menu; }
Action& reset_zoom_action() { return *m_reset_zoom_action; }
Menu& color_scheme_menu() { return *m_color_scheme_menu; }
Menu& contrast_menu() { return *m_contrast_menu; }
Menu& motion_menu() { return *m_motion_menu; }
@ -176,6 +179,9 @@ private:
RefPtr<Action> m_select_all_action;
RefPtr<Action> m_view_source_action;
RefPtr<Menu> m_zoom_menu;
RefPtr<Action> m_reset_zoom_action;
RefPtr<Menu> m_color_scheme_menu;
Web::CSS::PreferredColorScheme m_color_scheme { Web::CSS::PreferredColorScheme::Auto };

View file

@ -170,7 +170,7 @@ void HeadlessWebView::initialize_client(CreateNewClient create_new_client)
void HeadlessWebView::update_zoom()
{
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level);
ViewImplementation::update_zoom();
client().async_set_viewport_size(m_client_state.page_index, m_viewport_size);
}

View file

@ -53,6 +53,11 @@ enum class ActionID {
ToggleMediaControlsState,
ToggleMediaLoopState,
ZoomIn,
ZoomOut,
ResetZoom,
ResetZoomViaToolbar,
PreferredColorScheme,
PreferredContrast,
PreferredMotion,

View file

@ -553,6 +553,18 @@ void ViewImplementation::did_allocate_iosurface_backing_stores(i32 front_id, Cor
}
#endif
void ViewImplementation::update_zoom()
{
if (m_zoom_level != 1.0f) {
m_reset_zoom_action->set_text(MUST(String::formatted("{}%", round_to<int>(m_zoom_level * 100))));
m_reset_zoom_action->set_visible(true);
} else {
m_reset_zoom_action->set_visible(false);
}
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level);
}
void ViewImplementation::handle_resize()
{
client().async_set_viewport_size(page_id(), this->viewport_size());
@ -807,6 +819,12 @@ void ViewImplementation::initialize_context_menus()
m_navigate_back_action->set_enabled(false);
m_navigate_forward_action->set_enabled(false);
m_reset_zoom_action = Action::create("100%"sv, ActionID::ResetZoomViaToolbar, [this]() {
reset_zoom();
});
m_reset_zoom_action->set_tooltip("Reset zoom level"sv);
m_reset_zoom_action->set_visible(false);
m_search_selected_text_action = Action::create("Search Selected Text"sv, ActionID::SearchSelectedText, [this]() {
auto const& search_engine = Application::settings().search_engine();
if (!search_engine.has_value())

View file

@ -71,7 +71,7 @@ public:
void zoom_out();
void set_zoom(double zoom_level);
void reset_zoom();
float zoom_level() const { return m_zoom_level; }
float device_pixel_ratio() const { return m_device_pixel_ratio; }
double maximum_frames_per_second() const { return m_maximum_frames_per_second; }
@ -219,7 +219,6 @@ public:
Function<void(String const&)> on_test_finish;
Function<void(double milliseconds)> on_set_test_timeout;
Function<void(JsonValue)> on_reference_test_metadata;
Function<void(double factor)> on_set_browser_zoom;
Function<void(size_t current_match_index, Optional<size_t> const& total_match_count)> on_find_in_page;
Function<void(Gfx::Color)> on_theme_color_change;
Function<void(Web::Clipboard::SystemClipboardRepresentation, String const&)> on_insert_clipboard_entry;
@ -240,6 +239,7 @@ public:
Action& navigate_back_action() { return *m_navigate_back_action; }
Action& navigate_forward_action() { return *m_navigate_forward_action; }
Action& reset_zoom_action() { return *m_reset_zoom_action; }
virtual Web::DevicePixelSize viewport_size() const = 0;
virtual Gfx::IntPoint to_content_position(Gfx::IntPoint widget_position) const = 0;
@ -255,7 +255,8 @@ protected:
WebContentClient& client();
WebContentClient const& client() const;
u64 page_id() const;
virtual void update_zoom() = 0;
virtual void update_zoom();
void handle_resize();
@ -308,6 +309,8 @@ protected:
RefPtr<Action> m_navigate_back_action;
RefPtr<Action> m_navigate_forward_action;
RefPtr<Action> m_reset_zoom_action;
RefPtr<Action> m_search_selected_text_action;
Optional<String> m_search_text;

View file

@ -142,10 +142,8 @@ void WebContentClient::did_receive_reference_test_metadata(u64 page_id, JsonValu
void WebContentClient::did_set_browser_zoom(u64 page_id, double factor)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_set_browser_zoom)
view->on_set_browser_zoom(factor);
}
if (auto view = view_for_page_id(page_id); view.has_value())
view->set_zoom(factor);
}
void WebContentClient::did_find_in_page(u64 page_id, size_t current_match_index, Optional<size_t> total_match_count)

View file

@ -349,10 +349,6 @@ static void run_dump_test(TestWebView& view, Test& test, URL::URL const& url, in
timer->start(milliseconds);
};
view.on_set_browser_zoom = [&view](double factor) {
view.set_zoom(factor);
};
view.load(url);
timer->start();
}
@ -491,10 +487,6 @@ static void run_ref_test(TestWebView& view, Test& test, URL::URL const& url, int
timer->start(milliseconds);
};
view.on_set_browser_zoom = [&view](double factor) {
view.set_zoom(factor);
};
view.load(url);
timer->start();
}

View file

@ -370,6 +370,12 @@
auto* menu = [[NSMenuItem alloc] init];
auto* submenu = [[NSMenu alloc] initWithTitle:@"View"];
auto* zoom_menu = Ladybird::create_application_menu(WebView::Application::the().zoom_menu());
auto* zoom_menu_item = [[NSMenuItem alloc] initWithTitle:[zoom_menu title]
action:nil
keyEquivalent:@""];
[zoom_menu_item setSubmenu:zoom_menu];
auto* color_scheme_menu = Ladybird::create_application_menu(WebView::Application::the().color_scheme_menu());
auto* color_scheme_menu_item = [[NSMenuItem alloc] initWithTitle:[color_scheme_menu title]
action:nil
@ -388,26 +394,11 @@
keyEquivalent:@""];
[motion_menu_item setSubmenu:motion_menu];
auto* zoom_menu = [[NSMenu alloc] init];
[zoom_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Zoom In"
action:@selector(zoomIn:)
keyEquivalent:@"+"]];
[zoom_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Zoom Out"
action:@selector(zoomOut:)
keyEquivalent:@"-"]];
[zoom_menu addItem:[[NSMenuItem alloc] initWithTitle:@"Actual Size"
action:@selector(resetZoom:)
keyEquivalent:@"0"]];
auto* zoom_menu_item = [[NSMenuItem alloc] initWithTitle:@"Zoom"
action:nil
keyEquivalent:@""];
[zoom_menu_item setSubmenu:zoom_menu];
[submenu addItem:zoom_menu_item];
[submenu addItem:[NSMenuItem separatorItem]];
[submenu addItem:color_scheme_menu_item];
[submenu addItem:contrast_menu_item];
[submenu addItem:motion_menu_item];
[submenu addItem:zoom_menu_item];
[submenu addItem:[NSMenuItem separatorItem]];
[menu setSubmenu:submenu];

View file

@ -69,9 +69,4 @@
- (void)findInPageNextMatch;
- (void)findInPagePreviousMatch;
- (void)zoomIn;
- (void)zoomOut;
- (void)resetZoom;
- (float)zoomLevel;
@end

View file

@ -205,26 +205,6 @@ struct HideCursor {
m_web_view_bridge->find_in_page_previous_match();
}
- (void)zoomIn
{
m_web_view_bridge->zoom_in();
}
- (void)zoomOut
{
m_web_view_bridge->zoom_out();
}
- (void)resetZoom
{
m_web_view_bridge->reset_zoom();
}
- (float)zoomLevel
{
return m_web_view_bridge->zoom_level();
}
#pragma mark - Private methods
static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_type)
@ -797,11 +777,6 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
[NSMenu popUpContextMenu:self.select_dropdown withEvent:event forView:self];
};
m_web_view_bridge->on_set_browser_zoom = [](double factor) {
(void)factor;
dbgln("FIXME: A test called `window.internals.setBrowserZoom()` which is not implemented in the AppKit UI");
};
m_web_view_bridge->on_restore_window = [weak_self]() {
LadybirdWebView* self = weak_self;
if (self == nil) {

View file

@ -99,7 +99,7 @@ Optional<WebViewBridge::Paintable> WebViewBridge::paintable()
void WebViewBridge::update_zoom()
{
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level);
WebView::ViewImplementation::update_zoom();
if (on_zoom_level_changed)
on_zoom_level_changed();

View file

@ -135,6 +135,16 @@ static void initialize_native_control(WebView::Action& action, id control)
[control setKeyEquivalent:@"u"];
break;
case WebView::ActionID::ZoomIn:
[control setKeyEquivalent:@"+"];
break;
case WebView::ActionID::ZoomOut:
[control setKeyEquivalent:@"-"];
break;
case WebView::ActionID::ResetZoom:
[control setKeyEquivalent:@"0"];
break;
default:
break;
}

View file

@ -151,24 +151,6 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
[self.window makeFirstResponder:[self tab].web_view];
}
- (void)zoomIn:(id)sender
{
[[[self tab] web_view] zoomIn];
[self updateZoomButton];
}
- (void)zoomOut:(id)sender
{
[[[self tab] web_view] zoomOut];
[self updateZoomButton];
}
- (void)resetZoom:(id)sender
{
[[[self tab] web_view] resetZoom];
[self updateZoomButton];
}
- (void)clearHistory
{
// FIXME: Reimplement clearing history using WebContent's history.
@ -257,17 +239,6 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
self.tab.titlebarAppearsTransparent = YES;
}
- (void)updateZoomButton
{
auto zoom_level = [[[self tab] web_view] zoomLevel];
auto* zoom_level_text = [NSString stringWithFormat:@"%d%%", round_to<int>(zoom_level * 100.0f)];
[self.zoom_toolbar_item setTitle:zoom_level_text];
auto zoom_button_hidden = zoom_level == 1.0 ? YES : NO;
[[self.zoom_toolbar_item view] setHidden:zoom_button_hidden];
}
#pragma mark - Properties
- (NSButton*)create_button:(NSImageName)image
@ -340,11 +311,7 @@ static NSString* const TOOLBAR_TAB_OVERVIEW_IDENTIFIER = @"ToolbarTabOverviewIde
- (NSToolbarItem*)zoom_toolbar_item
{
if (!_zoom_toolbar_item) {
auto* button = [NSButton buttonWithTitle:@"100%"
target:self
action:@selector(resetZoom:)];
[button setToolTip:@"Reset zoom level"];
[button setHidden:YES];
auto* button = Ladybird::create_application_button([[[self tab] web_view] view].reset_zoom_action(), nil);
_zoom_toolbar_item = [[NSToolbarItem alloc] initWithItemIdentifier:TOOLBAR_ZOOM_IDENTIFIER];
[_zoom_toolbar_item setView:button];

View file

@ -190,31 +190,7 @@ BrowserWindow::BrowserWindow(Vector<URL::URL> const& initial_urls, IsPopupWindow
view_menu->addSeparator();
m_zoom_menu = view_menu->addMenu("&Zoom");
auto* zoom_in_action = new QAction("Zoom &In", this);
zoom_in_action->setIcon(load_icon_from_uri("resource://icons/16x16/zoom-in.png"sv));
auto zoom_in_shortcuts = QKeySequence::keyBindings(QKeySequence::StandardKey::ZoomIn);
auto secondary_zoom_shortcut = QKeySequence(Qt::CTRL | Qt::Key_Equal);
if (!zoom_in_shortcuts.contains(secondary_zoom_shortcut))
zoom_in_shortcuts.append(AK::move(secondary_zoom_shortcut));
zoom_in_action->setShortcuts(zoom_in_shortcuts);
m_zoom_menu->addAction(zoom_in_action);
QObject::connect(zoom_in_action, &QAction::triggered, this, &BrowserWindow::zoom_in);
auto* zoom_out_action = new QAction("Zoom &Out", this);
zoom_out_action->setIcon(load_icon_from_uri("resource://icons/16x16/zoom-out.png"sv));
zoom_out_action->setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::ZoomOut));
m_zoom_menu->addAction(zoom_out_action);
QObject::connect(zoom_out_action, &QAction::triggered, this, &BrowserWindow::zoom_out);
auto* reset_zoom_action = new QAction("&Reset Zoom", this);
reset_zoom_action->setIcon(load_icon_from_uri("resource://icons/16x16/zoom-reset.png"sv));
reset_zoom_action->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
m_zoom_menu->addAction(reset_zoom_action);
QObject::connect(reset_zoom_action, &QAction::triggered, this, &BrowserWindow::reset_zoom);
view_menu->addMenu(create_application_menu(*view_menu, Application::the().zoom_menu()));
view_menu->addSeparator();
view_menu->addMenu(create_application_menu(*view_menu, Application::the().color_scheme_menu()));
@ -367,13 +343,6 @@ void BrowserWindow::devtools_enabled()
statusBar()->showMessage(qstring_from_ak_string(message));
}
void BrowserWindow::set_current_tab(Tab* tab)
{
m_current_tab = tab;
if (tab)
update_displayed_zoom_level();
}
Tab& BrowserWindow::new_tab_from_url(URL::URL const& url, Web::HTML::ActivateTab activate_tab)
{
auto& tab = create_new_tab(activate_tab);
@ -657,37 +626,6 @@ void BrowserWindow::open_previous_tab()
m_tabs_container->setCurrentIndex(next_index);
}
void BrowserWindow::zoom_in()
{
if (!m_current_tab)
return;
m_current_tab->view().zoom_in();
update_displayed_zoom_level();
}
void BrowserWindow::zoom_out()
{
if (!m_current_tab)
return;
m_current_tab->view().zoom_out();
update_displayed_zoom_level();
}
void BrowserWindow::reset_zoom()
{
if (!m_current_tab)
return;
m_current_tab->view().reset_zoom();
update_displayed_zoom_level();
}
void BrowserWindow::update_zoom_menu()
{
VERIFY(m_zoom_menu);
auto zoom_level_text = MUST(String::formatted("&Zoom ({}%)", round_to<int>(m_current_tab->view().zoom_level() * 100)));
m_zoom_menu->setTitle(qstring_from_ak_string(zoom_level_text));
}
void BrowserWindow::show_find_in_page()
{
if (!m_current_tab)
@ -696,13 +634,6 @@ void BrowserWindow::show_find_in_page()
m_current_tab->show_find_in_page();
}
void BrowserWindow::update_displayed_zoom_level()
{
VERIFY(m_current_tab);
update_zoom_menu();
m_current_tab->update_reset_zoom_button();
}
void BrowserWindow::set_window_rect(Optional<Web::DevicePixels> x, Optional<Web::DevicePixels> y, Optional<Web::DevicePixels> width, Optional<Web::DevicePixels> height)
{
x = x.value_or(0);
@ -750,11 +681,14 @@ void BrowserWindow::moveEvent(QMoveEvent* event)
void BrowserWindow::wheelEvent(QWheelEvent* event)
{
if (!m_current_tab)
return;
if ((event->modifiers() & Qt::ControlModifier) != 0) {
if (event->angleDelta().y() > 0)
zoom_in();
m_current_tab->view().zoom_in();
else if (event->angleDelta().y() < 0)
zoom_out();
m_current_tab->view().zoom_out();
}
}

View file

@ -67,11 +67,6 @@ public slots:
void open_next_tab();
void open_previous_tab();
void open_file();
void zoom_in();
void zoom_out();
void reset_zoom();
void update_zoom_menu();
void update_displayed_zoom_level();
void show_find_in_page();
protected:
@ -87,7 +82,7 @@ private:
Tab& create_new_tab(Web::HTML::ActivateTab, Tab& parent, Optional<u64> page_index);
void initialize_tab(Tab*);
void set_current_tab(Tab* tab);
void set_current_tab(Tab* tab) { m_current_tab = tab; }
template<typename Callback>
void for_each_tab(Callback&& callback)
@ -115,7 +110,6 @@ private:
QTabWidget* m_tabs_container { nullptr };
Tab* m_current_tab { nullptr };
QMenu* m_zoom_menu { nullptr };
QToolBar* m_new_tab_button_toolbar { nullptr };

View file

@ -149,6 +149,27 @@ static void initialize_native_control(WebView::Action& action, QAction& qaction,
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/audio-volume-high.png"sv));
break;
case WebView::ActionID::ZoomIn: {
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/zoom-in.png"sv));
auto zoom_in_shortcuts = QKeySequence::keyBindings(QKeySequence::StandardKey::ZoomIn);
auto secondary_zoom_in_shortcut = QKeySequence(Qt::CTRL | Qt::Key_Equal);
if (!zoom_in_shortcuts.contains(secondary_zoom_in_shortcut))
zoom_in_shortcuts.append(move(secondary_zoom_in_shortcut));
qaction.setShortcuts(zoom_in_shortcuts);
break;
}
case WebView::ActionID::ZoomOut:
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/zoom-out.png"sv));
qaction.setShortcuts(QKeySequence::keyBindings(QKeySequence::StandardKey::ZoomOut));
break;
case WebView::ActionID::ResetZoom:
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/zoom-reset.png"sv));
qaction.setShortcut(QKeySequence(Qt::CTRL | Qt::Key_0));
break;
case WebView::ActionID::DumpSessionHistoryTree:
qaction.setIcon(load_icon_from_uri("resource://icons/16x16/history.png"sv));
break;

View file

@ -112,17 +112,7 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
m_hamburger_button_action->setVisible(!show_menubar);
});
m_reset_zoom_button = new QToolButton(m_toolbar);
m_reset_zoom_button->setToolButtonStyle(Qt::ToolButtonTextOnly);
m_reset_zoom_button->setToolTip("Reset zoom level");
m_reset_zoom_button_action = m_toolbar->addWidget(m_reset_zoom_button);
m_reset_zoom_button_action->setVisible(false);
QObject::connect(m_reset_zoom_button, &QAbstractButton::clicked, [this] {
view().reset_zoom();
update_reset_zoom_button();
m_window->update_zoom_menu();
});
m_toolbar->addAction(create_application_action(*m_toolbar, view().reset_zoom_action()));
view().on_activate_tab = [this] {
m_window->activate_tab(tab_index());
@ -475,18 +465,6 @@ Tab::Tab(BrowserWindow* window, RefPtr<WebView::WebContentClient> parent_client,
Tab::~Tab() = default;
void Tab::update_reset_zoom_button()
{
auto zoom_level = view().zoom_level();
if (zoom_level != 1.0f) {
auto zoom_level_text = MUST(String::formatted("{}%", round_to<int>(zoom_level * 100)));
m_reset_zoom_button->setText(qstring_from_ak_string(zoom_level_text));
m_reset_zoom_button_action->setVisible(true);
} else {
m_reset_zoom_button_action->setVisible(false);
}
}
void Tab::focus_location_editor()
{
m_location_edit->setFocus();

View file

@ -56,7 +56,6 @@ public:
void load_html(StringView);
void open_file();
void update_reset_zoom_button();
void show_find_in_page();
void find_previous();
@ -94,8 +93,6 @@ private:
QToolBar* m_toolbar { nullptr };
QToolButton* m_hamburger_button { nullptr };
QAction* m_hamburger_button_action { nullptr };
QToolButton* m_reset_zoom_button { nullptr };
QAction* m_reset_zoom_button_action { nullptr };
LocationEdit* m_location_edit { nullptr };
WebContentView* m_view { nullptr };
FindInPageWidget* m_find_in_page { nullptr };

View file

@ -166,12 +166,6 @@ WebContentView::WebContentView(QWidget* window, RefPtr<WebView::WebContentClient
m_select_dropdown->exec(map_point_to_global_position(content_position));
};
on_set_browser_zoom = [this](double factor) {
set_zoom(factor);
auto* window = static_cast<BrowserWindow*>(this->window());
window->update_displayed_zoom_level();
};
}
WebContentView::~WebContentView() = default;
@ -564,7 +558,7 @@ void WebContentView::update_viewport_size()
void WebContentView::update_zoom()
{
client().async_set_device_pixels_per_css_pixel(m_client_state.page_index, m_device_pixel_ratio * m_zoom_level);
ViewImplementation::update_zoom();
update_viewport_size();
}