Skip to content

Commit 2382b7c

Browse files
UI/Info Icons (#261)
HTTP Server Icon 🌐 Queue Full Icon ❗ Broadcast Icon πŸ“’ TCP timeout ⏰ TCP handshake 🀝 ![image](https://github.com/user-attachments/assets/24698a53-510d-48cb-afe7-5f87b18c78e6) ![image](https://github.com/user-attachments/assets/227464c7-af32-45d7-b710-563393b90180) close #173 --------- Co-authored-by: TomΓ‘s GrΓΌner <[email protected]>
1 parent 2b8aaa7 commit 2382b7c

File tree

10 files changed

+186
-8
lines changed

10 files changed

+186
-8
lines changed

β€Žsrc/programs/http_client.tsβ€Ž

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,18 @@ export class HttpClient extends ProgramBase {
8787
this.resource,
8888
);
8989

90+
srcDevice.showDeviceIcon(
91+
"tcp-handshake",
92+
"🀝",
93+
"TCP handshake in progress",
94+
Layer.App,
95+
);
96+
9097
// Write request
9198
const socket = await this.runner.tcpConnect(this.dstId);
99+
100+
srcDevice.hideDeviceIcon("tcp-handshake");
101+
92102
if (!socket) {
93103
console.error("HttpClient failed to connect");
94104
showError(
@@ -185,7 +195,7 @@ export class HttpServer extends ProgramBase {
185195
);
186196
return;
187197
}
188-
198+
srcDevice.showHttpServerIcon();
189199
const listener = await this.runner.tcpListenOn(this.port);
190200
if (!listener) {
191201
showError(`Port ${this.port} already in use`);
@@ -203,6 +213,7 @@ export class HttpServer extends ProgramBase {
203213
this.serveClient(socket);
204214
}
205215
listener.close();
216+
srcDevice.hideHttpServerIcon();
206217
}
207218

208219
// eslint-disable-next-line @typescript-eslint/no-unused-vars

β€Žsrc/styles/alert.cssβ€Ž

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616
border: 1px solid rgba(255, 255, 255, 0.2); /* Subtle border */
1717
max-width: 400px; /* Maximum width */
1818
line-height: 1.5; /* Line spacing */
19-
font-family:
20-
"Segoe UI", Tahoma, Geneva, Verdana, sans-serif; /* Font family */
19+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
2120
opacity: 0; /* Initially transparent */
2221
transition:
2322
opacity 0.3s ease-in-out,

β€Žsrc/styles/tooltips.cssβ€Ž

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
max-width: 37.5rem; /* Maximum width in rem (600px / 16) */
1818
white-space: pre-wrap; /* Allow line breaks */
1919
line-height: 1.8; /* Line spacing */
20-
font-family:
21-
"Segoe UI", Tahoma, Geneva, Verdana, sans-serif; /* Font family */
20+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
2221
overflow-y: auto; /* Enable scrolling for long content */
2322
opacity: 0.8; /* Slightly transparent */
2423
transition: opacity 0.2s ease-in-out; /* Smooth transition for opacity */

β€Žsrc/types/network-modules/tcp/tcpState.tsβ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { AsyncQueue } from "../asyncQueue";
99
import { SegmentWithIp } from "../tcpModule";
1010
import { GlobalContext } from "../../../context";
1111
import { CONFIG_SWITCH_KEYS } from "../../../config_menu/switches/switch_factory";
12+
import { Layer } from "../../layer";
1213

1314
enum TcpStateEnum {
1415
CLOSED = 0,
@@ -714,6 +715,13 @@ export class TcpState {
714715
console.debug("[" + this.srcHost.id + "] [TCP] Processing timeout");
715716
retransmitPromise = this.retransmissionQueue.pop();
716717
// Retransmit the segment
718+
this.srcHost.showDeviceIconFor(
719+
"tcp_timeout",
720+
"⏰",
721+
"TCP Timeout",
722+
2000,
723+
Layer.Transport,
724+
);
717725
this.resendPacket(result.seqNum, result.size);
718726
this.congestionControl.notifyTimeout();
719727
continue;

β€Žsrc/types/view-devices/vDevice.tsβ€Ž

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
TextStyle,
77
Text,
88
Container,
9+
Ticker,
910
} from "pixi.js";
1011
import { ViewGraph } from "../graphs/viewgraph";
1112
import {
@@ -15,7 +16,7 @@ import {
1516
urManager,
1617
} from "../viewportManager";
1718
import { RightBar } from "../../graphics/right_bar";
18-
import { Colors, ZIndexLevels } from "../../utils/utils";
19+
import { Colors, createDeviceIcon, ZIndexLevels } from "../../utils/utils";
1920
import { Position } from "../common";
2021
import { DeviceInfo } from "../../graphics/renderables/device_info";
2122
import {
@@ -68,6 +69,8 @@ export abstract class ViewDevice extends Container {
6869
private circleGraphic?: Graphics;
6970
private idLabel?: Text;
7071
private isVisibleFlag = true; // Flag to track visibility
72+
private deviceIcons: Record<string, Text | undefined> = {};
73+
private deviceTooltips: Record<string, Text | undefined> = {};
7174

7275
readonly id: DeviceId;
7376
readonly viewgraph: ViewGraph;
@@ -190,6 +193,9 @@ export abstract class ViewDevice extends Container {
190193

191194
updateDevicesAspect() {
192195
if (!this.isVisibleFlag) {
196+
for (const iconKey in this.deviceIcons) {
197+
this.hideDeviceIcon(iconKey);
198+
}
193199
const edges = this.viewgraph
194200
.getConnections(this.id)
195201
.filter((e) => e.isVisible());
@@ -400,6 +406,90 @@ export abstract class ViewDevice extends Container {
400406
RightBar.getInstance().renderInfo(new DeviceInfo(this));
401407
}
402408

409+
private repositionDeviceIcons() {
410+
const icons = Object.values(this.deviceIcons).filter(Boolean) as Text[];
411+
if (icons.length === 0) return;
412+
413+
const spacing = 28;
414+
const baseY = -this.height / 2 - 5;
415+
416+
const totalWidth = (icons.length - 1) * spacing;
417+
icons.forEach((icon, idx) => {
418+
icon.x = -totalWidth / 2 + idx * spacing;
419+
icon.y = baseY;
420+
});
421+
}
422+
423+
showDeviceIconFor(
424+
iconKey: string,
425+
emoji: string,
426+
tooltipText: string | undefined,
427+
durationMs = 2000,
428+
visibleFromLayer?: Layer,
429+
) {
430+
this.showDeviceIcon(iconKey, emoji, tooltipText, visibleFromLayer);
431+
let progress = 0;
432+
const tick = (ticker: Ticker) => {
433+
progress += ticker.elapsedMS * this.ctx.getCurrentSpeed();
434+
if (progress >= durationMs) {
435+
Ticker.shared.remove(tick, this);
436+
this.hideDeviceIcon(iconKey);
437+
}
438+
};
439+
Ticker.shared.add(tick, this);
440+
}
441+
442+
showDeviceIcon(
443+
iconKey: string,
444+
emoji: string,
445+
tooltipText?: string,
446+
visibleFromLayer?: Layer,
447+
) {
448+
if (
449+
visibleFromLayer !== undefined &&
450+
this.viewgraph.getLayer() > visibleFromLayer
451+
) {
452+
return;
453+
}
454+
if (!this.isVisible()) return;
455+
if (this.deviceIcons[iconKey]) return;
456+
const icon = createDeviceIcon(emoji, 0);
457+
this.deviceIcons[iconKey] = icon;
458+
459+
if (tooltipText) {
460+
icon.on("pointerover", () => {
461+
this.deviceTooltips[iconKey] = showTooltip(
462+
this,
463+
tooltipText,
464+
0,
465+
icon.y - 30,
466+
this.deviceTooltips[iconKey],
467+
);
468+
});
469+
icon.on("pointerout", () => {
470+
hideTooltip(this.deviceTooltips[iconKey]);
471+
});
472+
}
473+
474+
this.addChild(icon);
475+
this.repositionDeviceIcons();
476+
}
477+
478+
hideDeviceIcon(iconKey: string) {
479+
const icon = this.deviceIcons[iconKey];
480+
if (icon) {
481+
this.removeChild(icon);
482+
icon.destroy();
483+
this.deviceIcons[iconKey] = undefined;
484+
this.repositionDeviceIcons();
485+
}
486+
const tooltip = this.deviceTooltips[iconKey];
487+
if (tooltip) {
488+
removeTooltip(this, tooltip);
489+
this.deviceTooltips[iconKey] = undefined;
490+
}
491+
}
492+
403493
select() {
404494
if (this.isDragCircle) return;
405495
this.highlight(); // Calls highlight on select

β€Žsrc/types/view-devices/vHost.tsβ€Ž

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ export class ViewHost extends ViewNetworkDevice {
173173
this.runningPrograms.clear();
174174
}
175175

176+
showHttpServerIcon() {
177+
this.showDeviceIcon("httpServer", "🌐", "HTTP Server");
178+
}
179+
180+
hideHttpServerIcon() {
181+
this.hideDeviceIcon("httpServer");
182+
}
183+
176184
// TCP
177185

178186
private tcpModule = new TcpModule(this);

β€Žsrc/types/view-devices/vNetworkDevice.tsβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ export abstract class ViewNetworkDevice extends ViewDevice {
210210
// drop packet
211211
const frame = new EthernetFrame(mac, sha, packet);
212212
dropPacket(this.viewgraph, this.id, frame);
213+
this.showDeviceIconFor("arpDrop", "β›”", "ARP dropped");
213214
return;
214215
}
215216
// Send an ARP Reply to the requesting device

β€Žsrc/types/view-devices/vRouter.tsβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export class ViewRouter extends ViewNetworkDevice {
183183
}
184184
if (!this.packetQueue.enqueue(datagram)) {
185185
console.debug("Packet queue full, dropping packet");
186+
this.showDeviceIconFor("queueFull", "❗", "Queue full");
186187
this.dropPacket(datagram);
187188
return;
188189
}
@@ -255,7 +256,7 @@ export class ViewRouter extends ViewNetworkDevice {
255256
return;
256257
}
257258

258-
const result = device.routingTable.all().find((entry) => {
259+
const result = device.routingTable.allActive().find((entry) => {
259260
const ip = IpAddress.parse(entry.ip);
260261
const mask = IpAddress.parse(entry.mask);
261262
return datagram.destinationAddress.isInSubnet(ip, mask);

β€Žsrc/types/view-devices/vSwitch.tsβ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export class ViewSwitch extends ViewDevice {
7878
}
7979
if (device instanceof DataSwitch) {
8080
device.updateForwardingTable(mac, iface);
81+
this.showDeviceIconFor("update-ftable", "πŸ”„", "Table Updated");
8182
}
8283
});
8384
}
@@ -107,6 +108,8 @@ export class ViewSwitch extends ViewDevice {
107108
this.updateForwardingTable(frame.source, iface);
108109
if (frame.payload instanceof ArpRequest) {
109110
const { sha, spa, tha, tpa } = frame.payload;
111+
112+
this.showDeviceIconFor("broadcast", "πŸ“’", "Broadcast");
110113
this.interfaces.forEach((sendingIface, idx) => {
111114
const packet = new ArpRequest(sha, spa, tpa, tha);
112115
const frame = new EthernetFrame(
@@ -137,6 +140,10 @@ export class ViewSwitch extends ViewDevice {
137140
{ length: getNumberOfInterfaces(this.getType()) },
138141
(_, i) => i,
139142
);
143+
144+
if (sendingIfaces.length > 1) {
145+
this.showDeviceIconFor("broadcast", "πŸ“’", "Broadcast");
146+
}
140147
sendingIfaces.forEach((sendingIface) =>
141148
this.forwardFrame(frame, sendingIface, iface),
142149
);

β€Žsrc/utils/utils.tsβ€Ž

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { Application, GraphicsContext, RenderTexture } from "pixi.js";
1+
import {
2+
Application,
3+
GraphicsContext,
4+
RenderTexture,
5+
Container,
6+
TextStyle,
7+
Text,
8+
} from "pixi.js";
29
import { Viewport } from "../graphics/viewport";
310
import { ALERT_MESSAGES } from "./constants/alert_constants";
411
import { showError } from "../graphics/renderables/alert_manager";
@@ -82,3 +89,50 @@ export function captureAndDownloadViewport(
8289
link.click();
8390
}, "image/png");
8491
}
92+
93+
export function blockPointerEvents(obj: Container) {
94+
obj.on("pointerdown", (e) => e.stopPropagation());
95+
obj.on("pointerup", (e) => e.stopPropagation());
96+
obj.on("pointerupoutside", (e) => e.stopPropagation());
97+
obj.on("pointertap", (e) => e.stopPropagation());
98+
obj.on("pointermove", (e) => e.stopPropagation());
99+
obj.on("click", (e) => e.stopPropagation());
100+
}
101+
102+
/**
103+
* Creates a PixiJS emoji icon centered above the device.
104+
* @param emoji The emoji or text to display (e.g., "🌐")
105+
* @param yOffset Vertical offset from the center of the device (e.g., -this.height / 2 - 5)
106+
* @param fontSize Font size (optional, default 20)
107+
* @returns A Pixi.Text instance ready to be added as a child
108+
*/
109+
export function createDeviceIcon(
110+
emoji: string,
111+
yOffset: number,
112+
fontSize = 20,
113+
): Text {
114+
const textStyle = new TextStyle({
115+
fontSize,
116+
});
117+
118+
const icon = new Text({ text: emoji, style: textStyle });
119+
icon.anchor.set(0.5, 1);
120+
icon.x = 1;
121+
icon.y = yOffset;
122+
icon.zIndex = 100;
123+
icon.eventMode = "static";
124+
icon.interactive = true;
125+
icon.cursor = "pointer";
126+
127+
blockPointerEvents(icon);
128+
return icon;
129+
}
130+
131+
/**
132+
* Changes the emoji/text of a PixiJS Text icon.
133+
* @param icon The Pixi.Text instance.
134+
* @param emoji The emoji or text to display.
135+
*/
136+
export function setIconEmoji(icon: Text, emoji: string) {
137+
icon.text = emoji;
138+
}

0 commit comments

Comments
Β (0)