Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d4f6a6e
feat: add HTTP server icon display and tooltip management in ViewHost
pgallino May 28, 2025
4683a13
feat: enhance ViewHost with HTTP server icon and tooltip improvements
pgallino May 28, 2025
acdd42a
Merge branch 'main' into ui/server-icon
pgallino May 28, 2025
9fd6fed
feat: implement device icon management and tooltip functionality in V…
pgallino May 28, 2025
78fd2a6
feat: add device icon visibility for full packet queue in ViewRouter
pgallino May 28, 2025
01bea73
feat: hide device icons when not visible and prevent icon creation if…
pgallino May 28, 2025
9ec7fbc
fix: lints
pgallino May 28, 2025
b334e9b
feat: add showDeviceIconFor method to display device icons temporaril…
pgallino May 28, 2025
c3e1570
feat: update packet queue handling to show temporary icon for full queue
pgallino May 28, 2025
123df60
Merge branch 'main' into ui/server-icon
pgallino May 29, 2025
814c83b
fix: set default duration for device icon display to 2000ms
pgallino May 30, 2025
afc497d
feat: reposition device icons horizontally above device
pgallino May 30, 2025
934f20b
fix lints
pgallino May 31, 2025
7241e63
Merge branch 'main' into ui/server-icon
pgallino May 31, 2025
8956aa2
fix: lints
pgallino May 31, 2025
b55490a
Merge branch 'main' into ui/server-icon
MegaRedHand May 31, 2025
09079cd
feat: enhance device icon visibility control and update routing table…
pgallino Jun 1, 2025
60e8849
Merge branch 'ui/server-icon' of https://github.com/MegaRedHand/netwo…
pgallino Jun 1, 2025
91dc2d5
fix: improve device icon visibility timeout handling using Ticker
pgallino Jun 1, 2025
b43e8aa
feat: add TCP handshake icon display during connection process in Htt…
pgallino Jun 1, 2025
8f2921f
Merge branch 'main' into ui/server-icon
MegaRedHand Jun 2, 2025
4828a5a
Merge branch 'main' into ui/server-icon
MegaRedHand Jun 2, 2025
d08da10
Merge branch 'main' into ui/server-icon
MegaRedHand Jun 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/programs/http_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,18 @@ export class HttpClient extends ProgramBase {
this.resource,
);

srcDevice.showDeviceIcon(
"tcp-handshake",
"🀝",
"TCP handshake in progress",
Layer.App,
);

// Write request
const socket = await this.runner.tcpConnect(this.dstId);

srcDevice.hideDeviceIcon("tcp-handshake");

