I have decided to start with the UI. This might seem a bit counter-intuitive at first, but there is a system to the madness. The desktop, at least in the early phase, can do a lot without the rest of the system and the core server will be way easier to create when you already have some sort of UI to test it properly with.
Also, the core parts still need a bit of planning and the desktop is a nice thing to work on in the mean time
There is the repo. Right now, that is in commit 'b8d54e8b873a1dc11edd4b9ae2596120baf7fb48', you can just open the local 'index.html' file ant try it out.
Right now, I want a simple desktop with windows and tabs and rio-inspired controls. The contents in the windows is a work for another day, now I want the WM.
I want my tabs to be vertical, I want them to be dynamically addable and removable and I want them to be rearangable via dragging.
I have searched, yet I have not found any library with such functionality. Therefor, I have resorted to the secondary option: AI. I have asked Claude to give me a simple demo of rearangable vertical tabs. Turns out that there is already some HTML support for dragging, so the code wasn't even all that long.
Then I started hacking and molding the initial demo into what I wanted. I started by changing the layout. Then I added the ability to add and delete tabs.
The tab moving systems works on a bunch of event listeners (JS system for triggering functions on events). When you drag one tab into another, it puts the dragged tab either before or after the target, based on if it was originally before or after it. It's not the most polished system, but it works well.
One problems of this implementation, however, is that it relies on closures that are attached to the event listeners. While closures were of great use all through out the codebase, here they meant that I have to reassign them each time the tab count or order changes.
This is fine, but I first need the tabs to release the old listeners. The only proper way to remove listeners requires you to have the exact function assigned, which is hard to do for anonymous closures. Another way to remove all listeners is to clone the node and then replace it with the clone.
And so I did.
It's not the most elegant solution, but it works.
One last touch that I added was renumbering. Each tab handle displays a number. This is mostly because it feels weird for them to not display anything. When I remove or rearange them, the numbers are all weird, which I don't like. So when I alter the tab order, I also renumber them. There is a slight delay, however, just to make it more clear that tabs were, in fact, moved and which ended up where.
OK, so I have the tabs themselves, but now I need to be able to spawn windows within them. For windowing, I chose the WinBox.js library. (not to be confused with WinBox, the Mikrotik UI)
WinBox is nice, because it implements a lot of what I need and gives me enough API to implement the rest. At first, I made windows appear upon clicking the background. For it to differentiate between the windows and the background, one needs the background. A simple div will do.
After binding a windows to the tab container as their root, they will nicely switch with the tabs.
One complain I have is that when I maximise the window, it covers the whole screen, not just the tab content area, so you cannot switch tabs. This could either be fixed with keyboard shortcuts, or I just remove the icon from the window, who knows...
It wouldn't be a rio-like desktop without a proper mouse context menu. Now, I just went straight to Claude, as I expected all context menu solutions to not work like the rio menu
Claude once again delivered and I got once again to hacking, giving the menu a nice API, so that I can make multiple different menus in the future with ease. It basically just takes a list of objects, each containing a name, and a function to be executed upon selection.
But I have stumbled on yet another roadblock. First item I wanted to implement was a simple 'Kill' function. For this function, upon selection your cursor changes to a cross and whatever window you click gets closed.
In the menu code, I found a builtin function that finds the element under the cursor, but how do I call it in global onclick? Well, I came to Claude one more time and I got the following solution:
I make a function to create a 'overlay' div. This div will be put over the entire screen and will have a custom cursor. It also takes a function, which it call upon click with the mouse pos. Before calling it, tho, I remove the overlay.
There was also a version with promises, but I don't like JS async. You can only call async functions from another async functions, so you are better off just making all the functions async, which I didn't feel like doing.
Once again, closures are your friend.
Now to killing the window. Turns out the function to select element under cursor ('document.elementFromPoint') returns only a part of the window representation, and I actually don't need that. I need the JS object which controls the window.
So What I did was separate the windows into lists by their desktop, and then search through them if any collides with the position. Each window stores it's Z index (which grows infinitely, which will surely break things) and it can be quite nicely sorted by it, so that it selects the most top window under the cursor.
Turns out, however, that minimised windows don't change their position and size, so you could still select it by clicking it's ghost. To solve this, I decided to abuse the fact, that you can just add new attributes to objects on the fly. Now the window creation function looks like this:
const windows = {}; function spawnWindow(x, y, width, height, name, root, tag) { const win = new WinBox(name, { root: root, class: ["no-animation"], border: 4, header: 25, x: x, y: y, width: width, height: height, onclose: function(force) { // remove from windows const id = this.id; windows[tag] = windows[tag].filter((w) => w.id != this.id); }, // upon minimization, window still keeps it's size, which can be triggered // by kill and the like. // for this reason, I move it temporarily onminimize: function() { this['prevX'] = this.x; this['prevY'] = this.y; // cannot use this.move() here this.x = -4000; this.y = -4000; }, onrestore: function() { if (this['prevX']) { // cannot assign directly here this.move(this['prevX'], this['prevY']); delete this['prevX']; delete this['prevY']; } }, }) if (!windows[tag]) windows[tag] = []; windows[tag].push(win); return win; }
For more details, refer to the WinBox documentation.
Now to moving and resizing. For the few who don't know rio and are too lazy to try the demo, It works like this: For both, you select it from the menu and click the target window. For moving, you just drag the window to the target place. Because we're retro, you only drag it's outline. For resize, you select window and then drag a rectangle, which will become it's new dimensions.
It was not hard to copy the selection code and make it move/resize instead. I just create a div with red border and put under the overlay. It needs to know some base information like initial window pos, but that is not a problem.
Once again, it triggers a function, so closures are your friend.
If you should learn anything from this b-log, it's that you could always use a bit more closures in your live.
As one could imagine, tho, copying the function wasn't the prettiest solution, so I reached to Claude once more to refactor the code a bit. And it's better now. Nice!
I had to disable animations, because it just looked odd watching the window make it's way to the selected area.
Then I just added functions for minimize, maximize and fullscreen to the menu and made the window focus when you interact with it via the menu.
And that's where we are right now. I know that this b-log was probably not all that enjoyable. But while it might look impressive, because WinBox is a fancy library, the code itself if not all that interesting.
Now it's time to add some content to the windows and write some server code. That should be more exciting I think.