Visual Window Snapping

I had thought about adding two more boxes to show up (one left of the current one, and one right of it). It would allow the same positions just on the left or right monitor. This can be achieved using BetterTouchTool's "Custom Move and Resize" predefined action.

Awesome. Would there be any way to have to happen automatically? Meaning if you have 1, 2, or 3 screens it would detect that and handle it appropriately?

First things first: @panda I absolutely love this! Thank you for sharing it.

The second thing is after reading @GoldenChaos's comment on handling GUI from awhile ago, I saw that the functionality was implemented into his touch bar preset, but I was really hoping for more of a separate interface due to the fact that I don't have a touch bar.

Anyways, this is what I have so far!
image

That said, I am afraid that I am not exactly a deft hand at javascript. My goal is for the specific 'button' to have a slightly darker shade to it, while the representation of the position on the screen is a lighter gray. I am not the greatest with words, so allow me to present my quick and dirty Illustrator mockup to illustrate my point:

image
I got half way to my goal by using :hover in CSS for the corner buttons, but ultimately my lack of javascript knowledge did me in. :frowning:

I definitely still plan on working on this in the future, I just haven't gotten around to it - but it looks like you've made some solid progress! Mind posting yours here?

1 Like

That is fantastic news. I was initially going to post mine but I wasn't 100% sure on the rules around that. I will attach it!

VisualSnapping.bttpreset (20.4 KB)

1 Like

Hi!

This works quite well and is pretty useful! I've assigned it to show up when you right click the green window button, and additionally rounded the corners a bit more.

@Panda, I was wondering if I could feature @Jerosh's edit within my public preset AquaTouch, would you mind if I merge this in? I'll be sure to link this forum post within it's code and feature you two in the changelog!

1 Like

Just want to notify;

@panda, @Jerosh, I've just released a new AquaTouch version with this preset implemented. If you'd like to have it removed, then please let me know! Otherwise, a quick thumbs up would be helpful for me. Thanks!

1 Like

Definitely fine with me! Sorry I haven't made any modifications to this after posting here, I have been quite busy lately. In case you need any changes, please let me know.

2 Likes

Thanks for the permission!

1 Like

FINALLY cleaned this up!

I'm still getting things to highlight correctly, but it's about done. Coming next experimental release :metal:

11%20AM

Visual Window Snapping has evolved once again.

19%20pm 22%20pm 24%20pm 48%20pm

  • Edge and Centre highlighting done!

  • Refined GoldenChaos's Translucency to better match the macOS transparency

  • Fade animations now use the EaseOutExpo curve, which feels more responsive than a linear curve but has the same duration.

  • Completley redid how the borders are displayed. They now use actual borders instead of gridgaps. (the gridgaps faded away when you opened it up on a dark background. Hard to see.)

  • More rounded corners (i just like it rounder)

I'm not planning to release this as of yet, as I want to make it dynamically switch to a light style if the user is using light mode.

I also want to make two buttons appear automatically if a display is connected for move to display buttons.

It's visually not exactly as I wanted, sometimes the highlight can overexpose it's backgrounds and I'd like to fix and refine that over more time.

1 Like

A great way to handle multiple displays would be to somehow detect the second display and its position (left or right of the current display), and show an entire second window snapper box next to the original window snapper box in the correct position. Then, to move a window to the external display, you'd simply use the second window snapper box. I would personally use the ever-loving crap out of it.

That sounds like a great idea! I was thinking about how to handle switching desktops and snapping in one click.

I already have the ability to detect the amount of displays present, but I havent found a way to detect or keep track of which one is currently being used and directions.

i think instead of having two squares, which would get so confusing to users, you’d have a little square to the left and right with an icon on them. As soon as you mouseover, the main square would pop over and swallow up the icon, while a similar icon appears where the nain square was to allow you to snap in the same display.

I’ll get a mockup up soon. I’m good at figuring things out, but I’m still very weak and learning code so I’d appreciate some help! (Probably once I get my iteration refined enough i’ll upload it here for some help)

I think the animations involved in the variant you've described sound like overkill; perhaps just having a slightly smaller window snapper grid to represent an external display will provide enough visual distinction to make it clear that it isn't for snapping to the primary display.

