I was eager to learn the implementation details behind windowing GUI systems. Because I am mostly a high level programmer having not much experience in low-level stuff, I had to simulate it instead of implementing the actual one such as Windows Explorer, KDE, Gnome, X11 and Mac OSX Aqua.
There is the list of things you can do in the simulation:
- Move the windows by dragging their title bars.
- Click one to put it on top of other windows.
- Resize in one direction by dragging a border.
- Resize in two directions by dragging a corner.
- Upon resizing, the title will be contracted if does not fit the current width.
MyScreen.java:
package net.coderodde.gui.simulation;
import java.awt.Canvas;
import java.awt.Dimension;
import java.util.List;
import java.awt.Toolkit;
import java.util.ArrayList;
import javax.swing.JFrame;
public class MyScreen {
private final static int FRAME_WIDTH = 1024;
private final static int FRAME_HEIGHT = 768;
private final JFrame frame;
private final Canvas canvas;
private final List<MyWindow> wndList;
private MyWindow activeWindow;
private MyWindow draggedWindow;
private int draggedWindowOffsetX;
private int draggedWindowOffsetY;
private boolean topBorderLocked;
private boolean rightBorderLocked;
private boolean bottomBorderLocked;
private boolean leftBorderLocked;
private MyWindow highlightedWindow;
private boolean disallowDrag;
public MyScreen() {
wndList = new ArrayList<>();
frame = new JFrame("coderodde GUI simulation");
frame.setSize(FRAME_WIDTH, FRAME_HEIGHT);
frame.setResizable(false);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final Dimension realScreenDimension =
Toolkit.getDefaultToolkit().getScreenSize();
frame.setLocation((realScreenDimension.width - frame.getWidth()) / 2,
(realScreenDimension.height - frame.getHeight()) / 2);
canvas = new MyCanvas(frame.getWidth(),
frame.getHeight(),
wndList,
this);
frame.add(canvas);
frame.setVisible(true);
}
public void addWindow(final MyWindow wnd) {
wndList.add(wnd);
if (activeWindow != null) {
activeWindow.active(false);
}
wnd.active(true);
activeWindow = wnd;
}
public void move(final int x, final int y) {
MyWindow target = null;
for (int i = wndList.size() - 1; i >= 0; --i) {
if (wndList.get(i).windowContainsPoint(x, y)) {
target = wndList.get(i);
break;
}
}
if (target == null) {
if (highlightedWindow != null) {
highlightedWindow.clearBorderHighlights();
highlightedWindow = null;
canvas.repaint();
}
return;
}
if (highlightedWindow != null) {
highlightedWindow.clearBorderHighlights();
highlightedWindow = null;
}
if (target.topBorderContainsPoint(x, y)) {
target.highlightBorder(MyWindow.Border.TOP);
highlightedWindow = target;
} else if (target.bottomBorderContainsPoint(x, y)) {
target.highlightBorder(MyWindow.Border.BOTTOM);
highlightedWindow = target;
}
if (target.rightBorderContainsPoint(x, y)) {
target.highlightBorder(MyWindow.Border.RIGHT);
highlightedWindow = target;
} else if (target.leftBorderContainsPoint(x, y)) {
target.highlightBorder(MyWindow.Border.LEFT);
highlightedWindow = target;
}
canvas.repaint();
}
public void press(final int x, final int y) {
MyWindow newActiveWindow = null;
for (int i = wndList.size() - 1; i >= 0; --i) {
if (wndList.get(i).windowContainsPoint(x, y)) {
newActiveWindow = wndList.remove(i);
break;
}
}
if (newActiveWindow == null) {
return;
}
activeWindow.active(false);
activeWindow = newActiveWindow;
wndList.add(activeWindow);
activeWindow.active(true);
boolean doResize = false;
if (activeWindow.topBorderContainsPoint(x, y)) {
topBorderLocked = true;
doResize = true;
} else if (activeWindow.bottomBorderContainsPoint(x, y)) {
bottomBorderLocked = true;
doResize = true;
}
if (activeWindow.leftBorderContainsPoint(x, y)) {
leftBorderLocked = true;
doResize = true;
} else if (activeWindow.rightBorderContainsPoint(x, y)) {
rightBorderLocked = true;
doResize = true;
}
if (!doResize && activeWindow.titleBarContainsPoint(x, y)) {
draggedWindow = activeWindow;
draggedWindowOffsetX = x - draggedWindow.x();
draggedWindowOffsetY = y - draggedWindow.y();
}
if (doResize) {
draggedWindow = activeWindow;
}
canvas.repaint();
}
public void allowDrag() {
disallowDrag = false;
}
public void disallowDrag() {
disallowDrag = true;
}
public void drag(final int x, final int y) {
if (draggedWindow == null) {
return;
}
if (disallowDrag) {
return;
}
if (!(topBorderLocked
|| rightBorderLocked
|| bottomBorderLocked
|| leftBorderLocked)) {
// Move the entire window.
draggedWindow.x(x - draggedWindowOffsetX);
draggedWindow.y(y - draggedWindowOffsetY);
canvas.repaint();
}
// Resize the window.
final int originalX = draggedWindow.x();
final int originalY = draggedWindow.y();
if (topBorderLocked) {
draggedWindow.resizeAtTop(y - originalY);
} else if (bottomBorderLocked) {
draggedWindow.height(y - originalY);
}
if (leftBorderLocked) {
draggedWindow.resizeAtLeft(x - originalX);
} else if (rightBorderLocked) {
draggedWindow.width(x - originalX);
}
canvas.repaint();
}
public void releaseMouse() {
draggedWindow = null;
topBorderLocked = false;
leftBorderLocked = false;
bottomBorderLocked = false;
rightBorderLocked = false;
}
List<MyWindow> getWindows() {
return wndList;
}
}
MyWindow.java:
package net.coderodde.gui.simulation;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
public class MyWindow {
public static enum Border {
TOP,
RIGHT,
BOTTOM,
LEFT
}
public final static int MINIMUM_WIDTH = 50;
public final static int MINIMUM_HEIGHT = 50;
private final static int TITLE_BAR_HEIGHT = 30;
private final static Color ACTIVE_TITLE_BAR_BACKGROUND = new Color(255, 150, 100);
private final static Color TITLE_BAR_TEXT_COLOR = Color.WHITE;
private final static Color PASSIVE_TITLE_BAR_BACKGROUND = new Color(200, 200, 200);
private final static Color BORDER_HIGHLIGHT_COLOR = Color.GREEN;
private final static Color BODY_COLOR = new Color(50, 50, 50);
private final static Font TITLE_FONT = new Font("Monospaced", Font.BOLD, 12);
private final static int TITLE_PADDING = 14;
private String title;
private int width;
private int height;
private int x;
private int y;
private boolean active;
private boolean topBorderHightlighted;
private boolean rightBorderHighlighted;
private boolean bottomBorderHightlighted;
private boolean leftBorderHighlighted;
private boolean debug;
public MyWindow(final String title,
final int width,
final int height,
final int x,
final int y) {
this.title = title;
this.width = Math.max(width, MINIMUM_WIDTH);
this.height = Math.max(height, MINIMUM_HEIGHT) + TITLE_BAR_HEIGHT;
this.x = x;
this.y = y;
this.active = false;
}
public void setDebug(final boolean debug) {
this.debug = debug;
}
public boolean active() {
return active;
}
public boolean active(final boolean active) {
final boolean old = this.active;
this.active = active;
return old;
}
public void clearBorderHighlights() {
topBorderHightlighted = false;
rightBorderHighlighted = false;
bottomBorderHightlighted = false;
leftBorderHighlighted = false;
}
public void highlightBorder(Border border) {
switch (border) {
case TOP:
topBorderHightlighted = true;
return;
case RIGHT:
rightBorderHighlighted = true;
return;
case BOTTOM:
bottomBorderHightlighted = true;
return;
case LEFT:
leftBorderHighlighted = true;
return;
}
}
public String title() {
return title;
}
public String title(final String title) {
final String oldTitle = this.title;
this.title = title;
return oldTitle;
}
public int width() {
return width;
}
public int width(final int width) {
final int oldWidth = this.width;
this.width = Math.max(width, MINIMUM_WIDTH);
return oldWidth;
}
public int height() {
return height;
}
public int height(final int height) {
if (debug) {
System.out.println("Height changed from " + height() + " to " +
height);
}
final int oldHeight = this.height;
this.height = Math.max(height, MINIMUM_HEIGHT);
return oldHeight;
}
public int x() {
return x;
}
public int y() {
return y;
}
public int x(final int x) {
final int oldx = this.x;
this.x = x;
return oldx;
}
public int y(final int y) {
final int oldy = this.y;
this.y = y;
return oldy;
}
public boolean titleBarContainsPoint(final int xx, final int yy) {
return x() <= xx
&& xx < x() + width
&& y() <= yy
&& yy < y() + TITLE_BAR_HEIGHT;
}
public boolean windowContainsPoint(final int xx, final int yy) {
return x() <= xx
&& xx < x() + width
&& y() <= yy
&& yy < y() + height;
}
public boolean topBorderContainsPoint(final int xx, final int yy) {
return x() <= xx && xx < x() + width && (yy == y() || yy == y() + 1);
}
public boolean rightBorderContainsPoint(final int xx, final int yy) {
return (xx == x() + width - 1 || xx == x() + width - 2)
&& y() <= yy && yy < y() + height;
}
public boolean bottomBorderContainsPoint(final int xx, final int yy) {
return x() <= xx
&& xx < x() + width
&& (yy == y() + height - 1 || yy == y() + height - 2);
}
public boolean leftBorderContainsPoint(final int xx, final int yy) {
return (xx == x() || xx == x() + 1) && y() <= yy && yy < y() + height;
}
public void draw(final Graphics g) {
final Color titleBarColor = active() ?
ACTIVE_TITLE_BAR_BACKGROUND :
PASSIVE_TITLE_BAR_BACKGROUND;
// Draw title bar.
g.setColor(titleBarColor);
g.fillRect(x, y, width, TITLE_BAR_HEIGHT);
g.setColor(TITLE_BAR_TEXT_COLOR);
g.setFont(TITLE_FONT);
g.drawString(getContractedTitle(g.getFontMetrics(TITLE_FONT)),
x + TITLE_PADDING,
y + 18);
// Fill body rectangle.
g.setColor(BODY_COLOR);
g.fillRect(x, y + TITLE_BAR_HEIGHT, width, height - TITLE_BAR_HEIGHT);
// Draw left, right and bottom colors.
g.setColor(titleBarColor);
// Draw left border.
g.drawLine(x, y, x, y + height - 1);
// Draw right border.
g.drawLine(x + width - 1, y, x + width - 1, y + height - 1);
// Draw bottom border
g.drawLine(x, y + height - 1, x + width - 1, y + height - 1);
// Try draw border highlights.
g.setColor(BORDER_HIGHLIGHT_COLOR);
// Top border.
if (topBorderHightlighted) {
g.fillRect(x, y, width, 2);
}
// Left border.
if (leftBorderHighlighted) {
g.fillRect(x, y, 2, height);
}
// Right border.
if (rightBorderHighlighted) {
g.fillRect(x + width - 2, y, 2, height);
}
// Bottom border.
if (bottomBorderHightlighted) {
g.fillRect(x, y + height - 2, width, 2);
}
}
public void resizeAtTop(final int delta) {
final int oldHeight = height;
height = Math.max(MINIMUM_HEIGHT, height - delta);
y += (oldHeight - height);
}
public void resizeAtLeft(final int delta) {
final int oldWidth = width;
width = Math.max(MINIMUM_WIDTH, width - delta);
x += (oldWidth - width);
}
private String getContractedTitle(final FontMetrics fm) {
final int allowedWidth = width() - 2 * TITLE_PADDING;
if (fm.stringWidth(title) <= allowedWidth) {
// Once here, the window title fits entirely in the title bar.
return title;
}
// Once here, we need to contract the string from right.
for (int i = title.length() - 1; i > 0; --i) {
final String contractedTitle = title.substring(0, i);
final String titleWithDots = contractedTitle + "...";
if (fm.stringWidth(titleWithDots) <= allowedWidth) {
return titleWithDots;
}
}
if (fm.stringWidth("...") <= allowedWidth) {
return "...";
}
if (fm.stringWidth("..") <= allowedWidth) {
return "..";
}
return "";
}
}
MyCanvas.java:
package net.coderodde.gui.simulation;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.List;
class MyCanvas extends Canvas {
private final int width;
private final int height;
private final List<MyWindow> wndList;
MyCanvas(final int width,
final int height,
final List<MyWindow> wndList,
final MyScreen screen) {
this.width = width;
this.height = height;
setSize(width, height);
this.wndList = wndList;
final MyMouseListener mml = new MyMouseListener(screen);
addMouseListener(mml);
addMouseMotionListener(mml);
}
@Override
public void paint(final Graphics g) {
g.setColor(Color.BLACK);
g.fillRect(0, 0, width, height);
for (final MyWindow wnd : wndList) {
wnd.draw(g);
}
}
@Override
public void update(final Graphics g) {
paint(g);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(width, height);
}
static class MyMouseListener implements MouseListener,
MouseMotionListener {
private final MyScreen screen;
MyMouseListener(final MyScreen screen) {
this.screen = screen;
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mousePressed(MouseEvent e) {
screen.press(e.getX(), e.getY());
}
@Override
public void mouseReleased(MouseEvent e) {
screen.releaseMouse();
}
@Override
public void mouseEntered(MouseEvent e) {
screen.allowDrag();
}
@Override
public void mouseExited(MouseEvent e) {
screen.disallowDrag();
}
@Override
public void mouseDragged(MouseEvent e) {
screen.drag(e.getX(), e.getY());
}
@Override
public void mouseMoved(MouseEvent e) {
screen.move(e.getX(), e.getY());
}
}
}
Demo.java:
package net.coderodde.gui.simulation;
import javax.swing.SwingUtilities;
public class Demo {
public static void main(final String... args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
MyScreen screen = new MyScreen();
screen.addWindow(new MyWindow("Hello", 200, 200, 30, 30));
screen.addWindow(new MyWindow("World and Funkeeh", 250, 150, 400, 400));
screen.addWindow(new MyWindow("Yeah!", 400, 300, 10, 100));
}
});
}
}