Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions modules/juce_gui_extra/misc/juce_WebBrowserComponent.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,18 +216,38 @@ class JUCE_API WebBrowserComponent : public Component
return withMember (*this, &WinWebView2::backgroundColour, colour);
}

/** Floors the WebView's effective render scale so content stays sharp when the
host scales the editor up on a low-DPI (100%) display. The counterpart to
AppleWkWebView::withMinimumDeviceScaleFactor.

Implemented with the public ICoreWebView2Controller3::RasterizationScale,
which (unlike ZoomFactor) rasterises content at the target resolution rather
than scaling an already-rendered bitmap. Set to max(scale, monitorScale), with
monitor auto-detection disabled so the floor isn't reset on DPI changes (it is
re-applied with the live monitor scale instead). Raising RasterizationScale
also shrinks the CSS layout viewport (CSS px = bounds / scale), so this is only
appropriate for content that lays out responsively. No-op on WebView2 runtimes
without ICoreWebView2Controller3.
*/
[[nodiscard]] WinWebView2 withMinimumDeviceScaleFactor (double scale) const
{
return withMember (*this, &WinWebView2::minimumDeviceScaleFactor, scale);
}

//==============================================================================
File getDLLLocation() const { return dllLocation; }
File getUserDataFolder() const { return userDataFolder; }
bool getIsStatusBarDisabled() const noexcept { return disableStatusBar; }
bool getIsBuiltInErrorPageDisabled() const noexcept { return disableBuiltInErrorPage; }
Colour getBackgroundColour() const { return backgroundColour; }
auto getMinimumDeviceScaleFactor() const { return minimumDeviceScaleFactor; }

private:
//==============================================================================
File dllLocation, userDataFolder;
bool disableStatusBar = false, disableBuiltInErrorPage = false;
Colour backgroundColour;
std::optional<double> minimumDeviceScaleFactor;
};

/** Options specific to the WkWebView backend used on Apple systems. These options will be
Expand Down Expand Up @@ -280,14 +300,36 @@ class JUCE_API WebBrowserComponent : public Component
return withMember (*this, &AppleWkWebView::backgroundColour, colour);
}

/** Floors the WebView's effective render scale so content stays sharp when the
host scales the editor up on a low-DPI (non-Retina) display.

When the host enlarges the WebView via an ancestor AffineTransform (which
grows the WebView's bounds without changing its CSS layout) on a 1x display,
the OS upscales a 1x raster and the content — most visibly text — looks soft.
A Retina display already rasterises at 2x and hides it.

Implemented with the public WKWebView.pageZoom property (macOS 11+): page zoom
re-lays-out and re-rasterises at the zoomed size, so glyphs stay crisp. The
applied zoom is max(1, scale / backingScaleFactor), so a Retina display is a
no-op and only low-DPI displays get supersampling. Page zoom shrinks the CSS
layout viewport (the page sees fewer CSS px), so this is only appropriate for
content that lays out responsively to its viewport. No-op below macOS 11.
*/
[[nodiscard]] AppleWkWebView withMinimumDeviceScaleFactor (double scale) const
{
return withMember (*this, &AppleWkWebView::minimumDeviceScaleFactor, scale);
}

auto getAllowAccessToEnclosingDirectory() const { return allowAccessToEnclosingDirectory; }
auto getAcceptsFirstMouse() const { return acceptsFirstMouse; }
auto getBackgroundColour() const { return backgroundColour; }
auto getMinimumDeviceScaleFactor() const { return minimumDeviceScaleFactor; }

private:
bool allowAccessToEnclosingDirectory = false;
bool acceptsFirstMouse = true;
std::optional<Colour> backgroundColour;
std::optional<double> minimumDeviceScaleFactor;
};