I’ll experiment, and refine. I have a high standard of quality.

Hey, iOS animations are pretty overkill too, and anyway, i’m not skilled with CSS and HTML so that’ll limit me quite a bit!

1 Like

Ahhhaha sorry, I didn't mean it to sound so critical! I think it'll be less work to make a non-animated snapper anyway :stuck_out_tongue: but if you still wanna do it animated then go for it!

Its okay, feedback is valuable and i really take it seriously. It’s quite hard for me to get too ‘judged’ by feedback as I usually take it as tips to improve.

Also maybe that explanation is a bit much making it sound complicated, but I imagine it’s motion to be smooth and all-at-once (much like iPhone X gestures)

Thanks for your feedback!

Just worked on it again. it's so close to what I wanted it to be:

winsnap2%402x 36%20am

More Shots

06%20am
33%20am


19%20am

Excuse my uncropped and unproportionate screenshots :slight_smile:


Here's what I did:

COMPLETLEY cleaned up how the widget was rendered. (my it was a mess)

Changes in the code:
Before:

  • the grid-container did the blur and shaders
  • each cell overlayed a darken onto the grid-container
  • each cell had a grid-gap that let the lighter grid-container shine through for the grid borders
  • when mouseover, the cells become semi-transparent to allow the lighter grid-container to shine through

After:

  • grid-container does the blur, shaders, darken, all the colouring.
  • each cell is fully transparent, letting the grid-container through untouched.
  • grid gaps removed and replaced with normal css borders now. This allows more control over them.
  • when mouseover, the cells become opaque/overlay the grid-container with whatever effect you want.
  • the moseover style of the BUTTON is now seperate from the REPRESENTATION style. You can style the button seperate to the representation now.

I made the button mouseover style red for my debug purposes, Here's what I saw while I was debugging:

11%20pm 06%20pm


After I redid how it all works, I (painstakingly) refined and adjusted the widget a bit more.

  • Reduce over-saturation / over-exposure
  • Refined the mouseover styles
  • Thicken the grid borders (now 2px, easier to see for not so good eyes)
  • Dimmed the grid borders
  • moved all repeating attributes to a single .cell class for easier editing and a minimised script (e.g. 'border-width: 1px' is not in every single cell class anymore, instead it is in one ‘.cell’ class that’s added to all cells)

BUT,
I'm having trouble with the grid borders. I want them to have the hard-light blend mode but I can't seem to assign it to them, they just don't respond to it.

It may be small but I guess this is where I'm stuck. Oh well, if this can't be fixed i'll just settle for the awfully flat looking, grey grid borders


"I Want to Try It" you say?

I'll just release the updated code for now. It's still missing the dynamic dark-mode sensing, desktop switching snapping and other stuff, but if you're itching to look into the new refinements then here's the code:

Show HTML Code
<!--
VISUAL WINDOW SNAPPING by panda modified by Jerosh.
Forum link: https://community.folivora.ai/t/visual-window-snapping/2863

This one was refined by GoldenChaos, then refined further by me, yyuuiko!
-->

