Skip to content
Closed
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
47 changes: 47 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,42 @@ class JUCE_API WebBrowserComponent : public Component
return withMember (*this, &WinWebView2::backgroundColour, colour);
}

/** Forces WebView2 to rasterise its content at no less than this scale, regardless
of the monitor's DPI scaling.

The counterpart to AppleWkWebView::withMinimumDeviceScaleFactor. On a display
at 100% scaling WebView2 otherwise rasterises at one device pixel per CSS pixel;
if the host scales the editor up via an ancestor AffineTransform (which enlarges
the WebView's bounds without changing its CSS layout), the OS upscales that 1x
raster and the content looks soft — most visibly text. Raising the floor gives
the renderer headroom so the upscale stays sharp. A high-DPI monitor already
exceeds this, so it is a floor, not a fixed value.

Implemented via ICoreWebView2Controller3::put_RasterizationScale, with monitor
auto-detection disabled so the floor isn't reset on DPI changes (it is re-applied
with the live monitor scale instead). Note that raising RasterizationScale also
shrinks the CSS layout viewport (CSS px = bounds / scale), so this is only
appropriate for content that lays out responsively to its viewport.
*/
[[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 +304,37 @@ class JUCE_API WebBrowserComponent : public Component
return withMember (*this, &AppleWkWebView::backgroundColour, colour);
}

/** Forces the WKWebView to rasterise its content at no less than this device
scale factor, regardless of the backing scale of the display it sits on.

On a standard-DPI (1x) display the WebView otherwise renders one device
pixel per CSS pixel. If the host then scales the editor up (e.g. via an
ancestor AffineTransform that enlarges the WebView's peer bounds), the OS
upscales that 1x raster and the content looks soft — most visibly text.
Raising the effective device scale gives the renderer headroom so the
upscale stays sharp. A high-DPI (Retina) display already exceeds this, so
it is a floor, not a fixed value.

This affects rasterisation density and window.devicePixelRatio only — CSS
layout is unchanged, so it cannot shift or reflow the UI. Implemented via a
private WKWebView SPI and re-applied on display moves; silently ignored if
the running OS doesn't expose it.
*/
[[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
67 changes: 67 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,23 @@ static var fromObject (id object)
return getIvar<std::atomic<bool>*> (instance, editableFocusActiveMemberName);
}

#if JUCE_MAC
// Per-instance pointer to the WKWebViewImpl's minimum-device-scale value (0 = feature
// off). Read in viewDidChangeBackingProperties so the override 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);
}
#endif

#if JUCE_MAC
template <class WebViewClass>
struct WebViewKeyEquivalentResponder final : public ObjCClass<WebViewClass>
Expand All @@ -203,6 +220,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 +372,41 @@ explicit WebViewKeyEquivalentResponder (bool acceptsFirstMouse)

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

// Optional rasterisation-density floor (withMinimumDeviceScaleFactor).
// Re-applied here so it survives moves between displays of differing
// backingScaleFactor. We never go below the display's own backing
// scale, so a Retina screen is unaffected. Affects raster density and
// window.devicePixelRatio only — not CSS layout.
if (double* minScale = getMinDeviceScaleHandle (self);
minScale != nullptr && *minScale > 0.0)
{
const CGFloat effective = jmax ((CGFloat) *minScale, scale);

// _setOverrideDeviceScaleFactor: is private WKWebView SPI,
// so it isn't in any public header — call it through a
// cast objc_msgSend rather than declaring a category (an
// @interface can't live inside this namespace). Guarded by
// respondsToSelector + @try so a future OS that drops it is
// a silent no-op rather than a crash.
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector")
const SEL overrideSel = @selector (_setOverrideDeviceScaleFactor:);
JUCE_END_IGNORE_WARNINGS_GCC_LIKE

if ([(NSView*) self respondsToSelector: overrideSel])
{
@try
{
using SetScaleFn = void (*) (id, SEL, CGFloat);
const auto setScale = reinterpret_cast<SetScaleFn> (objc_msgSend);
setScale (self, overrideSel, effective);
}
@catch (NSException* exception)
{
(void) exception;
}
}
}
});

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

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

// Rasterisation-density floor (off unless withMinimumDeviceScaleFactor is set).
// The override is actually applied in viewDidChangeBackingProperties, which
// fires when the view attaches to a window and on every display move, so the
// handle must be in place before addAndMakeVisible below.
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 +1387,11 @@ 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 };
// Rasterisation-density floor (withMinimumDeviceScaleFactor); 0 = off. Held by
// value so the WKWebView subclass can read it through a per-instance handle in
// viewDidChangeBackingProperties. Must outlive the webView — it does, both are
// members of this impl.
double minDeviceScaleFactor = 0.0;
ObjCObjectHandle<WKWebView*> webView;
ObjCObjectHandle<id> webViewDelegate;
String lastRequestedUrl, lastLoadedUrl;
Expand Down
38 changes: 38 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,38 @@ 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. Requires the WebView2 runtime to support
// ICoreWebView2Controller3; a no-op on older runtimes.
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 +1309,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
Loading