Discussion:
Zoom: a window management minor mode -- best practices and questions
Andrea Cardaci
2018-05-02 16:31:11 UTC
Permalink
Hi,

This is a follow-up discussion for a bug I reported (
https://debbugs.gnu.org/cgi/bugreport.cgi?bug=31312), since the
conversation was moving away from the bug itself I'm starting a new thread
as suggested by Eli.
In that case, could you explain in more detail what zoom.el attempts
to do
Zoom (https://github.com/cyrus-and/zoom) aims to enforce a fixed window
layout so that the selected window is always *big enough*. I think the
screencast at the project page is quite clear about the usage.

The implementation is very simple, every time a window change is *detected*
the following happens:
1. `balance-windows` is called;
2. the selected window is resized.

This, in practice, has the effect of disabling the manual resizing of
windows.

The problem is that to implement the "every time a window change is
detected" part I currently hook several functions, specifically:

(add-hook 'window-size-change-functions #'zoom--handler)
(advice-add #'select-window :after #'zoom--handler)

This is clearly an hack-ish solution but Zoom works fairly well most of the
times. On the other thread Martin suggested to replace `advice-add` with an
hook to `buffer-list-update-hook`, but the main issue remains that
`zoom--handler` actually resizes windows in the
`window-size-change-functions` hook.

Moreover, this solution causes the handler to be called several times and
most of them are useless. (Additional guards can be added but still...)
and what does it need from Emacs core to be able to do that?
My knowledge of the Emacs internals is quite limited but at the highest
level of abstraction what AFAIK is missing is a way to enforce custom
window layouts and manage the windows size. This part seems quite fragile /
extremely hard to work with (just look at the implementation of
`balance-windows`). In addition to that there are some features that
further complicate the situation, e.g., side windows and not resizable
windows.

Said that, I'm definitely not in a position to make any feature requests
but I would appreciate any feedback or suggestions.


Thanks,

Andrea
Eli Zaretskii
2018-05-02 17:32:16 UTC
Permalink
Date: Wed, 2 May 2018 18:31:11 +0200
Zoom (https://github.com/cyrus-and/zoom) aims to enforce a fixed window layout so that the selected window
is always *big enough*. I think the screencast at the project page is quite clear about the usage.
I've seen the screencast before asking the question, but couldn't
grasp the intent well enough, probably because the display in
screencast switches windows too quickly, and it's hard to understand
what Zoom attempts to do.
1. `balance-windows` is called;
2. the selected window is resized.
This, in practice, has the effect of disabling the manual resizing of windows.
The problem is that to implement the "every time a window change is detected" part I currently hook several
(add-hook 'window-size-change-functions #'zoom--handler)
(advice-add #'select-window :after #'zoom--handler)
I understand why you need to hook select-window, but not why you need
the other hook. Is it because there are too many functions that could
modify the window size, and you didn't want to hook all of them?

Did you try to use pre-redisplay-functions instead? That doesn't
necessarily tell you that the selected window is going to change or
its dimensions are about to change, but determining that for a single
window shouldn't be too expensive, right?

Alternatively, we could provide some control to disable resizing of
the selected window -- would that be enough, in addition to hooking
select-widnow using the method suggested by martin?
My knowledge of the Emacs internals is quite limited but at the highest level of abstraction what AFAIK is
missing is a way to enforce custom window layouts and manage the windows size. This part seems quite
fragile / extremely hard to work with (just look at the implementation of `balance-windows`). In addition to that
there are some features that further complicate the situation, e.g., side windows and not resizable windows.
Maybe Martin will suggest a good way of doing that using the existing
facilities, if they are enough.
Andrea Cardaci
2018-05-02 18:41:38 UTC
Permalink
Thanks for the answer.
Post by Eli Zaretskii
I understand why you need to hook select-window, but not why you need
the other hook. Is it because there are too many functions that could
modify the window size, and you didn't want to hook all of them?
The handler should be triggered in these cases:
- a window is resized;
- a window is selected;
- a window is created (this is actually included in the "a window is resized"
case);
- a window changes its buffer (because it's possible to exclude certain
windows from zooming so the layout should be updated accordingly).

So yes, instead of trying to figure out what the functions that cause such
events are (probably way too many) I simply tried to hook on the effects of
such actions.
Post by Eli Zaretskii
Did you try to use pre-redisplay-functions instead?
Not yet, but a quick test shows that it gets called *very* often, even when
Emacs is idle. I should really implement some additional guard if I decide
to go that way to avoid incurring in performance issues.

Also it seems to not work well with the `track-mouse` variable that I use
to delay the re-layout if there is some mouse selection in progress.

Besides, doesn't this in principle suffer of the same
do-something-that-triggers-the-hook-inside-such-hook problem like
`window-size-change-functions`?
Post by Eli Zaretskii
Alternatively, we could provide some control to disable resizing of
the selected window
Please elaborate on this point. How can it help this situation?
Post by Eli Zaretskii
Date: Wed, 2 May 2018 18:31:11 +0200
Zoom (https://github.com/cyrus-and/zoom) aims to enforce a fixed window
layout so that the selected window
is always *big enough*. I think the screencast at the project page is
quite clear about the usage.
I've seen the screencast before asking the question, but couldn't
grasp the intent well enough, probably because the display in
screencast switches windows too quickly, and it's hard to understand
what Zoom attempts to do.
The implementation is very simple, every time a window change is
1. `balance-windows` is called;
2. the selected window is resized.
This, in practice, has the effect of disabling the manual resizing of
windows.
The problem is that to implement the "every time a window change is
detected" part I currently hook several
(add-hook 'window-size-change-functions #'zoom--handler)
(advice-add #'select-window :after #'zoom--handler)
I understand why you need to hook select-window, but not why you need
the other hook. Is it because there are too many functions that could
modify the window size, and you didn't want to hook all of them?
Did you try to use pre-redisplay-functions instead? That doesn't
necessarily tell you that the selected window is going to change or
its dimensions are about to change, but determining that for a single
window shouldn't be too expensive, right?
Alternatively, we could provide some control to disable resizing of
the selected window -- would that be enough, in addition to hooking
select-widnow using the method suggested by martin?
My knowledge of the Emacs internals is quite limited but at the highest
level of abstraction what AFAIK is
missing is a way to enforce custom window layouts and manage the windows
size. This part seems quite
fragile / extremely hard to work with (just look at the implementation
of `balance-windows`). In addition to that
there are some features that further complicate the situation, e.g.,
side windows and not resizable windows.
Maybe Martin will suggest a good way of doing that using the existing
facilities, if they are enough.
Eli Zaretskii
2018-05-02 18:58:18 UTC
Permalink
Date: Wed, 2 May 2018 20:41:38 +0200
- a window is resized;
This is the one I mentioned as missing.
- a window is selected;
Hooking select-window will take care of this one.
- a window is created (this is actually included in the "a window is resized" case);
If you mean that the selected window is resized as result of creating
a new window, then yes, "window is resized" takes care of this. (What
about deleting a window, btw?)
- a window changes its buffer (because it's possible to exclude certain windows from zooming so the layout
should be updated accordingly).
The hook suggested by Martin should take care of this.
Besides, doesn't this in principle suffer of the same do-something-that-triggers-the-hook-inside-such-hook
problem like `window-size-change-functions`?
No, because pre-redisplay-functions are run _before_ anything
significant in redisplay, so your chances to confuse redisplay are
nil.
Post by Eli Zaretskii
Alternatively, we could provide some control to disable resizing of
the selected window
Please elaborate on this point. How can it help this situation?
Well, as you said, the main purpose of Zoom is to prevent resizing of
the selected window due to Emacs's own considerations. My
interpretation of this was that you want to control the size of the
selected window, and disallow anything else changing it. So if you
change the size at select-window time, and the size is not allowed to
change after that, you have reached your purpose, right?
martin rudalics
2018-05-03 07:11:34 UTC
Permalink
Post by Eli Zaretskii
Well, as you said, the main purpose of Zoom is to prevent resizing of
the selected window due to Emacs's own considerations. My
interpretation of this was that you want to control the size of the
selected window, and disallow anything else changing it. So if you
change the size at select-window time, and the size is not allowed to
change after that, you have reached your purpose, right?
IUUC zoom.el does not necessarily prevent resizing of the selected
window (we allow fixing or preserving window sizes to do that). It
tries to have the selected window stand out by making it "very" large.

martin
Andrea Cardaci
2018-05-03 09:50:21 UTC
Permalink
Post by martin rudalics
IUUC zoom.el does not necessarily prevent resizing of the selected
window (we allow fixing or preserving window sizes to do that). It
tries to have the selected window stand out by making it "very" large.
No it doesn't but that's a nice side effect and not only for the
selected window, I don't want the user messes up the layout if
`zoom-mode` is enabled, for the same reason I disable the mouse drag
events on windows borders (I noticed that under certain circumstances
some glitches appear).
Andrea Cardaci
2018-05-03 09:46:42 UTC
Permalink
Post by Eli Zaretskii
If you mean that the selected window is resized as result of creating
a new window, then yes, "window is resized" takes care of this. (What
about deleting a window, btw?)
Yes I mean that and the same applies when a window is deleted.
Post by Eli Zaretskii
No, because pre-redisplay-functions are run _before_ anything
significant in redisplay, so your chances to confuse redisplay are
nil.
Got it, thanks.
Post by Eli Zaretskii
Well, as you said, the main purpose of Zoom is to prevent resizing of
the selected window due to Emacs's own considerations. My
interpretation of this was that you want to control the size of the
selected window, and disallow anything else changing it. So if you
change the size at select-window time, and the size is not allowed to
change after that, you have reached your purpose, right?
Hm maybe, but not only that, the reason why I use `balance-windows`
before resizing the selected window is ensure some consistency in the
sizes of other windows too.
martin rudalics
2018-05-03 07:11:18 UTC
Permalink
Post by Andrea Cardaci
- a window is resized;
- a window is selected;
- a window is created (this is actually included in the "a window is resized"
case);
- a window changes its buffer (because it's possible to exclude certain
windows from zooming so the layout should be updated accordingly).
There are probably other ones like when a window gets deleted or the
configuration changes but all these should get caught by your code.
I'm not sure why hooking 'minibuffer-setup-hook' is needed, I suppose
it is not, at least for Emacs 26. Other than that your code seems
valid to me (even disabling 'window-configuration-change-hook' in
'balance-windows' is OK, that hook should not get called there any
more).

Obviously, 'window-size-change-functions' is meant for applications to
react to size changes and possibly readjust buffer text shown in a
window whose size changed. You should warn other applications to make
sure they _append_ their functions to this hook in order to be aware
of your adjustments. Maybe you should re-prepend your function when
you find out that other ones have been prepended in between.

Otherwise, I would use 'buffer-list-update-hook' instead of advising
'select-window' to make sure that all occurences of selecting a window
get caught. And I would experimentally try to not zoom immediately in
'buffer-list-update-hook' and 'window-configuration-change-hook' but
simply feed these occurrencs to 'pre-redisplay-function' to reduce the
number of times you zoom. 'window-pixel-height-before-size-change'
and 'window-pixel-width-before-size-change' should allow to easily do
the 'window-size-change-functions' part in 'pre-redisplay-function' as
well. Though my personal experiences with 'pre-redisplay-function'
are not bright enough to recommend it for sure.

martin
Andrea Cardaci
2018-05-03 09:47:40 UTC
Permalink
Post by martin rudalics
There are probably other ones like when a window gets deleted or the
configuration changes but all these should get caught by your code.
Yep, I forgot to mention when a window gets deleted.
Post by martin rudalics
I'm not sure why hooking 'minibuffer-setup-hook' is needed, I suppose
it is not, at least for Emacs 26. Other than that your code seems
valid to me (even disabling 'window-configuration-change-hook' in
'balance-windows' is OK, that hook should not get called there any
more).
`minibuffer-setup-hook` is needed to detect when the minibuffer is
selected because there's an option to preserve the currently zoomed
window in that case to avoid quick and useless layout changes.
Post by martin rudalics
Obviously, 'window-size-change-functions' is meant for applications to
react to size changes and possibly readjust buffer text shown in a
window whose size changed. You should warn other applications to make
sure they _append_ their functions to this hook in order to be aware
of your adjustments. Maybe you should re-prepend your function when
you find out that other ones have been prepended in between.
That's a interesting advice.
Post by martin rudalics
Otherwise, I would use 'buffer-list-update-hook' instead of advising
'select-window' to make sure that all occurences of selecting a window
get caught. And I would experimentally try to not zoom immediately in
'buffer-list-update-hook' and 'window-configuration-change-hook' but
simply feed these occurrencs to 'pre-redisplay-function' to reduce the
number of times you zoom. 'window-pixel-height-before-size-change'
and 'window-pixel-width-before-size-change' should allow to easily do
the 'window-size-change-functions' part in 'pre-redisplay-function' as
well. Though my personal experiences with 'pre-redisplay-function'
are not bright enough to recommend it for sure.
I will try this approach, thanks.
Andrea Cardaci
2018-05-07 12:32:28 UTC
Permalink
Post by Andrea Cardaci
Post by martin rudalics
Otherwise, I would use 'buffer-list-update-hook' instead of advising
'select-window' to make sure that all occurences of selecting a window
get caught. And I would experimentally try to not zoom immediately in
'buffer-list-update-hook' and 'window-configuration-change-hook' but
simply feed these occurrencs to 'pre-redisplay-function' to reduce the
number of times you zoom. 'window-pixel-height-before-size-change'
and 'window-pixel-width-before-size-change' should allow to easily do
the 'window-size-change-functions' part in 'pre-redisplay-function' as
well. Though my personal experiences with 'pre-redisplay-function'
are not bright enough to recommend it for sure.
I will try this approach, thanks.
I've been trying this, I like the separation between checking whether
a relayout is needed or not and the actual relayout.

There's one problem with `buffer-list-update-hook` though, it gets
called (multiple times) even wen the buffer list is not changed, e.g.,
simply by clicking in the buffer. Is this the expected behaviour?

Besides this, if there's no way to get rid of false positives in event
handling (i.e., a relayout is triggered but no actual change happened)
I guess I need a way to decide if a relayout is *really* needed. A
naive way would be saving the current window and buffer but I'm not
sure if we can do better.

Also, oddly enough, `pre-redisplay-function` is never called on macOS
(Emacs 26.1)...
Eli Zaretskii
2018-05-07 18:19:16 UTC
Permalink
Date: Mon, 7 May 2018 14:32:28 +0200
There's one problem with `buffer-list-update-hook` though, it gets
called (multiple times) even wen the buffer list is not changed, e.g.,
simply by clicking in the buffer. Is this the expected behaviour?
I don't remember (perhaps Martin does). But if you show a C-level
backtrace from such a call to buffer-list-update-hook, it will be easy
to say whether this is expected or not.
Besides this, if there's no way to get rid of false positives in event
handling (i.e., a relayout is triggered but no actual change happened)
Do you mean that pre-redisplay-function is called? If not, what
exactly do you mean by "relayout is triggered"?
Also, oddly enough, `pre-redisplay-function` is never called on macOS
(Emacs 26.1)...
That's strange. The most frequent call to pre-redisplay-function is
in prepare_menu_bars; are you saying that function is never called on
macOS? If you put a breakpoint inside that function, does it never
break?
martin rudalics
2018-05-10 10:37:54 UTC
Permalink
It doesn't because keeping just the window object doesn't retain the
(setq x (selected-window))
(switch-to-buffer "foo")
(setq y (selected-window))
(equal x y) ; t
So formatting is IMHO a nice workaround to storing also the buffer name.
You're right.

martin

Loading...