<html>
<head>
    <style>

       .grid-container { /* The MAIN BACKGROUND of the pallete */
            display: grid;
            height: 150px;
            width: 150px;
            margin: 40px;
/**/            padding: 0px;
            background-color: rgba(30,30,30,0.7);
            -webkit-backdrop-filter: saturate(180%) blur(20px) brightness(100%);
            border-radius: 38px;
            -webkit-background-clip: padding-box;
            background-clip: padding-box;
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr 1fr;
            grid-gap: 0px 0px;
            grid-template-areas: "TopLeft TopRight""BottomLeft BottomRight";
            box-shadow: rgba(0,0,0,.5) 0 0 0 1px, rgba(0,0,0,.5) 0 12px 50px;
            overflow: hidden;
/**/            border: 1.5px solid rgba(255,255,255,0.7);
        }

        .highlighted { /* -------------------------------FADE (Representation)------------------------------------ */
          -webkit-backdrop-filter: brightness(150%) saturate(180%);
          -webkit-transition: all 600ms cubic-bezier(0.19, 1, 0.22, 1); /* easeOutExpo */
          transition: all 600ms cubic-bezier(0.19, 1, 0.22, 1); /* easeOutExpo */
        }

        .highlightCell {
          -webkit-backdrop-filter: brightness(120%);
          background-color: rgba(255,255,255,.4);
          -webkit-transition: -webkit-backdrop-filter 600ms cubic-bezier(0.19, 1, 0.22, 1);
          transition: -webkit-backdrop-filter 600ms cubic-bezier(0.19, 1, 0.72, 1);

          -webkit-transition: background-color 300ms cubic-bezier(0.19, 1, 0.22, 1);
          transition: background-color 300ms cubic-bezier(0.19, 1, 0.72, 1);
        }

        .cell {
/**/            border: solid rgba(255,255,255,.5);
/**/            mix-blend-mode: hard-light;
        }

        .TopLeft {
            display: grid;
            grid-area: TopLeft;
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr 1fr;
/**/            grid-gap: 0px 0px;
            grid-template-areas: "TopLeftTopLeft TopLeftTopRight""TopLeftBottomLeft TopLeftBottomRight";
            border-top-left-radius: 36px;
          }

        .TopLeftTopLeft {
            grid-area: TopLeftTopLeft;
            border-top-left-radius: 36px;
/**/            border-width: 0px 1px 1px 0px;
        }

        .TopLeftBottomLeft {
            grid-area: TopLeftBottomLeft;
/**/            border-width: 1px 1px 0px 0px;
        }

        .TopLeftTopRight {
            grid-area: TopLeftTopRight;
/**/            border-width: 0px 0px 1px 1px;
        }

        .TopLeftBottomRight {
            grid-area: TopLeftBottomRight;
/**/            border-width: 1px 0px 0px 1px;
        }

        .TopRight {
            display: grid;
            grid-area: TopRight;
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr 1fr;
/**/            grid-gap: 0px 0px;
            grid-template-areas: "TopRightTopLeft TopRightTopRight""TopRightBottomLeft TopRightBottomRight";
            border-top-right-radius: 36px;
        }

        .TopRightTopLeft {
            grid-area: TopRightTopLeft;
/**/            border-width: 0px 1px 1px 0px;
        }

        .TopRightTopRight {
            grid-area: TopRightTopRight;
            border-top-right-radius: 36px;
/**/            border-width: 0px 0px 1px 1px;
 -webkit-background-clip: padding-box;
background-clip: padding-box;
        }

        .TopRightBottomLeft {
            grid-area: TopRightBottomLeft;
/**/            border-width: 1px 1px 0px 0px;

        }

        .TopRightBottomRight {
            grid-area: TopRightBottomRight;
/**/            border-width: 1px 0px 0px 1px;
        }



        .BottomLeft {
            display: grid;
            grid-area: BottomLeft;
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr 1fr;
/**/            grid-gap: 0px 0px;
            grid-template-areas: "BottomLeftTopLeft BottomLeftTopRight""BottomLeftBottomLeft BottomLeftBottomRight";
            border-bottom-left-radius: 36px;
        }

        .BottomLeftTopLeft {
            grid-area: BottomLeftTopLeft;
/**/            border-width: 0px 1px 1px 0px;

        }

        .BottomLeftTopRight {
            grid-area: BottomLeftTopRight;
/**/            border-width: 0px 0px 1px 1px;

        }

        .BottomLeftBottomLeft {
            grid-area: BottomLeftBottomLeft;
            border-bottom-left-radius: 36px;
/**/            border-width: 1px 1px 0px 0px;
        }

        .BottomLeftBottomRight {
            grid-area: BottomLeftBottomRight;
/**/            border-width: 1px 0px 0px 1px;
        }



        .BottomRight {
            display: grid;
            grid-area: BottomRight;
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr 1fr;
            border-bottom-right-radius: 36px;
/**/            grid-gap: 0px 0px;
            grid-template-areas: "BottomRightTopLeft BottomRightTopRight""BottomRightBottomLeft BottomRightBottomRight";
        }

        .BottomRightTopLeft {
            grid-area: BottomRightTopLeft;
/**/            border-width: 0px 1px 1px 0px;

        }

        .BottomRightTopRight {
            grid-area: BottomRightTopRight;
/**/            border-width: 0px 0px 1px 1px;
        }

        .BottomRightBottomLeft {
            grid-area: BottomRightBottomLeft;
/**/            border-width: 1px 1px 0px 0px;
        }

        .BottomRightBottomRight {
            grid-area: BottomRightBottomRight;
            border-bottom-right-radius: 36px;
/**/            border-width: 1px 0px 0px 1px;
        }
    </style>

    <script>
        window.currentlyHighlighted = [];

        //████████████████████████████- Add Highlight FX for Representation-██████████████
        function addHighlight(classNames) {
            for (var i = 0; i < classNames.length; i++) {
                window.currentlyHighlighted.push(classNames[i]);
                document.getElementsByClassName(classNames[i]).item(0).classList.add('highlighted');
            }
        }

        //████████████████████████████- Remove Highlight FX for Representation -██████████████
        function removeHighlight(classNames) {
            for (var i = 0; i < classNames.length; i++) {
                window.currentlyHighlighted = window.currentlyHighlighted.filter(e => e !== classNames[i]);
                document.getElementsByClassName(classNames[i]).item(0).classList.remove('highlighted');
                //document.getElementsByClassName(classNames[i]).item(0).classList.add('highlightOff');
            }
        }


        //████████████████████████████- Add Highlight FX for Button -██████████████
        function addCellHighlight(classNames) {
            for (var i = 0; i < classNames.length; i++) {
                window.currentlyHighlighted.push(classNames[i]);
                document.getElementsByClassName(classNames[i]).item(0).classList.add('highlightCell');
            }
        }

        //████████████████████████████- Remove Highlight FX for Button-██████████████
        function removeCellHighlight(classNames) {
            for (var i = 0; i < classNames.length; i++) {
                window.currentlyHighlighted = window.currentlyHighlighted.filter(e => e !== classNames[i]);
                document.getElementsByClassName(classNames[i]).item(0).classList.remove('highlightCell');
                //document.getElementsByClassName(classNames[i]).item(0).classList.add('highlightOff');
            }
        }

        function uniq(a) {
            var prims = {
                    "boolean": {},
                    "number": {},
                    "string": {}
                },
                objs = [];

            return a.filter(function(item) {
                var type = typeof item;
                if (type in prims)
                    return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true);
                else
                    return objs.indexOf(item) >= 0 ? false : objs.push(item);
            });
        }

        function performAction() {
            console.log(uniq(window.currentlyHighlighted));
            var trigger = '';
            if (window.currentlyHighlighted.indexOf('TopLeft') !== -1 &&
                window.currentlyHighlighted.indexOf('TopRight') !== -1 &&
                window.currentlyHighlighted.indexOf('BottomLeft') !== -1 &&
                window.currentlyHighlighted.indexOf('BottomRight') !== -1) {
                trigger = 'maximize';
            } else if (window.currentlyHighlighted.indexOf('TopLeft') !== -1 &&
                window.currentlyHighlighted.indexOf('TopRight') !== -1) {
                trigger = 'tophalf';
            } else if (window.currentlyHighlighted.indexOf('BottomLeft') !== -1 &&
                window.currentlyHighlighted.indexOf('BottomRight') !== -1) {
                trigger = 'bottomhalf';
            } else if (window.currentlyHighlighted.indexOf('TopLeft') !== -1 &&
                window.currentlyHighlighted.indexOf('BottomLeft') !== -1) {
                trigger = 'lefthalf';
            } else if (window.currentlyHighlighted.indexOf('TopRight') !== -1 &&
                window.currentlyHighlighted.indexOf('BottomRight') !== -1) {
                trigger = 'righthalf';
            } else if (window.currentlyHighlighted.indexOf('TopRight') !== -1) {
                trigger = 'topright';

            } else if (window.currentlyHighlighted.indexOf('TopLeft') !== -1) {
                trigger = 'topleft';

            } else if (window.currentlyHighlighted.indexOf('BottomLeft') !== -1) {
                trigger = 'bottomleft';

            } else if (window.currentlyHighlighted.indexOf('BottomRight') !== -1) {
                trigger = 'bottomright';
            }

            console.log('trigger', trigger);
            window.BTT.callHandler('trigger_named', {
                trigger_name: trigger,
                closeFloatingHTMLMenu: 1
            })
        }
    </script>
