ui: mobile screens use a tab bar instead of split

This commit is contained in:
Steven Hugg committed 2026-09-14 11:37:14 +02:00
1 parent 3d588cbe1b
commit 5310e3dbf6
5 files changed
+258 -80

No files matched your search

+75 -17
View File
@@ -57,33 +57,97 @@
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
pointer-events: none;
}
/* body is a column: top bar, notebook, status bar. controls_top is allowed
to grow (its inline controls wrap on narrow screens) and the notebook
takes whatever is left, so no height math is needed. */
body {
display:flex;
flex-direction:column;
height:100vh;
height:100dvh;
}
#controls_top {
position:absolute;
flex:0 0 auto;
min-height:3em;
padding:0.5em;
height:3em;
width:100%;
background-color:#999;
}
/* contain the floated logo/menus so wrapped content counts toward the height */
#controls_top::after {
content:"";
display:table;
clear:both;
}
#notebook {
position:absolute;
top:3em;
bottom:1.8em;
left:0;
right:0;
position:relative;
flex:1 1 auto;
min-height:0;
background-color:#666;
overflow:hidden;
display:flex;
flex-direction:row;
}
/* small screens: the three panes become tabs, one visible at a time */
#mobiletabs {
display:none;
flex:0 0 auto;
background-color:#444;
border-bottom:1px solid #555;
}
body.tab-layout #notebook {
flex-direction:column;
}
body.tab-layout #mobiletabs {
display:flex;
}
/* save horizontal room for the wrapped controls */
body.tab-layout .logo-gradient {
display:none;
}
#mobiletabs .mobiletab {
flex:1 1 0;
background:none;
border:none;
border-right:1px solid #555;
color:#ccc;
padding:0.6em 0.5em;
font-size:1em;
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
}
#mobiletabs .mobiletab:last-child {
border-right:none;
}
#mobiletabs .mobiletab.active {
background-color:#666;
color:#fff;
font-weight:bold;
}
body.tab-layout #notebook > #sidebar:not(.active),
body.tab-layout #notebook > #workspace:not(.active),
body.tab-layout #notebook > #emulator:not(.active) {
display:none;
}
body.tab-layout #notebook > #sidebar.active,
body.tab-layout #notebook > #workspace.active,
body.tab-layout #notebook > #emulator.active {
/* fill the notebook: flex-grow takes the height, width overrides the
calc(% - gutter) width that Split.js sets inline */
flex:1 1 auto;
width:100% !important;
}
body.tab-layout #notebook > .gutter {
display:none;
}
#workspace {
background-color:#333;
overflow:hidden;
}
#shortcuts_bar {
position:absolute;
bottom:0;
left:0;
right:0;
position:relative;
flex:0 0 auto;
height:2em;
line-height:2em;
font-size:0.9em;
@@ -143,16 +207,10 @@
#shortcuts_bar {
display:none;
}
#notebook {
bottom:0;
}
}
body.statusbar-hidden #shortcuts_bar {
display:none;
}
body.statusbar-hidden #notebook {
bottom:0;
}
div.editor {
font-size: 1.25rem;
height: 100%;
+7 -2
View File
@@ -274,9 +274,13 @@ body {
</div><!-- controls_dynamic -->
</div><!-- controls_top -->
<div id="shortcuts_bar" class="disable-select"></div>
<div id="notebook">
<!-- shown only on small screens (see ide/layout.ts); switches the panes below -->
<div id="mobiletabs" class="disable-select">
<button type="button" class="mobiletab" data-tab="sidebar">Files</button>
<button type="button" class="mobiletab" data-tab="editor">Editor</button>
<button type="button" class="mobiletab active" data-tab="emulator">Emulator</button>
</div>
<div id="sidebar">
<ul id="windowMenuList">
</ul>
@@ -405,6 +409,7 @@ body {
<div id="error_alert_msg"></div>
</div>
</div>
<div id="shortcuts_bar" class="disable-select"></div>
<!--
<div class="twitbtn">
<a target="_new" href="https://twitter.com/8bitworkshop" class="twitter-follow-button" data-show-count="false">Follow @8bitworkshop</a>-->
+11
View File
@@ -110,6 +110,17 @@ tools appear:
| [Breakpoints](breakpoints.md) | Add and edit breakpoints |
| [Asset Editor](asseteditor.md) | Edit bitmaps, tilemaps, and palettes embedded in source |
## Small screens
On narrow screens (phones, or any window under 760px wide) the three
panes — **Files**, **Editor**, and **Emulator** — are shown one at a
time with a tab bar at the top instead of side by side, starting on the
**Emulator** tab. Tap a tab to switch panes; the emulator keeps running
while you edit. The top control bar wraps onto multiple lines as needed,
and the 8bitworkshop logo is hidden to make room. Rotating the device or
resizing the window back above the breakpoint restores the side-by-side
layout and your previous pane sizes.
## Keyboard Shortcuts
`mod` below is **Ctrl** on Windows/Linux and **Cmd** on macOS.
+155
View File
@@ -0,0 +1,155 @@
// Pane layout for the IDE.
//
// On wide screens the sidebar, editor and emulator sit side by side as three
// resizable panes (Split.js). On small screens they become a tab bar instead,
// showing one pane at a time; switching tabs just moves the splits
// programmatically with setSizes() (hence the zero minSize in tab mode).
import Split = require('split.js');
import { isMobileDevice } from "./views/baseviews";
declare var $: JQueryStatic; // use browser jquery
// panes, in Split.js order
const PANES = ['#sidebar', '#workspace', '#emulator'];
// split sizes for each tab (collapse the other two)
const TAB_SIZES = {
sidebar: [100, 0, 0],
editor: [0, 100, 0],
emulator: [0, 0, 100],
};
// ui.ts injects these so this module doesn't have to import (and cycle with) it
export interface LayoutHooks {
getPlatformId(): string;
isEmbed: boolean;
hasLocalStorage: boolean;
resizePlatform(): void;
resizeWindows(): void;
}
var hooks: LayoutHooks = null;
// Split.js instance; kept around so tabs can move the splits
var notebookSplit: any = null;
// true when the panes are shown as tabs (one at a time) instead of side by side
var tabMode = false;
var activeTab = 'emulator';
// the side-by-side sizes to restore when we leave tab mode
var desktopSizes: number[] = null;
var splitStorageName: string = null;
// live query (isMobileDevice in baseviews is computed once at load and
// wouldn't react to rotating the device or resizing the window)
var mobileMediaQuery = (typeof window !== 'undefined' && window.matchMedia)
? window.matchMedia("only screen and (max-width: 760px)") : null;
var wired = false;
function isTabLayout(): boolean {
return mobileMediaQuery ? mobileMediaQuery.matches : isMobileDevice;
}
function createSplit(sizes: number[]) {
if (notebookSplit) {
notebookSplit.destroy();
notebookSplit = null;
}
notebookSplit = Split(PANES, {
sizes: sizes,
// tab mode collapses two panes to 0, so the desktop minSize can't apply
minSize: tabMode ? [0, 0, 0] : [0, 250, 250],
onDrag: () => {
hooks.resizePlatform();
},
onDragEnd: () => {
if (!tabMode && hooks.hasLocalStorage) localStorage.setItem(splitStorageName, JSON.stringify(notebookSplit.getSizes()))
hooks.resizeWindows();
},
});
}
function applyTabSizes() {
notebookSplit.setSizes(TAB_SIZES[activeTab] || TAB_SIZES.editor);
}
function setActiveTabClass(tab: string) {
activeTab = TAB_SIZES[tab] ? tab : 'editor';
$("#mobiletabs .mobiletab").removeClass("active");
$("#mobiletabs .mobiletab[data-tab='" + activeTab + "']").addClass("active");
$("#sidebar,#workspace,#emulator").removeClass("active");
var sel = activeTab === 'sidebar' ? '#sidebar' : activeTab === 'emulator' ? '#emulator' : '#workspace';
$(sel).addClass("active");
}
function setTabMode(on: boolean) {
tabMode = on;
if (typeof document === 'undefined') return;
$("body").toggleClass("tab-layout", on);
}
export function showTab(tab: string) {
setActiveTabClass(tab);
if (!tabMode || !notebookSplit) return;
applyTabSizes();
// the revealed pane was 0px wide; let it relayout once it's visible
setTimeout(() => {
if (activeTab === 'emulator') hooks.resizePlatform();
if (activeTab === 'editor') hooks.resizeWindows();
}, 0);
}
function updateSplitLayout() {
var wantTabs = isTabLayout();
if (wantTabs === tabMode) return;
if (wantTabs) {
// entering tab mode: remember the side-by-side sizes, recreate with 0 mins
if (notebookSplit) desktopSizes = notebookSplit.getSizes();
setTabMode(true);
createSplit(desktopSizes || [0, 55, 45]);
showTab(activeTab);
} else {
// leaving tab mode: restore the side-by-side panes and their sizes
setTabMode(false);
$("#sidebar,#workspace,#emulator").removeClass("active");
createSplit(desktopSizes || [12, 44, 44]);
hooks.resizePlatform();
hooks.resizeWindows();
}
}
export function setupSplits(h: LayoutHooks) {
hooks = h;
var platform_id = hooks.getPlatformId();
splitStorageName = 'workspace-split3-' + platform_id;
if (hooks.isEmbed) splitStorageName = 'embed-' + splitStorageName;
var sizes;
if (platform_id.startsWith('vcs'))
sizes = [0, 50, 50];
else if (hooks.isEmbed || isMobileDevice)
sizes = [0, 55, 45];
else
sizes = [12, 44, 44];
var sizesStr = hooks.hasLocalStorage && localStorage.getItem(splitStorageName);
if (sizesStr) {
try {
sizes = JSON.parse(sizesStr);
} catch (e) { console.log(e); }
}
if (isTabLayout()) desktopSizes = sizes; // these came from localStorage; keep them for desktop
setTabMode(isTabLayout());
createSplit(sizes);
if (tabMode) {
applyTabSizes();
setActiveTabClass(activeTab);
} else {
$("#sidebar,#workspace,#emulator").removeClass("active");
}
if (!wired) {
wired = true;
$("#mobiletabs").on("click", ".mobiletab", function () {
showTab($(this).attr("data-tab"));
});
if (mobileMediaQuery) {
if (mobileMediaQuery.addEventListener) mobileMediaQuery.addEventListener("change", updateSplitLayout);
else if (mobileMediaQuery.addListener) mobileMediaQuery.addListener(updateSplitLayout);
}
}
}
+10 -61
View File
@@ -33,10 +33,11 @@ import { DisassemblerView, HeaderView, ListingView, PC_LINE_LOOKAHEAD, SourceEdi
import { HELP_TOPICS, HelpView, helpTopicForView } from "./views/helpview";
import { CallStackView, DebugBrowserView } from "./views/treeviews";
import { ProjectWindows } from "./windows";
import { setupSplits, showTab } from "./layout";
export { setupSplits, showTab };
import { bpStore, resolveBreakpoints } from "./breakpoints";
import { CondFn } from "./breakcond";
import { findListingLocation as findListingLocationPure } from "./search/listinglocation";
import Split = require('split.js');
import DOMPurify = require("dompurify");
// external libs (TODO)
@@ -2068,27 +2069,6 @@ function setupReplaySlider() {
uitoolbar.add('mod+shift+e', 'Start/Stop Replay Recording', 'glyphicon-record', _toggleRecording).prop('id', 'dbg_record');
}
function isLandscape() {
try {
var object = window.screen['orientation'] || window.screen['msOrientation'] || window.screen['mozOrientation'] || null;
if (object) {
if (object.type.indexOf('landscape') !== -1) { return true; }
if (object.type.indexOf('portrait') !== -1) { return false; }
}
if ('orientation' in window) {
var value = window.orientation;
if (value === 0 || value === 180) {
return false;
} else if (value === 90 || value === 270) {
return true;
}
}
} catch (e) { }
// fallback to comparing width to height
return window.innerWidth > window.innerHeight;
}
async function showWelcomeMessage() {
if (userPrefs.shouldCompleteTour()) {
await loadScript('lib/bootstrap-tourist.js');
@@ -2134,20 +2114,12 @@ async function showWelcomeMessage() {
content: "Pull right to expose the sidebar. It lets you switch between source files, view assembly listings, and use other tools like Disassembler, Memory Browser, and Asset Editor."
}
];
if (!isLandscape()) {
steps.unshift({
element: "#controls_top",
placement: 'bottom',
title: "Portrait mode detected",
content: "This site works best on desktop browsers. For best results, rotate your device to landscape orientation."
});
}
if (isProductionHost()) {
steps.unshift({
element: "#dropdownMenuButton",
placement: 'right',
title: "Cookie Consent",
content: 'Before we start, we should tell you that this website stores cookies and other data in your browser. You can review our <a href="/privacy.html" target="_new">privacy policy</a>.'
content: 'Before we start, we should tell you that this website stores persistent data in your browser. Review our <a href="/privacy.html" target="_new">privacy policy</a>.'
});
steps.push({
element: "#booksMenuButton",
@@ -2419,35 +2391,6 @@ function revealTopBar() {
setTimeout(() => { $("#controls_dynamic").css('visibility', 'inherit'); }, 250);
}
export function setupSplits() {
var splitName = 'workspace-split3-' + platform_id;
if (isEmbed) splitName = 'embed-' + splitName;
var sizes;
if (platform_id.startsWith('vcs'))
sizes = [0, 50, 50];
else if (isEmbed || isMobileDevice)
sizes = [0, 55, 45];
else
sizes = [12, 44, 44];
var sizesStr = hasLocalStorage && localStorage.getItem(splitName);
if (sizesStr) {
try {
sizes = JSON.parse(sizesStr);
} catch (e) { console.log(e); }
}
var split = Split(['#sidebar', '#workspace', '#emulator'], {
sizes: sizes,
minSize: [0, 250, 250],
onDrag: () => {
if (platform && platform.resize) platform.resize();
},
onDragEnd: () => {
if (hasLocalStorage) localStorage.setItem(splitName, JSON.stringify(split.getSizes()))
if (projectWindows) projectWindows.resize();
},
});
}
function loadImportedURL(url: string) {
// TODO: zip file?
const ignore = parseBool(qs.ignore) || isEmbed;
@@ -2570,7 +2513,13 @@ export async function startUI() {
delete qs.repo; // continue without repo
}
getPlatformAndRepo();
setupSplits();
setupSplits({
getPlatformId: () => platform_id,
isEmbed: isEmbed,
hasLocalStorage: hasLocalStorage,
resizePlatform: () => { if (platform && platform.resize) platform.resize(); },
resizeWindows: () => { if (projectWindows) projectWindows.resize(); },
});
// get store ID, repo id or platform id
store_id = repo_id || getBasePlatform(platform_id);
// are we embedded?