Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add per-window content scaling #9428

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open

Add per-window content scaling #9428

wants to merge 14 commits into from

Conversation

dawsers
Copy link
Contributor

@dawsers dawsers commented Feb 17, 2025

Add a new dispatcher scaleactive that scales the content of the active window

per-window_scaling

Each window can have its contents with a different scale. This can be useful for background apps/jobs you want to control and you don't want them to take much space, pinned windows etc. You can also use it for applications that have their UI too big or too small and don't support native re-scaling.

I don't think I have covered every case yet, but I wanted to propose this initial PR so you guys tell me if there is any real interest for this before I spend more time in it. I was going to implement this in hyprscroller, but it made more sense to integrate the feature in Hyprland.

There is currently a bug that I haven't figured out: Sometimes the client renders popups to be "inside the monitor", so despite the window being visible and interactive, some popups render in what would be the non-scaled monitor extents. I can attach an image if there is any interest to help.

The change should be transparent. By default the scale is 1.0, so it shouldn't interfere with Hyprland unless changed.

To try it out, add something like this to your config:

# Scaling submap
# will switch to a submap called scaling
bind = $mainMod, period, submap, scaling
# will start a submap called "scaling"
submap = scaling
# sets repeatable binds for scaling the content of the active window
bind = , 1, scaleactive, 0.4 
bind = , 1, submap, reset
bind = , 2, scaleactive, 0.6
bind = , 2, submap, reset
bind = , 3, scaleactive, 0.8
bind = , 3, submap, reset
bind = , 4, scaleactive, 1.25
bind = , 4, submap, reset
bind = , 5, scaleactive, 1.5
bind = , 5, submap, reset
bind = , 6, scaleactive, 2.0
bind = , 6, submap, reset
bind = , 0, scaleactive, -1.0
bind = , 0, submap, reset
bind = , minus, scaleactive, -0.05
bind = , minus, submap, reset
bind = , equal, scaleactive, +0.05
bind = , equal, submap, reset
# use reset to go back to the global submap
bind = , escape, submap, reset
# will reset the submap, meaning end the current one and return to the global one
submap = reset

#bind = $mainMode, period, scroller:scaleactive, 0.5
bind = $mainMode, comma, scaleactive, -1

The dispatcher accepts any positive floating point number as scale. Using a negative number (-1 by convention) will reset the scale of the content of that window. The scale has the same meaning as the fractional value for the monitor: 2 would make the content 2x2 times bigger. 0.5 would make it smaller.

Thanks!

Copy link
Member

@vaxerski vaxerski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some general design remarks:

  • consider adding a window rule for this (see prop window rules) then the dispatcher would be to override it.
  • we're adding the conentScale param everywhere - no better method? in most cases, it could be read from the window param.

No style nits for now, general design needs some work

@dawsers
Copy link
Contributor Author

dawsers commented Feb 18, 2025

Thank you for your prompt review.

I have added the window rule and modified the dispatcher to only override it. I think I am not missing anything, but please let me know if you find something strange. The only difference with other WindowData properties is this one needs to trigger a sendWindowSize() call every time the property changes, so I had to create a specific entry in the window rule check instead of relying on the default for properties.

About having to add contentScale as a parameter to a few functions, I tried not to. I think I only added it to those that didn't have access to a window:

std::pair<SP<CWLSurfaceResource>, Vector2D> CWLSurfaceResource::at(const Vector2D& localCoords, bool allowsInput, double scale)

This function can be called for any type of surface, either attached to a window or to a layer, I don't think I can remove the parameter.

void CHyprRenderer::calculateUVForSurface(PHLWINDOW pWindow, SP<CWLSurfaceResource> pSurface, PHLMONITOR pMonitor, bool main, const Vector2D& projSize, const Vector2D& projSizeUnscaled, bool fixMisalignedFSV1, float contentScale);

This function is called by CSurfacePassElement::draw(), and sometimes there is no window there either.

I don't think I have added any more parameters to functions, but let me know if I am mistaken or any of those can be removed somehow I haven't figured out.

Thanks!

@SNAKESLIGHT
Copy link

Would love to see the ability to use a relative float such as +0.2, similar to what the splitratio dispatcher accepts. That way you could bind + and - to scale a window.

