DEV Community

Cover image for How I made a Desktop Environment in the Browser (Part 1: Window Manager)
Dustin Brett
Dustin Brett

Posted on

How I made a Desktop Environment in the Browser (Part 1: Window Manager)

Welcome to my series on how I built a Desktop Environment in the browser.

This is actually my 3rd version of a desktop environment and by far the one which I gave my most dedication. The first try was using Angular 8 and I only spent a month working on it with no intention of taking it too serious. The second try took me 4 months to make and was a bit more functional. But for my latest version I decided to do a total rewrite from scratch.

I took the opportunity of a total redo to begin live streaming my coding progress for the project on my YouTube channel. In the end I streamed 52 weeks in a row until the end of 2021 at which time I published the web app as my personal website.

Window Manager

Window Manager

The first topic I'd like to discuss in this series is the Window Manager which is responsible for displaying the component for each of the apps that exists within the processes object. The processes are stored in a React context. Then within the Next.js's index page I load a component called the AppsLoader which will render the component in each process.

AppsLoader.tsx

import { ProcessConsumer } from "contexts/process";
import dynamic from "next/dynamic";

const RenderComponent = dynamic(
  () => import("components/RenderComponent")
);

const AppsLoader = (): JSX.Element => (
  <ProcessConsumer>
    {({ processes = {} }) => (
      {Object.entries(processes)
        .map(([id, { Component, hasWindow }]) => (
          <RenderComponent
            key={id}
            Component={Component}
            hasWindow={hasWindow}
            id={id}
          />
        ))}
    )}
  </ProcessConsumer>
);

export default AppsLoader;
Enter fullscreen mode Exit fullscreen mode

Rather than just render the component directly I pass it to an wrapper component called RenderComponent that will conditionally render the component wrapped around a Window depending on if it needs to be contained in a window.

RenderComponent.ts

import dynamic from "next/dynamic";

const Window = dynamic(() => import("components/Window"));

export type ComponentProcessProps = {
  id: string;
};

type RenderComponentProps = {
  Component: React.ComponentType<ComponentProcessProps>;
  hasWindow?: boolean;
  id: string;
};

const RenderComponent = ({
  Component,
  hasWindow = true,
  id,
}: RenderComponentProps): JSX.Element =>
  hasWindow ? (
    <Window id={id}>
      <Component id={id} />
    </Window>
  ) : (
    <Component id={id} />
  );

export default RenderComponent;
Enter fullscreen mode Exit fullscreen mode

If the process is removed from the process context object then it is equivalent to closing the window as once it's no longer in the process context the <AppsLoader /> will re-render without that component. Now that we have the ability to open an app which shows it's component wrapped in a window, we can look at what the window does with that component.

The way I have setup this component is yet another wrapper, this time wrapping the functionality of dragging and resizing as well as styling the section HTML5 container element. The library I used for dragging and resizing is called react-rnd. For the styling I used Styled Components.

Window.ts

import type {
  ComponentProcessProps
} from "components/RenderComponent";
import { StyledTitlebar, Titlebar } from "components/Window/Titlebar";
import React from "react";
import { Rnd } from "react-rnd";
import styled from "styled-components";

const StyledWindow = styled.section`
  contain: strict;
  height: 100%;
  overflow: hidden;
  width: 100%;

  ${StyledTitlebar} + * {
    height: ${({ theme }) => `calc(100% - ${theme.titleBar.height})`};
  }
`;

const Window = ({
  children,
  id,
}: React.PropsWithChildren<ComponentProcessProps>): JSX.Element => (
  <Rnd dragHandleClassName="dragHandle">
    <StyledWindow>
      <Titlebar id={id} />
      {children}
    </StyledWindow>
  </Rnd>
);

export default Window;
Enter fullscreen mode Exit fullscreen mode

The usage of <Rnd /> is best explained on their GitHub page, I've assigned a dragHandleClassName which will be added to the titlebar to allow dragging the window from there. The <StyledWindow /> is define above the component and has a few styling choices worth mentioning.

I've used contain in an attempt to eke out a little performance boost. Then I've set this section to be 100% dimensions so it takes up all space within the <Rnd /> container element. I've also set overflow to hidden to prevent any content getting outside the window. Finally I have the process component which comes directly after the <StyledTitlebar /> set it's height to be 100% but subtracting the height of the <Titlebar /> component which will be at the top of the window to show it's icon, title & minimize/maximize/close buttons.

Titlebar.tsx

import {
  CloseIcon,
  MaximizeIcon,
  MinimizeIcon
} from "components/Window/Icons";
import { useProcesses } from "contexts/process";
import styled from "styled-components";
import { Button, Icon } from "styles/common";

type TitlebarProps = {
  id: string;
};

const StyledTitlebar = styled.header`
  height: ${({ theme }) => theme.titleBar.height};
  position: relative;
  top: 0;
`;

const Titlebar = ({ id }: TitlebarProps): JSX.Element => {
  const {
    close,
    maximize,
    minimize,
    processes: { [id]: process },
  } = useProcesses();
  const { icon, title } = process || {};

  return (
    <StyledTitlebar className="dragHandle">
      <h1>
        <figure>
          <Icon alt={title} imgSize={16} src={icon} />
          <figcaption>{title}</figcaption>
        </figure>
      </h1>
      <nav>
        <Button onClick={() => minimize(id)} title="Minimize">
          <MinimizeIcon />
        </Button>
        <Button onClick={() => maximize(id)} title="Maximize">
          <MaximizeIcon />
        </Button>
        <Button onClick={() => close(id)} title="Close">
          <CloseIcon />
        </Button>
      </nav>
    </StyledTitlebar>
  );
};

export default Titlebar;
Enter fullscreen mode Exit fullscreen mode

Finally the titlebar component above displays information about the process as well as controls for setting the minimize/maximize states within the process context or to close the window by removing the process from the context. I've left out details about the icons and styling but at this point you could style this anyway you like.

With this code we now have a mechanism of representing components in our app dynamically as they are added to the process context. The elements will be added to the DOM and go through the typical lifecycle for a React component.

If you would like a detailed overview of my desktop environment features while you wait for my next part to be released, please check it out below as well as like/subscribe if you enjoy the content.

Thanks for reading this topic in my series. I am not sure which topic I will cover for the next part but in this series I at least plan to discuss the Start Menu, Taskbar, File System/Manager, Wallpaper, Clock & lots more. I will also get into some specifics "apps" that were fun/challenging to make.

Top comments (0)