/** Specifies options that apply to the Windows implementation when the WebView2 feature is
Expand Down
45 changes: 45 additions & 0 deletions modules/juce_gui_extra/native/juce_WebBrowserComponent_mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,21 @@ static var fromObject (id object)
return getIvar<std::atomic<bool>*> (instance, editableFocusActiveMemberName);
}

// Per-instance pointer to the WKWebViewImpl's minimum-render-scale value (0 = feature
// off). Read in viewDidChangeBackingProperties so pageZoom is (re)applied as the window
// attaches and as it moves between displays of differing backingScaleFactor.
static const char* minDeviceScaleMemberName = "minDeviceScaleHandle";

[[maybe_unused]] static void setMinDeviceScaleHandle (id instance, double* value)
{
object_setInstanceVariable (instance, minDeviceScaleMemberName, value);
}

[[maybe_unused]] static double* getMinDeviceScaleHandle (id instance)
{
return getIvar<double*> (instance, minDeviceScaleMemberName);
}

#if JUCE_MAC
template <class WebViewClass>
struct WebViewKeyEquivalentResponder final : public ObjCClass<WebViewClass>
Expand All @@ -203,6 +218,7 @@ explicit WebViewKeyEquivalentResponder (bool acceptsFirstMouse)
{
this->template addIvar<LastFocusChange*> (lastFocusChangeMemberName);
this->template addIvar<std::atomic<bool>*> (editableFocusActiveMemberName);
this->template addIvar<double*> (minDeviceScaleMemberName);

// When an editable DOM element has focus (signalled by JS via
// __juceSetEditableFocusActive), keys are routed to WKWebView's default
Expand Down Expand Up @@ -354,6 +370,23 @@ explicit WebViewKeyEquivalentResponder (bool acceptsFirstMouse)

if (CALayer* layer = [(NSView*) self layer])
layer.contentsScale = scale;

// Optional render-scale floor (withMinimumDeviceScaleFactor). We
// raise WKWebView.pageZoom (public, macOS 11+) so the page renders
// at a higher density; the consumer's responsive layout absorbs the
// resulting CSS-viewport shrink. Applied zoom is max(1, min/backing)
// so a Retina display (backing >= min) is a no-op. Re-applied here
// so it tracks moves between displays of differing backingScaleFactor.
if (double* minScale = getMinDeviceScaleHandle (self);
minScale != nullptr && *minScale > 0.0)
{
const CGFloat backing = jmax ((CGFloat) 1.0, scale);
const CGFloat zoom = jmax ((CGFloat) 1.0, (CGFloat) (*minScale) / backing);

if (@available (macOS 11.0, *))
if ([self isKindOfClass: [WKWebView class]])
((WKWebView*) self).pageZoom = zoom;
}
});

if (acceptsFirstMouse)
Expand Down Expand Up @@ -1004,6 +1037,14 @@ void setEditableFocusActive (bool active) override

setLastFocusChangeHandle (webView.get(), &lastFocusChange);
setEditableFocusActiveHandle (webView.get(), &editableFocusActive);

// Render-scale floor (off unless withMinimumDeviceScaleFactor is set). Applied via
// pageZoom in viewDidChangeBackingProperties, which fires on window-attach and on
// display moves, so the handle must be in place before addAndMakeVisible.
minDeviceScaleFactor = browserOptions.getAppleWkWebViewOptions()
.getMinimumDeviceScaleFactor()
.value_or (0.0);
setMinDeviceScaleHandle (webView.get(), &minDeviceScaleFactor);
#else
webView.reset ([[WKWebView alloc] initWithFrame: CGRectMake (0, 0, 100.0f, 100.0f)
configuration: config.get()]);
Expand Down Expand Up @@ -1325,6 +1366,10 @@ void setEditableFocusActive (bool active) override
// to true by the JS layer via __juceSetEditableFocusActive when an
// editable DOM element gains focus, so character input reaches the DOM.
std::atomic<bool> editableFocusActive { false };
// Render-scale floor (withMinimumDeviceScaleFactor); 0 = off. Held by value so the
// WKWebView subclass can read it through a per-instance handle in
// viewDidChangeBackingProperties. Outlives the webView — both are members here.
double minDeviceScaleFactor = 0.0;
ObjCObjectHandle<WKWebView*> webView;
ObjCObjectHandle<id> webViewDelegate;
String lastRequestedUrl, lastLoadedUrl;
Expand Down
40 changes: 40 additions & 0 deletions modules/juce_gui_extra/native/juce_WebBrowserComponent_windows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,8 @@ class WebBrowserComponent::Impl::Platform::WebView2 final : public WebBrowserCom
(BYTE) bgColour.getBlue() });
}

applyRasterizationScaleFloor();

ComSmartPtr<ICoreWebView2Settings> settings;
webView->get_Settings (settings.resetAndGetPointerAddress());

Expand Down Expand Up @@ -1233,6 +1235,40 @@ class WebBrowserComponent::Impl::Platform::WebView2 final : public WebBrowserCom
webViewController->put_IsVisible (shouldBeVisible);
}

// Floors the WebView's rasterisation scale (withMinimumDeviceScaleFactor). Off
// unless the option is set. On a 100%-scaled monitor the content otherwise
// rasterises at one device pixel per CSS pixel, so when the host scales the
// editor up via an ancestor transform the OS upscales a 1x raster and the UI
// (most visibly text) looks blurry. We set RasterizationScale to at least the
// requested floor and disable WebView2's own monitor-scale detection so it
// doesn't reset us on DPI changes — re-applied from the scale-factor notifier
// with the live monitor scale instead. RasterizationScale rasterises at the
// target resolution (unlike ZoomFactor, which scales an already-rendered
// bitmap). Requires a runtime supporting ICoreWebView2Controller3; otherwise a
// no-op.
void applyRasterizationScaleFloor() const
{
const auto minScale = preferences.getWinWebView2BackendOptions().getMinimumDeviceScaleFactor();

if (! minScale.has_value() || webViewController == nullptr)
return;

ComSmartPtr<ICoreWebView2Controller3> controller3;
webViewController->QueryInterface (controller3.resetAndGetPointerAddress());

if (controller3 == nullptr)
return;

controller3->put_ShouldDetectMonitorScaleChanges (FALSE);

auto monitorScale = 1.0;

if (auto* peer = owner.getTopLevelComponent()->getPeer())
monitorScale = peer->getPlatformScaleFactor();

controller3->put_RasterizationScale (jmax (*minScale, monitorScale));
}

//==============================================================================
WebBrowserComponent& owner;
WebBrowserComponent::Options preferences;
Expand Down Expand Up @@ -1275,6 +1311,10 @@ class WebBrowserComponent::Impl::Platform::WebView2 final : public WebBrowserCom
NativeScaleFactorNotifier scaleFactorNotifier { this,
[this] (auto)
{
// Re-apply the rasterisation floor against the new
// monitor scale (auto-detection is off when the floor
// is in use), then refresh bounds.
applyRasterizationScaleFloor();
componentMovedOrResized (true, true);
} };

Expand Down