Copy link
Member

@vaxerski vaxerski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as someone else pointed out, relative scaling for the dispatcher would be a nice addition

@dawsers
Copy link
Contributor Author

dawsers commented Feb 21, 2025

OK, done.

I have one question. I want to try to fix the bug I mentioned about the application thinking the window doesn't fit in the viewport and locating some tooltips in the "wrong" position. Where is the code that sends monitor information to the applications? I found how to send window sizes (sendWindowSize() sends an Ack request through the protocol), but I haven't found how monitor sizes are communicated.

@vaxerski
Copy link
Member

they aren't, it's xdg_positioner. Good luck with getting that to work. Check CXDGPositionerRules::getPosition

@dawsers
Copy link
Contributor Author

dawsers commented Feb 25, 2025

Everything seems to be working now for Wayland windows. I am going to see if XWayland can be adapted too. Scaling is OK, popups are not, but I haven't tried anything yet.

@dawsers
Copy link
Contributor Author

dawsers commented Feb 25, 2025

I see XWayland popups are "regular" floating windows, and don't keep any reference to a parent. I see there is a CWindow::m_pX11Parent field, but it is never used or updated. This means I cannot know if the window needs its coordinates tansformed, so I don't think there is anything else to do for now until this changes or anyone has an idea on how to make this work. Maybe some other structure keeps parenting information or are these popups handled specifically by the application?

So, current status: It seems everything is working for Wayland windows: popups and not. For X windows, scaling works, but popups render at the wrong location. Anything else?

@vaxerski
Copy link
Member

for X11, I'd say:
who gaf? :P

I'll test this MR a bit tomorrow, if I don't forget.

@dawsers
Copy link
Contributor Author

dawsers commented Feb 28, 2025

Cool.

I think this may close #3861

@github-actions github-actions bot added the debug label Mar 2, 2025
Copy link
Member

@vaxerski vaxerski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

res ok

@dawsers
Copy link
Contributor Author

dawsers commented Mar 7, 2025

Do we want this or should I close it?

@vaxerski
Copy link
Member

vaxerski commented Mar 8, 2025

sorry, just busy and I get tons of notifs. We do, just fix the conflict pls