</head>
<body>
    <div class="grid-container" onclick="performAction()">
        <div class="TopLeft"                         onmouseover="addHighlight(['TopLeft']);" onmouseout="removeHighlight(['TopLeft']);">
            <div class="area-overlap TopLeftTopLeft cell" onmouseover="addHighlight(['TopLeftTopLeft']); addCellHighlight(['TopLeftTopLeft'])" onmouseout="removeHighlight(['TopLeftTopLeft']); removeCellHighlight(['TopLeftTopLeft'])"></div>
            <div class="TopLeftBottomLeft cell"           onmouseover="addHighlight(['BottomLeft', 'TopLeft']); addCellHighlight(['TopLeftBottomLeft', 'BottomLeftTopLeft'])" onmouseout="removeHighlight(['BottomLeft', 'TopLeft']); removeCellHighlight([ 'TopLeftBottomLeft', 'BottomLeftTopLeft'])"></div>
            <div class="TopLeftTopRight cell"             onmouseover="addHighlight(['TopRight', 'TopLeft']); addCellHighlight(['TopLeftTopRight', 'TopRightTopLeft']);" onmouseout="removeHighlight(['TopRight', 'TopLeft']); removeCellHighlight([ 'TopLeftTopRight', 'TopRightTopLeft'])"></div>
            <div class="TopLeftBottomRight cell"          onmouseover="addHighlight(['TopRight', 'BottomRight', 'BottomLeft', 'TopLeft']); addCellHighlight(['TopLeftBottomRight', 'TopRightBottomLeft', 'BottomLeftTopRight', 'BottomRightTopLeft'])" onmouseout="removeHighlight(['TopRight', 'BottomRight', 'BottomLeft', 'TopLeft']); removeCellHighlight(['TopLeftBottomRight', 'TopRightBottomLeft', 'BottomLeftTopRight', 'BottomRightTopLeft'])"></div>
        </div>

        <div class="TopRight"                        onmouseover="addHighlight(['TopRight'])" onmouseout="removeHighlight(['TopRight'])">
            <div class="area-overlap TopRightTopLeft cell"onmouseover="addHighlight(['TopRight', 'TopLeft']); addCellHighlight([ 'TopLeftTopRight', 'TopRightTopLeft'])" onmouseout="removeHighlight(['TopRight', 'TopLeft']); removeCellHighlight(['TopLeftTopRight', 'TopRightTopLeft'])"></div>
            <div class="TopRightTopRight cell"            onmouseover="addHighlight(['TopRightTopRight']); addCellHighlight(['TopRightTopRight'])" onmouseout="removeHighlight(['TopRightTopRight']); removeCellHighlight(['TopRightTopRight'])"></div>
            <div class="TopRightBottomLeft cell"          onmouseover="addHighlight(['TopRight', 'BottomRight', 'BottomLeft', 'TopLeft']); addCellHighlight(['TopLeftBottomRight', 'TopRightBottomLeft', 'BottomLeftTopRight', 'BottomRightTopLeft'])" onmouseout="removeHighlight(['TopRight', 'BottomRight', 'BottomLeft', 'TopLeft']); removeCellHighlight(['TopLeftBottomRight', 'TopRightBottomLeft', 'BottomLeftTopRight', 'BottomRightTopLeft'])"></div>
            <div class="TopRightBottomRight cell"         onmouseover="addHighlight(['TopRight', 'BottomRight']); addCellHighlight(['TopRightBottomRight', 'BottomRightTopRight'])" onmouseout="removeHighlight(['TopRight', 'BottomRight']); removeCellHighlight(['TopRightBottomRight', 'BottomRightTopRight'])"></div>
        </div>

        <div class="BottomLeft"                      onmouseover="addHighlight(['BottomLeft'])" onmouseout="removeHighlight(['BottomLeft'])">
            <div class="area-overlap BottomLeftTopLeft cell" onmouseover="addHighlight(['BottomLeft', 'TopLeft']); addCellHighlight([ 'TopLeftBottomLeft', 'BottomLeftTopLeft'])" onmouseout="removeHighlight(['BottomLeft', 'TopLeft']); removeCellHighlight(['TopLeftBottomLeft', 'BottomLeftTopLeft'])"></div>
            <div class="BottomLeftTopRight cell"          onmouseover="addHighlight(['TopRight', 'BottomRight', 'BottomLeft', 'TopLeft']); addCellHighlight([ 'TopLeftBottomRight', 'TopRightBottomLeft', 'BottomLeftTopRight', 'BottomRightTopLeft'])" onmouseout="removeHighlight(['TopRight', 'BottomRight', 'BottomLeft', 'TopLeft']); removeCellHighlight(['TopLeftBottomRight', 'TopRightBottomLeft', 'BottomLeftTopRight', 'BottomRightTopLeft'])"></div>
            <div class="BottomLeftBottomLeft cell"        onmouseover="addHighlight(['BottomLeftBottomLeft']); addCellHighlight(['BottomLeftBottomLeft'])" onmouseout="removeHighlight(['BottomLeftBottomLeft']); removeCellHighlight(['BottomLeftBottomLeft'])"></div>
            <div class="BottomLeftBottomRight cell"       onmouseover="addHighlight(['BottomLeft', 'BottomRight']); addCellHighlight(['BottomRightBottomLeft', 'BottomLeftBottomRight'])" onmouseout="removeHighlight(['BottomLeft', 'BottomRight']); removeCellHighlight([ 'BottomRightBottomLeft', 'BottomLeftBottomRight'])"></div>
        </div>

        <div class="BottomRight"                      onmouseover="addHighlight(['BottomRight'])" onmouseout="removeHighlight(['BottomRight'])">
            <div class="area-overlap BottomRightTopLeft cell" onmouseover="addHighlight(['TopRight', 'BottomRight', 'BottomLeft', 'TopLeft']); addCellHighlight([ 'TopLeftBottomRight', 'TopRightBottomLeft', 'BottomLeftTopRight', 'BottomRightTopLeft'])" onmouseout="removeHighlight(['TopRight', 'BottomRight', 'BottomLeft', 'TopLeft']); removeCellHighlight(['TopLeftBottomRight', 'TopRightBottomLeft', 'BottomLeftTopRight', 'BottomRightTopLeft'])"></div>
            <div class="BottomRightTopRight cell"          onmouseover="addHighlight(['BottomRight', 'TopRight']); addCellHighlight(['TopRightBottomRight', 'BottomRightTopRight'])" onmouseout="removeHighlight(['BottomRight', 'TopRight']); removeCellHighlight(['TopRightBottomRight', 'BottomRightTopRight'])"></div>
            <div class="BottomRightBottomLeft cell"        onmouseover="addHighlight(['BottomLeft', 'BottomRight']); addCellHighlight(['BottomRightBottomLeft', 'BottomLeftBottomRight'])" onmouseout="removeHighlight(['BottomLeft', 'BottomRight']); removeCellHighlight([ 'BottomRightBottomLeft', 'BottomLeftBottomRight'])"></div>
            <div class="BottomRightBottomRight cell"       onmouseover="addHighlight(['BottomRightBottomRight']); addCellHighlight(['BottomRightBottomRight'])" onmouseout="removeHighlight(['BottomRightBottomRight']); removeCellHighlight(['BottomRightBottomRight'])"></div>
        </div>
    </div>
</body>
</html>

Refactoring to use borders was probably not the best idea :stuck_out_tongue: as you have learned, they don't support any fancy blend modes, and they can't blur the stuff behind it. That's why I had specifically refactored it to not use borders, haha.

You can actually stack blend modes, so you can have the cells on top apply additional blending and filters. I didn't do that in my version, but I've since spent a lot more time on recreating macOS's vibrancy effect for my settings menu, so I think I've got it down now. I'll take a swing at this using your updated version and see what I can achieve!

1 Like

Yeah i thought a bit about that last night and I did realise that was the case.

That method is a pain to shade correctly but with the new changes (.cell class, seperate highlighting styles) it should be easier to control and update the look.

Funny though, the corner cells were responding to the blend mode perfectly for some reason. It got me excited but now it seems weird

(still a beginner anywayy ahahh, I’ll take that advice and see what I can do)

Imprint | Privacy Policy