mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
ui: mobile screens use a tab bar instead of split
This commit is contained in:
1 parent
3d588cbe1b
commit
5310e3dbf6
5 files changed
+258
-80
No files matched your search
+75
-17
@@ -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
@@ -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>-->
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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?
|
||||
|
||||
Reference in new issue
Block a user