Allow detaching from autoscroll

This commit is contained in:
momo5502
2025-09-07 09:49:58 +02:00
parent f621fef81e
commit 3c2672b109
4 changed files with 73 additions and 22 deletions

View File

@@ -136,3 +136,9 @@ button.fancy.bg-secondary:hover {
.terminal-dark-gray {
color: rgb(81, 81, 81);
}
.terminal-glass {
box-shadow: 0px 0px 15px 4px rgba(255, 255, 255, 0.04);
backdrop-filter: blur(6px) brightness(0.8) saturate(1.3);
background: transparent;
}

View File

@@ -1,5 +0,0 @@
.emulation-summary {
box-shadow: 0px 0px 15px 4px rgba(255, 255, 255, 0.04);
/*border: 1px solid rgba(255, 255, 255, 0.2);*/
backdrop-filter: blur(7px) brightness(0.8) saturate(1.3);
}

View File

@@ -2,8 +2,6 @@ import { EmulationStatus } from "@/emulator";
import { TextTooltip } from "./text-tooltip";
import { BarChartSteps, CpuFill, FloppyFill } from "react-bootstrap-icons";
import "./emulation-summary.css";
export interface EmulationSummaryProps {
status?: EmulationStatus;
}
@@ -28,7 +26,7 @@ export function EmulationSummary(props: EmulationSummaryProps) {
}
return (
<div className="emulation-summary items-center absolute z-49 right-0 m-6 rounded-xl min-w-[150px] p-3 text-whtie cursor-default font-medium text-right text-sm whitespace-nowrap leading-6 font-mono">
<div className="emulation-summary terminal-glass items-center absolute z-49 right-0 m-6 rounded-xl min-w-[150px] p-3 text-white cursor-default font-medium text-right text-sm whitespace-nowrap leading-6 font-mono">
<TextTooltip tooltip={"Active threads"}>
{props.status.activeThreads}
<BarChartSteps className="inline ml-3" />

View File

@@ -1,10 +1,7 @@
import React from "react";
import {
List,
ListImperativeAPI,
useListRef,
type RowComponentProps,
} from "react-window";
import { List, ListImperativeAPI, type RowComponentProps } from "react-window";
import { ArrowDown } from "react-bootstrap-icons";
import { Button } from "./ui/button";
interface OutputProps {}
@@ -26,6 +23,7 @@ interface FullOutputState extends OutputState {
height: number;
width: number;
state: SizeState;
autoScroll: boolean;
}
interface LogLine {
@@ -182,6 +180,7 @@ export class Output extends React.Component<OutputProps, FullOutputState> {
private outputRef: React.RefObject<HTMLDivElement | null>;
private listRef: React.RefObject<ListImperativeAPI | null>;
private resizeObserver: ResizeObserver;
private scrollElement: HTMLDivElement | null | undefined;
constructor(props: OutputProps) {
super(props);
@@ -189,6 +188,7 @@ export class Output extends React.Component<OutputProps, FullOutputState> {
this.clear = this.clear.bind(this);
this.logLine = this.logLine.bind(this);
this.logLines = this.logLines.bind(this);
this.handleScroll = this.handleScroll.bind(this);
this.updateDimensions = this.updateDimensions.bind(this);
this.outputRef = React.createRef();
@@ -202,6 +202,7 @@ export class Output extends React.Component<OutputProps, FullOutputState> {
height: 10,
width: 10,
state: SizeState.Final,
autoScroll: true,
};
this.state.grouper.handler = (lines: string[]) => {
@@ -209,28 +210,65 @@ export class Output extends React.Component<OutputProps, FullOutputState> {
};
}
handleScroll(e: Event) {
const threshold = 40;
const element = e.target as HTMLElement;
const { scrollTop, scrollHeight, clientHeight } = element;
const isAtEnd = scrollTop + clientHeight >= scrollHeight - threshold;
this.setState({ autoScroll: isAtEnd });
}
unregisterScrollListener() {
this.scrollElement?.removeEventListener("scroll", this.handleScroll);
}
registerScrollListener(element: HTMLDivElement | null | undefined) {
if (element == this.scrollElement) {
return;
}
this.unregisterScrollListener();
this.scrollElement = element;
element?.addEventListener("scroll", this.handleScroll);
}
registerScrollOnList() {
this.registerScrollListener(this.listRef.current?.element);
}
componentDidMount() {
this.updateDimensions();
if (this.outputRef.current) {
this.resizeObserver.observe(this.outputRef.current);
}
this.registerScrollOnList();
}
componentWillUnmount() {
this.resizeObserver.disconnect();
this.unregisterScrollListener();
}
scrollListToEnd() {
if (this.listRef.current && this.state.lines.length > 0) {
this.listRef.current.scrollToRow({ index: this.state.lines.length - 1 });
}
this.setState({ autoScroll: true });
}
componentDidUpdate(_: OutputProps, prevState: FullOutputState) {
if (
!this.listRef.current ||
this.state.lines.length == 0 ||
prevState.lines.length == this.state.lines.length
) {
return;
}
this.registerScrollOnList();
this.listRef.current.scrollToRow({ index: this.state.lines.length - 1 });
if (
this.state.autoScroll &&
prevState.lines.length != this.state.lines.length
) {
this.scrollListToEnd();
}
}
clear() {
@@ -292,6 +330,20 @@ export class Output extends React.Component<OutputProps, FullOutputState> {
rowHeight={20}
style={{ height: this.state.height, width: this.state.width }}
/>
{this.state.autoScroll ? (
<></>
) : (
<Button
className="absolute bottom-6 right-6 z-50 terminal-glass"
variant="secondary"
onClick={() => {
this.scrollListToEnd();
}}
>
<ArrowDown />
</Button>
)}
</div>
);
}