if (!socket) {
console.error("HttpClient failed to connect");
showError(
Expand Down Expand Up @@ -185,7 +195,7 @@ export class HttpServer extends ProgramBase {
);
return;
}

srcDevice.showHttpServerIcon();
const listener = await this.runner.tcpListenOn(this.port);
if (!listener) {
showError(`Port ${this.port} already in use`);
Expand All @@ -203,6 +213,7 @@ export class HttpServer extends ProgramBase {
this.serveClient(socket);
}
listener.close();
srcDevice.hideHttpServerIcon();
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
3 changes: 1 addition & 2 deletions src/styles/alert.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
border: 1px solid rgba(255, 255, 255, 0.2); /* Subtle border */
max-width: 400px; /* Maximum width */
line-height: 1.5; /* Line spacing */
font-family:
"Segoe UI", Tahoma, Geneva, Verdana, sans-serif; /* Font family */
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
opacity: 0; /* Initially transparent */
transition:
opacity 0.3s ease-in-out,
Expand Down
3 changes: 1 addition & 2 deletions src/styles/tooltips.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
max-width: 37.5rem; /* Maximum width in rem (600px / 16) */
white-space: pre-wrap; /* Allow line breaks */
line-height: 1.8; /* Line spacing */
font-family:
"Segoe UI", Tahoma, Geneva, Verdana, sans-serif; /* Font family */
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
overflow-y: auto; /* Enable scrolling for long content */
opacity: 0.8; /* Slightly transparent */
transition: opacity 0.2s ease-in-out; /* Smooth transition for opacity */
Expand Down
8 changes: 8 additions & 0 deletions src/types/network-modules/tcp/tcpState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AsyncQueue } from "../asyncQueue";
import { SegmentWithIp } from "../tcpModule";
import { GlobalContext } from "../../../context";
import { CONFIG_SWITCH_KEYS } from "../../../config_menu/switches/switch_factory";
import { Layer } from "../../layer";

enum TcpStateEnum {
CLOSED = 0,
Expand Down Expand Up @@ -714,6 +715,13 @@ export class TcpState {
console.debug("[" + this.srcHost.id + "] [TCP] Processing timeout");
retransmitPromise = this.retransmissionQueue.pop();
// Retransmit the segment
this.srcHost.showDeviceIconFor(
"tcp_timeout",
"⏰",
"TCP Timeout",
2000,
Layer.Transport,
);
this.resendPacket(result.seqNum, result.size);
this.congestionControl.notifyTimeout();
continue;
Expand Down
92 changes: 91 additions & 1 deletion src/types/view-devices/vDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TextStyle,
Text,
Container,
Ticker,
} from "pixi.js";
import { ViewGraph } from "../graphs/viewgraph";
import {
Expand All @@ -15,7 +16,7 @@ import {
urManager,
} from "../viewportManager";
import { RightBar } from "../../graphics/right_bar";
import { Colors, ZIndexLevels } from "../../utils/utils";
import { Colors, createDeviceIcon, ZIndexLevels } from "../../utils/utils";
import { Position } from "../common";
import { DeviceInfo } from "../../graphics/renderables/device_info";
import {
Expand Down Expand Up @@ -68,6 +69,8 @@ export abstract class ViewDevice extends Container {
private circleGraphic?: Graphics;
private idLabel?: Text;
private isVisibleFlag = true; // Flag to track visibility
private deviceIcons: Record<string, Text | undefined> = {};
private deviceTooltips: Record<string, Text | undefined> = {};

readonly id: DeviceId;
readonly viewgraph: ViewGraph;
Expand Down Expand Up @@ -190,6 +193,9 @@ export abstract class ViewDevice extends Container {

updateDevicesAspect() {
if (!this.isVisibleFlag) {
for (const iconKey in this.deviceIcons) {
this.hideDeviceIcon(iconKey);
}
const edges = this.viewgraph
.getConnections(this.id)
.filter((e) => e.isVisible());
Expand Down Expand Up @@ -400,6 +406,90 @@ export abstract class ViewDevice extends Container {
RightBar.getInstance().renderInfo(new DeviceInfo(this));
}

private repositionDeviceIcons() {
const icons = Object.values(this.deviceIcons).filter(Boolean) as Text[];
if (icons.length === 0) return;

const spacing = 28;
const baseY = -this.height / 2 - 5;

const totalWidth = (icons.length - 1) * spacing;
icons.forEach((icon, idx) => {
icon.x = -totalWidth / 2 + idx * spacing;
icon.y = baseY;
});
}

showDeviceIconFor(
iconKey: string,
emoji: string,
tooltipText: string | undefined,
durationMs = 2000,
visibleFromLayer?: Layer,
) {
this.showDeviceIcon(iconKey, emoji, tooltipText, visibleFromLayer);
let progress = 0;
const tick = (ticker: Ticker) => {
progress += ticker.elapsedMS * this.ctx.getCurrentSpeed();
if (progress >= durationMs) {
Ticker.shared.remove(tick, this);
this.hideDeviceIcon(iconKey);
}
};
Ticker.shared.add(tick, this);
}

showDeviceIcon(
iconKey: string,
emoji: string,
tooltipText?: string,
visibleFromLayer?: Layer,
) {
if (
visibleFromLayer !== undefined &&
this.viewgraph.getLayer() > visibleFromLayer
) {
return;
}
if (!this.isVisible()) return;
if (this.deviceIcons[iconKey]) return;
const icon = createDeviceIcon(emoji, 0);
this.deviceIcons[iconKey] = icon;

if (tooltipText) {
icon.on("pointerover", () => {
this.deviceTooltips[iconKey] = showTooltip(
this,
tooltipText,
0,
icon.y - 30,
this.deviceTooltips[iconKey],
);
});
icon.on("pointerout", () => {
hideTooltip(this.deviceTooltips[iconKey]);
});
}

this.addChild(icon);
this.repositionDeviceIcons();
}

hideDeviceIcon(iconKey: string) {
const icon = this.deviceIcons[iconKey];
if (icon) {
this.removeChild(icon);
icon.destroy();
this.deviceIcons[iconKey] = undefined;
this.repositionDeviceIcons();
}
const tooltip = this.deviceTooltips[iconKey];
if (tooltip) {
removeTooltip(this, tooltip);
this.deviceTooltips[iconKey] = undefined;
}
}

select() {
if (this.isDragCircle) return;
this.highlight(); // Calls highlight on select
Expand Down
8 changes: 8 additions & 0 deletions src/types/view-devices/vHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ export class ViewHost extends ViewNetworkDevice {
this.runningPrograms.clear();
}

showHttpServerIcon() {
this.showDeviceIcon("httpServer", "🌐", "HTTP Server");
}

hideHttpServerIcon() {
this.hideDeviceIcon("httpServer");
}

// TCP

private tcpModule = new TcpModule(this);
Expand Down
1 change: 1 addition & 0 deletions src/types/view-devices/vNetworkDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export abstract class ViewNetworkDevice extends ViewDevice {
// drop packet
const frame = new EthernetFrame(mac, sha, packet);
dropPacket(this.viewgraph, this.id, frame);
this.showDeviceIconFor("arpDrop", "β›”", "ARP dropped");
return;
}
// Send an ARP Reply to the requesting device
Expand Down
3 changes: 2 additions & 1 deletion src/types/view-devices/vRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ export class ViewRouter extends ViewNetworkDevice {
}
if (!this.packetQueue.enqueue(datagram)) {
console.debug("Packet queue full, dropping packet");
this.showDeviceIconFor("queueFull", "❗", "Queue full");
this.dropPacket(datagram);
return;
}
Expand Down Expand Up @@ -255,7 +256,7 @@ export class ViewRouter extends ViewNetworkDevice {
return;
}

const result = device.routingTable.all().find((entry) => {
const result = device.routingTable.allActive().find((entry) => {
const ip = IpAddress.parse(entry.ip);
const mask = IpAddress.parse(entry.mask);
return datagram.destinationAddress.isInSubnet(ip, mask);
Expand Down
7 changes: 7 additions & 0 deletions src/types/view-devices/vSwitch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export class ViewSwitch extends ViewDevice {
}
if (device instanceof DataSwitch) {
device.updateForwardingTable(mac, iface);
this.showDeviceIconFor("update-ftable", "πŸ”„", "Table Updated");
}
});
}
Expand Down Expand Up @@ -107,6 +108,8 @@ export class ViewSwitch extends ViewDevice {
this.updateForwardingTable(frame.source, iface);
if (frame.payload instanceof ArpRequest) {
const { sha, spa, tha, tpa } = frame.payload;

this.showDeviceIconFor("broadcast", "πŸ“’", "Broadcast");
this.interfaces.forEach((sendingIface, idx) => {
const packet = new ArpRequest(sha, spa, tpa, tha);
const frame = new EthernetFrame(
Expand Down Expand Up @@ -137,6 +140,10 @@ export class ViewSwitch extends ViewDevice {
{ length: getNumberOfInterfaces(this.getType()) },
(_, i) => i,
);

if (sendingIfaces.length > 1) {
this.showDeviceIconFor("broadcast", "πŸ“’", "Broadcast");
}
sendingIfaces.forEach((sendingIface) =>
this.forwardFrame(frame, sendingIface, iface),
);
Expand Down
56 changes: 55 additions & 1 deletion src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Application, GraphicsContext, RenderTexture } from "pixi.js";
import {
Application,
GraphicsContext,
RenderTexture,
Container,
TextStyle,
Text,
} from "pixi.js";
import { Viewport } from "../graphics/viewport";
import { ALERT_MESSAGES } from "./constants/alert_constants";
import { showError } from "../graphics/renderables/alert_manager";
Expand Down Expand Up @@ -82,3 +89,50 @@ export function captureAndDownloadViewport(
link.click();
}, "image/png");
}

export function blockPointerEvents(obj: Container) {
obj.on("pointerdown", (e) => e.stopPropagation());
obj.on("pointerup", (e) => e.stopPropagation());
obj.on("pointerupoutside", (e) => e.stopPropagation());
obj.on("pointertap", (e) => e.stopPropagation());
obj.on("pointermove", (e) => e.stopPropagation());
obj.on("click", (e) => e.stopPropagation());
}

/**
* Creates a PixiJS emoji icon centered above the device.
* @param emoji The emoji or text to display (e.g., "🌐")
* @param yOffset Vertical offset from the center of the device (e.g., -this.height / 2 - 5)
* @param fontSize Font size (optional, default 20)
* @returns A Pixi.Text instance ready to be added as a child
*/
export function createDeviceIcon(
emoji: string,
yOffset: number,
fontSize = 20,
): Text {
const textStyle = new TextStyle({
fontSize,
});

const icon = new Text({ text: emoji, style: textStyle });
icon.anchor.set(0.5, 1);
icon.x = 1;
icon.y = yOffset;
icon.zIndex = 100;
icon.eventMode = "static";
icon.interactive = true;
icon.cursor = "pointer";

blockPointerEvents(icon);
return icon;
}

/**
* Changes the emoji/text of a PixiJS Text icon.
* @param icon The Pixi.Text instance.
* @param emoji The emoji or text to display.
*/
export function setIconEmoji(icon: Text, emoji: string) {
icon.text = emoji;
}