@@ -10,7 +10,7 @@ static const auto RULES = std::unordered_set<std::string>{
static const auto RULES_PREFIX = std::unordered_set<std::string>{
"animation", "bordercolor", "bordersize", "center", "content", "fullscreenstate", "group", "idleinhibit", "maxsize", "minsize",
"monitor", "move", "opacity", "plugin:", "prop", "pseudo", "rounding", "roundingpower", "scrollmouse", "scrolltouchpad",
"size", "suppressevent", "tag", "workspace", "xray",
"size", "suppressevent", "tag", "workspace", "xray", "contentscale",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't be needed, props are already being searched
(and same for rounding roundingpower scrollmouse scrolltouchpad it seems)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this because like you saw..."everybody is doing it, why don't we". I thought maybe there is some need or plan to have every property listed there, even if they don't get accessed this way. Do I remove it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you're using a prop (NWindowProperties) it's already checked for you

@@ -55,6 +55,8 @@ CWindowRule::CWindowRule(const std::string& rule, const std::string& value, bool
ruleType = RULE_MAXSIZE;
else if (rule.starts_with("minsize"))
ruleType = RULE_MINSIZE;
else if (rule.starts_with("contentscale"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the property needs to be here, because it needs special treatment. Like you mention below, it needs to invert the value and also call sendWindowSize() to update the client's resolution. Adding an if below is in my opinion, less clean than stating the property is "special" in a way. In the special code (in src/desktop/Window.cpp), aside from inversion and updating the client, we could also add checks for the range of the value if wanted (as there are in the dispatcher, suggested by @vaxerski

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah it does if you're using your custom rule type, but like I said bellow this should use the default treatment if you're using a prop (NWindowProperties)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, since you are using a prop it's handled for you

@@ -3135,6 +3171,9 @@ SDispatchResult CKeybindManager::setProp(std::string args) {
PWINDOW->m_sWindowData.minSize = CWindowOverridableVar(configStringToVector2D(VAL), PRIORITY_SET_PROP);
PWINDOW->clampWindowSize(PWINDOW->m_sWindowData.minSize.value(), std::nullopt);
PWINDOW->setHidden(false);
} else if (PROP == "contentscale") {
PWINDOW->m_sWindowData.contentScale = CWindowOverridableVar(1.0f / std::stof(VAL), PRIORITY_SET_PROP);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should also use the already existing window prop code to support unset and avoid duplication
you won't be able to store the inverted value doe
as for the PWINDOW->sendWindowSize(); maybe add a separate if check at the end just for it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we need to agree on whether this property is "special" like the others in the if statements or not.

When I implemented it at the beginning, I tried to add it to the list, like you suggest. But then I saw I couldn't do the things I needed if I added it there. I saw other properties had the same problem, and I followed their process.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a point in inverting the value here
just invert the value when reading/writing or just make the actual internal value what the user will input

as for adding an if, I don't know how heavy that function is
but the best is probably just adding it at the end where all the recalculation is being done, if not inside recalculateWindow();, which should be called at the end by g_pLayoutManager->getCurrentLayout()->recalculateMonitor(m->ID);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The input value is chosen to be similar to fractional scale so the user has a better intuition on how to use the feature, but then internally it is easier to manage it inverted. That is the only reason, and inverting it just here is easier and more efficient than having to do it every time the value is read or written, or so I thought.

@@ -2148,6 +2149,41 @@ SDispatchResult CKeybindManager::resizeActive(std::string args) {
return {};
}

SDispatchResult CKeybindManager::scaleActive(std::string args) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't the setprop dispatcher be used instead of making a new one?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dispatcher allows absolute and delta increases, as requested above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#9566 would allow that for all props

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, so we will wait for that to happen.

@@ -769,6 +769,14 @@ void CWindow::applyDynamicRule(const SP<CWindowRule>& r) {
} catch (std::exception& e) { Debug::log(ERR, "minsize rule \"{}\" failed with: {}", r->szRule, e.what()); }
break;
}
case CWindowRule::RULE_CONTENTSCALE: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't be needed

@@ -26,6 +26,7 @@ class CWindowRule {
RULE_IDLEINHIBIT,
RULE_MAXSIZE,
RULE_MINSIZE,
RULE_CONTENTSCALE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nor this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what is your suggestion? It is the only way I found to be able to update the client's resolution, invert the scale etc. It seems to me a generic floating point property doesn't allow me to do those things.

@MightyPlaza
Copy link
Contributor

here's an example
had to swap contentscale to scalecontent due hyprland mathing the content window rule instead (which is a bug)

diff --git a/src/desktop/Window.cpp b/src/desktop/Window.cpp
index e5924b81..487302c7 100644
--- a/src/desktop/Window.cpp
+++ b/src/desktop/Window.cpp
@@ -769,14 +769,6 @@ void CWindow::applyDynamicRule(const SP<CWindowRule>& r) {
             } catch (std::exception& e) { Debug::log(ERR, "minsize rule \"{}\" failed with: {}", r->szRule, e.what()); }
             break;
         }
-        case CWindowRule::RULE_CONTENTSCALE: {
-            try {
-                const auto SCALE           = std::stof(r->szRule.substr(13));
-                m_sWindowData.contentScale = CWindowOverridableVar(1.0f / SCALE, priority);
-                sendWindowSize();
-            } catch (std::exception& e) { Debug::log(ERR, "contentscale rule \"{}\" failed with: {}", r->szRule, e.what()); }
-            break;
-        }
         case CWindowRule::RULE_RENDERUNFOCUSED: {
             m_sWindowData.renderUnfocused = CWindowOverridableVar(true, priority);
             g_pHyprRenderer->addWindowToRenderUnfocused(m_pSelf.lock());
@@ -835,6 +827,7 @@ void CWindow::updateDynamicRules() {
     EMIT_HOOK_EVENT("windowUpdateRules", m_pSelf.lock());
 
     g_pLayoutManager->getCurrentLayout()->recalculateMonitor(monitorID());
+    sendWindowSize();
 }
 
 // check if the point is "hidden" under a rounded corner of the window
@@ -1238,7 +1231,7 @@ float CWindow::getScrollTouchpad() {
 }
 
 float CWindow::getContentScale() {
-    return m_sWindowData.contentScale.valueOr(1.0f);
+    return 1.f / m_sWindowData.contentScale.valueOr(1.0f);
 }
 
 bool CWindow::canBeTorn() {
diff --git a/src/desktop/Window.hpp b/src/desktop/Window.hpp
index 9e2a55d3..318b16f0 100644
--- a/src/desktop/Window.hpp
+++ b/src/desktop/Window.hpp
@@ -191,6 +191,7 @@ struct SWindowData {
 
     CWindowOverridableVar<float>              scrollMouse;
     CWindowOverridableVar<float>              scrollTouchpad;
+    CWindowOverridableVar<float>              contentScale;
 
     CWindowOverridableVar<std::string>        animationStyle;
     CWindowOverridableVar<Vector2D>           maxSize;
@@ -199,8 +200,6 @@ struct SWindowData {
     CWindowOverridableVar<CGradientValueData> activeBorderColor;
     CWindowOverridableVar<CGradientValueData> inactiveBorderColor;
 
-    CWindowOverridableVar<float>              contentScale;
-
     CWindowOverridableVar<bool>               persistentSize;
 };
 
@@ -578,7 +577,7 @@ namespace NWindowProperties {
         {"roundingpower", [](const PHLWINDOW& pWindow) { return &pWindow->m_sWindowData.roundingPower; }},
         {"scrollmouse", [](const PHLWINDOW& pWindow) { return &pWindow->m_sWindowData.scrollMouse; }},
         {"scrolltouchpad", [](const PHLWINDOW& pWindow) { return &pWindow->m_sWindowData.scrollTouchpad; }},
-        {"contentscale", [](const PHLWINDOW& pWindow) { return &pWindow->m_sWindowData.contentScale; }},
+        {"scalecontent", [](const PHLWINDOW& pWindow) { return &pWindow->m_sWindowData.contentScale; }},
     };
 };
 
diff --git a/src/desktop/WindowRule.cpp b/src/desktop/WindowRule.cpp
index 598c29e7..23269085 100644
--- a/src/desktop/WindowRule.cpp
+++ b/src/desktop/WindowRule.cpp
@@ -10,7 +10,7 @@ static const auto RULES = std::unordered_set<std::string>{
 static const auto RULES_PREFIX = std::unordered_set<std::string>{
     "animation", "bordercolor",   "bordersize", "center",    "content", "fullscreenstate", "group",    "idleinhibit",   "maxsize",     "minsize",
     "monitor",   "move",          "opacity",    "plugin:",   "prop",    "pseudo",          "rounding", "roundingpower", "scrollmouse", "scrolltouchpad",
-    "size",      "suppressevent", "tag",        "workspace", "xray",    "contentscale",
+    "size",      "suppressevent", "tag",        "workspace", "xray",
 };
 
 CWindowRule::CWindowRule(const std::string& rule, const std::string& value, bool isV2, bool isExecRule) : szValue(value), szRule(rule), v2(isV2), execRule(isExecRule) {
@@ -57,8 +57,6 @@ CWindowRule::CWindowRule(const std::string& rule, const std::string& value, bool
         ruleType = RULE_MAXSIZE;
     else if (rule.starts_with("minsize"))
         ruleType = RULE_MINSIZE;
-    else if (rule.starts_with("contentscale"))
-        ruleType = RULE_CONTENTSCALE;
     else if (rule.starts_with("monitor"))
         ruleType = RULE_MONITOR;
     else if (rule.starts_with("move"))
diff --git a/src/desktop/WindowRule.hpp b/src/desktop/WindowRule.hpp
index 98a680ef..f0722362 100644
--- a/src/desktop/WindowRule.hpp
+++ b/src/desktop/WindowRule.hpp
@@ -26,7 +26,6 @@ class CWindowRule {
         RULE_IDLEINHIBIT,
         RULE_MAXSIZE,
         RULE_MINSIZE,
-        RULE_CONTENTSCALE,
         RULE_MONITOR,
         RULE_MOVE,
         RULE_OPACITY,
diff --git a/src/managers/KeybindManager.cpp b/src/managers/KeybindManager.cpp
index 6cb19e6a..f2ccef2b 100644
--- a/src/managers/KeybindManager.cpp
+++ b/src/managers/KeybindManager.cpp
@@ -3171,9 +3171,6 @@ SDispatchResult CKeybindManager::setProp(std::string args) {
             PWINDOW->m_sWindowData.minSize = CWindowOverridableVar(configStringToVector2D(VAL), PRIORITY_SET_PROP);
             PWINDOW->clampWindowSize(PWINDOW->m_sWindowData.minSize.value(), std::nullopt);
             PWINDOW->setHidden(false);
-        } else if (PROP == "contentscale") {
-            PWINDOW->m_sWindowData.contentScale = CWindowOverridableVar(1.0f / std::stof(VAL), PRIORITY_SET_PROP);
-            PWINDOW->sendWindowSize();
         } else if (PROP == "alpha") {
             PWINDOW->m_sWindowData.alpha = CWindowOverridableVar(SAlphaValue{std::stof(VAL), PWINDOW->m_sWindowData.alpha.valueOrDefault().m_bOverride}, PRIORITY_SET_PROP);
         } else if (PROP == "alphainactive") {
@@ -3240,6 +3237,7 @@ SDispatchResult CKeybindManager::setProp(std::string args) {
             return {.success = false, .error = "Prop not found"};
     } catch (std::exception& e) { return {.success = false, .error = std::format("Error parsing prop value: {}", std::string(e.what()))}; }
 
+    PWINDOW->sendWindowSize();
     g_pCompositor->updateAllWindowsAnimatedDecorationValues();
 
     if (!(PWINDOW->m_sWindowData.noFocus.valueOrDefault() == noFocus)) {

@MightyPlaza
Copy link
Contributor

on another note, the scaling doesn't seem to be working during animations that resize the window

@dawsers
Copy link
Contributor Author

dawsers commented Mar 8, 2025

here's an example had to swap contentscale to scalecontent due hyprland mathing the content window rule instead (which is a bug)

+    PWINDOW->sendWindowSize();
     g_pCompositor->updateAllWindowsAnimatedDecorationValues();
 
     if (!(PWINDOW->m_sWindowData.noFocus.valueOrDefault() == noFocus)) {

So, to be able to remove the special rule, you'd' rather call sendWindowSize() every time a property changes, no matter which one it is. sendWindowSize() seems to be an expensive operation, it tells the client to change the resolution at which it is rendering.

And that is also why during animations, this "feature" doesn't work until the animation is finished, because you shouldn't tell the client once very tick to update the resolution at which it has to render the application window.

@MightyPlaza
Copy link
Contributor

MightyPlaza commented Mar 8, 2025

here's an example had to swap contentscale to scalecontent due hyprland mathing the content window rule instead (which is a bug)

+    PWINDOW->sendWindowSize();
     g_pCompositor->updateAllWindowsAnimatedDecorationValues();
 
     if (!(PWINDOW->m_sWindowData.noFocus.valueOrDefault() == noFocus)) {

So, to be able to remove the special rule, you'd' rather call sendWindowSize() every time a property changes, no matter which one it is. sendWindowSize() seems to be an expensive operation, it tells the client to change the resolution at which it is rendering.

And that is also why during animations, this "feature" doesn't work until the animation is finished, because you shouldn't tell the client once very tick to update the resolution at which it has to render the application window.

again, you can add a separate if inside the loop if that is a problem
my main concern is having a separate parsing of the variable itself, so features across floating NWindowProperties don't get inconsistent (for example toggle for bools, unset, or the relative value from the other PR)

@dawsers
Copy link
Contributor Author

dawsers commented Mar 8, 2025

OK, we will wait until those PRs are in and I will try to make those changes then, because right now the system doesn't support what you are asking while covering the needs of this feature. I have been waiting for three weeks, and merged upstream changes quite a few times already, so this is starting to take much more time that I was expecting. We can also forget about this PR, and you take the pieces of this you want and use them. I don't care either way. I am not asking or care for any attribution, I only added this PR because I wanted to use the feature in my plugin. Now I don't even know if I will be able to after these changes anyway.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants