xmonad icon indicating copy to clipboard operation
xmonad copied to clipboard

Add option to create borders externally

Open incertia opened this issue 9 years ago • 8 comments

Problem Description

Borders are created internally, so if borderWidth = 2, a call to xcb_create_window() with width=1280 and height=720 and borderwidth = 0 ends up creating a client area measuring 1276 x 716. For graphics applications that require a fixed window size, this can be particularly annoying if you have to wait for the window to become exposed to resize the window.

An .xinitrc file consisting of precisely

#! /bin/bash
exec xterm

can create borders that go outward like so, confirmed via xcb_get_geometry having the correct client area.

Configuration File

module Main (main) where

import XMonad

main :: IO ()
main = xmonad def

Checklist

incertia avatar Feb 15 '17 21:02 incertia

@incertia You seem to know way more about this than I do. Any chance you want to submit a patch?

pjones avatar Mar 14 '17 15:03 pjones

@pjones @incertia if you don't mind I'll try to tackle this question.

erthalion avatar Apr 14 '17 19:04 erthalion

@erthalion Got for it!

pjones avatar Apr 14 '17 20:04 pjones

An .xinitrc file consisting of precisely ... can create borders that go outward like so

Well, I'm really confused by this statement. I couldn't find any mention of external/outside borders in libx11 source code, xcb_get_geometry_reply_t (a structure related to the xcb_get_geometry) also doesn't contain any information, that may help to detect whether borders are external or internal.

I couldn't also reproduce a situation when borders are outside of a window using provided .xinitrc and the xmonad config. So it would be nice if @incertia will explain this point in more details.

For graphics applications that require a fixed window size

So, at this point I see following options:

  • I'm wrong and it's possible to create borders outside of a window, so we can use this feature.

  • "Emulate" external borders by creating a window with dimensions window_width + 2 * border_width x window_heigth + 2 * border_width. But it means some overhead for the operations with window sizes, because it's necessary to calculate it since we're pretending that dimensions are still window_width x window_heigth.

  • It really makes sense only for graphics applications with fixes window size (I assume it's mostly about games), so it would be great to be able to differentiate those apps and just remove borders for them (but I almost sure that it's impossible).

I think it also worth to mention that I see similar problems when I toggle panels (e.g. xmobar).

erthalion avatar Apr 23 '17 20:04 erthalion

Super late reply, but here's another attempt at explaining what I'm trying to get at:

Other window managers/desktop sessions will draw window decorators around the client area. Specifically, in the case of cinnamon, when a window is displayed and then unmapped/destroyed, I check its geometry with xcb_get_geometry and it reports 1600 x 900 with border=0. When I run the same application within xmonad it reports 1596 x 896 with border=2. Under the basic xterm xinitrc, it reports as 1600 x 900 with border=0. Creating the X11 window with the border width set to 2 yields client areas of size 1600 x 900 in all 3 sessions.

My xmonad config is mostly just

 defaults xmb_input xmb_notif = defaultConfig
      { modMask               = mod4Mask
      , manageHook            = myManageHook <+> manageDocks
      , layoutHook            = avoidStruts $ myLayoutHook
      , startupHook           = myStartupHook
      , logHook               = myLogHook xmb_input xmb_notif
      , handleEventHook       = docksEventHook
      , terminal              = "urxvtc"
      , normalBorderColor     = "#63c0f5"
      , focusedBorderColor    = "#00d000"
      , borderWidth           = 2 -- <-- the important line here
      , workspaces            = myWorkspaces }

The window creation code is nothing fancy:

  xcb_create_window(app->xc,
                    XCB_COPY_FROM_PARENT,
                    app->xw,
                    screen->root,
                    (screen->width_in_pixels - w) / 2,
                    (screen->height_in_pixels - h) / 2,
                    w, h, // width, height
                    0,  // border
                    XCB_WINDOW_CLASS_INPUT_OUTPUT,
                    screen->root_visual,
                    mask, values);
  xcb_change_property(app->xc, XCB_PROP_MODE_REPLACE, app->xw,
                      app->ewmh->_NET_WM_WINDOW_TYPE, XCB_ATOM_ATOM,
                      32, 1, &app->ewmh->_NET_WM_WINDOW_TYPE_NORMAL);
  memset(&size_hints, 0, sizeof(xcb_size_hints_t));
  memset(&wm_hints, 0, sizeof(xcb_icccm_wm_hints_t));
  xcb_icccm_size_hints_set_base_size(&size_hints, w, h);
  xcb_icccm_size_hints_set_min_size(&size_hints, w, h);
  xcb_icccm_size_hints_set_max_size(&size_hints, w, h);
  xcb_icccm_size_hints_set_win_gravity(&size_hints, XCB_GRAVITY_CENTER);
  xcb_icccm_set_wm_size_hints(app->xc, app->xw, XCB_ATOM_WM_NORMAL_HINTS,
                              &size_hints);
  xcb_icccm_set_wm_transient_for(app->xc, app->xw, screen->root);
  xcb_icccm_wm_hints_set_normal(&wm_hints);
  xcb_icccm_set_wm_hints(app->xc, app->xw, &wm_hints);

and we report geometry during the event loop as follows:

          case XCB_INPUT_RAW_KEY_RELEASE:
            keysym = get_keysym(app, ev.kr->detail, mods);
            if (keysym == XK_Escape) {
              printf("escape released\n");
              ggc = xcb_get_geometry(app->xc, app->xw);
              ggr = xcb_get_geometry_reply(app->xc, ggc, NULL);
              printf("window size: %u x %u\n", ggr->width, ggr->height);
              printf("border size: %u\n", ggr->border_width);
              xcb_destroy_window(app->xc, app->xw);
              xcb_flush(app->xc);
            }
            break;

tl;dr it seems to me that the default behavior of xmonad borders is to grow into the client area whereas the default behavior from other window managers/desktop sessions do not touch the client area and grow outwards around the client area.

EDIT: this could be a problem inherent to XSetWindowBorderWidth in which case I'm not too sure what the default option should be.

EDIT (again): I have updated my program to use an additional call to xcb_configure_window to set the border width to 2 after initially setting it to 0. xmonad reports 1596 x 896, 2, cinnamon reports 1600 x 900, 0, and xterm reports 1600 x 900, 2. Note that cinnamon reports 0 border width for every configuration.

EDIT (yet again): I took a look at floatLocation and the code seems correct to me, so I am completely clueless as to why XMonad gets a smaller client area.

incertia avatar Dec 26 '19 22:12 incertia

I think "external" borders are usually what you get with reparenting window managers. Rather than drawing outside the window, a new window is created for the border and WM controls, and the client window is embedded inside that.

aij avatar Dec 26 '19 23:12 aij

Aha, I think I tracked down the issue. xmonad is the only window manager which issues a resize request to my application, taking off the border width and resizing it to 1596 x 896. Does it make sense to only do this for tiled windows instead of all visible windows?

incertia avatar Dec 26 '19 23:12 incertia

Here's a hack I put together to theoretically address this issue, I'm not sure if there are any other changes that need to be made. I am also able to make a PR with these changes @pjones @erthalion.

diff --git a/src/XMonad/Operations.hs b/src/XMonad/Operations.hs
index 2845a6e..b25dd07 100644
--- a/src/XMonad/Operations.hs
+++ b/src/XMonad/Operations.hs
@@ -128,7 +128,7 @@ windows f = do
     -- for each workspace, layout the currently visible workspaces
     let allscreens     = W.screens ws
         summed_visible = scanl (++) [] $ map (W.integrate' . W.stack . W.workspace) allscreens
-    rects <- fmap concat $ forM (zip allscreens summed_visible) $ \ (w, vis) -> do
+    rects <- fmap (foldl (\(a, b) (c, d) -> (a ++ c, b ++ d)) ([], [])) $ forM (zip allscreens summed_visible) $ \ (w, vis) -> do
         let wsp   = W.workspace w
             this  = W.view n ws
             n     = W.tag wsp
@@ -151,11 +151,11 @@ windows f = do
 
         io $ restackWindows d (map fst vs)
         -- return the visible windows for this workspace:
-        return vs
+        return (flt, rs)
 
-    let visible = map fst rects
+    let visible = let (floating, tiled) = rects in map fst floating ++ map fst tiled
 
-    mapM_ (uncurry tileWindow) rects
+    mapM_ (uncurry tileWindow) (snd rects)
 
     whenJust (W.peek ws) $ \w -> do
       fbs <- asks (focusedBorderColor . config)

incertia avatar Dec 26 '19 23:12 incertia