mirror of
https://github.com/Eugeny/tabby.git
synced 2025-07-23 11:58:01 +00:00
splitting tabs (fixes #49)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
export { BaseTabComponent, BaseTabProcess } from '../components/baseTab.component'
|
||||||
|
export { SplitTabComponent, SplitContainer } from '../components/splitTab.component'
|
||||||
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
|
export { TabRecoveryProvider, RecoveredTab } from './tabRecovery'
|
||||||
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
|
export { ToolbarButtonProvider, IToolbarButton } from './toolbarButtonProvider'
|
||||||
export { ConfigProvider } from './configProvider'
|
export { ConfigProvider } from './configProvider'
|
||||||
|
5
terminus-core/src/components/splitTab.component.scss
Normal file
5
terminus-core/src/components/splitTab.component.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
flex: auto;
|
||||||
|
}
|
@@ -13,6 +13,10 @@ export class SplitContainer {
|
|||||||
orientation: SplitOrientation = 'h'
|
orientation: SplitOrientation = 'h'
|
||||||
children: (BaseTabComponent | SplitContainer)[] = []
|
children: (BaseTabComponent | SplitContainer)[] = []
|
||||||
ratios: number[] = []
|
ratios: number[] = []
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
|
||||||
allTabs () {
|
allTabs () {
|
||||||
let r = []
|
let r = []
|
||||||
@@ -60,6 +64,14 @@ export class SplitContainer {
|
|||||||
this.ratios = this.ratios.map(x => x / s)
|
this.ratios = this.ratios.map(x => x / s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOffsetRatio (index: number): number {
|
||||||
|
let s = 0
|
||||||
|
for (let i = 0; i < index; i++) {
|
||||||
|
s += this.ratios[i]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
async serialize () {
|
async serialize () {
|
||||||
let children = []
|
let children = []
|
||||||
for (let child of this.children) {
|
for (let child of this.children) {
|
||||||
@@ -78,12 +90,23 @@ export class SplitContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SpannerInfo {
|
||||||
|
container: SplitContainer
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'splitTab',
|
selector: 'split-tab',
|
||||||
template: '<ng-container #vc></ng-container>',
|
template: `
|
||||||
styles: [
|
<ng-container #vc></ng-container>
|
||||||
':host { position: relative; flex: auto; display: block; }'
|
<split-tab-spanner
|
||||||
]
|
*ngFor='let spanner of spanners'
|
||||||
|
[container]='spanner.container'
|
||||||
|
[index]='spanner.index'
|
||||||
|
(change)='layout()'
|
||||||
|
></split-tab-spanner>
|
||||||
|
`,
|
||||||
|
styles: [require('./splitTab.component.scss')],
|
||||||
})
|
})
|
||||||
export class SplitTabComponent extends BaseTabComponent {
|
export class SplitTabComponent extends BaseTabComponent {
|
||||||
root: SplitContainer
|
root: SplitContainer
|
||||||
@@ -92,6 +115,7 @@ export class SplitTabComponent extends BaseTabComponent {
|
|||||||
hotkeysSubscription: Subscription
|
hotkeysSubscription: Subscription
|
||||||
focusedTab: BaseTabComponent
|
focusedTab: BaseTabComponent
|
||||||
recoveredState: any
|
recoveredState: any
|
||||||
|
spanners: SpannerInfo[] = []
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private hotkeys: HotkeysService,
|
private hotkeys: HotkeysService,
|
||||||
@@ -297,6 +321,7 @@ export class SplitTabComponent extends BaseTabComponent {
|
|||||||
|
|
||||||
private layout () {
|
private layout () {
|
||||||
this.root.normalize()
|
this.root.normalize()
|
||||||
|
this.spanners = []
|
||||||
this.layoutInternal(this.root, 0, 0, 100, 100)
|
this.layoutInternal(this.root, 0, 0, 100, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +329,11 @@ export class SplitTabComponent extends BaseTabComponent {
|
|||||||
let size = (root.orientation === 'v') ? h : w
|
let size = (root.orientation === 'v') ? h : w
|
||||||
let sizes = root.ratios.map(x => x * size)
|
let sizes = root.ratios.map(x => x * size)
|
||||||
|
|
||||||
|
root.x = x
|
||||||
|
root.y = y
|
||||||
|
root.w = w
|
||||||
|
root.h = h
|
||||||
|
|
||||||
let offset = 0
|
let offset = 0
|
||||||
root.children.forEach((child, i) => {
|
root.children.forEach((child, i) => {
|
||||||
let childX = (root.orientation === 'v') ? x : (x + offset)
|
let childX = (root.orientation === 'v') ? x : (x + offset)
|
||||||
@@ -323,6 +353,13 @@ export class SplitTabComponent extends BaseTabComponent {
|
|||||||
element.style.opacity = (child === this.focusedTab) ? 1 : 0.75
|
element.style.opacity = (child === this.focusedTab) ? 1 : 0.75
|
||||||
}
|
}
|
||||||
offset += sizes[i]
|
offset += sizes[i]
|
||||||
|
|
||||||
|
if (i !== 0) {
|
||||||
|
this.spanners.push({
|
||||||
|
container: root,
|
||||||
|
index: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
terminus-core/src/components/splitTabSpanner.component.scss
Normal file
22
terminus-core/src/components/splitTabSpanner.component.scss
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
transition: 0.125s background;
|
||||||
|
|
||||||
|
&.v {
|
||||||
|
cursor: ns-resize;
|
||||||
|
height: 10px;
|
||||||
|
margin-top: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.h {
|
||||||
|
cursor: ew-resize;
|
||||||
|
width: 10px;
|
||||||
|
margin-left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
background: rgba(255, 255, 255, .125);
|
||||||
|
}
|
||||||
|
}
|
87
terminus-core/src/components/splitTabSpanner.component.ts
Normal file
87
terminus-core/src/components/splitTabSpanner.component.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { Component, Input, HostBinding, ElementRef, Output, EventEmitter } from '@angular/core'
|
||||||
|
import { SplitContainer } from './splitTab.component'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'split-tab-spanner',
|
||||||
|
template: '',
|
||||||
|
styles: [require('./splitTabSpanner.component.scss')],
|
||||||
|
})
|
||||||
|
export class SplitTabSpannerComponent {
|
||||||
|
@Input() container: SplitContainer
|
||||||
|
@Input() index: number
|
||||||
|
@Output() change = new EventEmitter<void>()
|
||||||
|
@HostBinding('class.active') isActive = false
|
||||||
|
@HostBinding('class.h') isHorizontal = false
|
||||||
|
@HostBinding('class.v') isVertical = true
|
||||||
|
@HostBinding('style.left') cssLeft: string
|
||||||
|
@HostBinding('style.top') cssTop: string
|
||||||
|
@HostBinding('style.width') cssWidth: string
|
||||||
|
@HostBinding('style.height') cssHeight: string
|
||||||
|
private marginOffset = -5
|
||||||
|
|
||||||
|
constructor (private element: ElementRef) { }
|
||||||
|
|
||||||
|
ngAfterViewInit () {
|
||||||
|
this.element.nativeElement.addEventListener('mousedown', e => {
|
||||||
|
this.isActive = true
|
||||||
|
let start = this.isVertical ? e.pageY : e.pageX
|
||||||
|
let current = start
|
||||||
|
let oldPosition = this.isVertical ? this.element.nativeElement.offsetTop : this.element.nativeElement.offsetLeft
|
||||||
|
|
||||||
|
const dragHandler = e => {
|
||||||
|
current = this.isVertical ? e.pageY : e.pageX
|
||||||
|
let newPosition = oldPosition + (current - start)
|
||||||
|
if (this.isVertical) {
|
||||||
|
this.element.nativeElement.style.top = `${newPosition - this.marginOffset}px`
|
||||||
|
} else {
|
||||||
|
this.element.nativeElement.style.left = `${newPosition - this.marginOffset}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const offHandler = () => {
|
||||||
|
this.isActive = false
|
||||||
|
document.removeEventListener('mouseup', offHandler)
|
||||||
|
this.element.nativeElement.parentElement.removeEventListener('mousemove', dragHandler)
|
||||||
|
|
||||||
|
let diff = (current - start) / (this.isVertical ? this.element.nativeElement.parentElement.clientHeight : this.element.nativeElement.parentElement.clientWidth)
|
||||||
|
|
||||||
|
diff = Math.max(diff, -this.container.ratios[this.index - 1] + 0.1)
|
||||||
|
diff = Math.min(diff, this.container.ratios[this.index] - 0.1)
|
||||||
|
|
||||||
|
this.container.ratios[this.index - 1] += diff
|
||||||
|
this.container.ratios[this.index] -= diff
|
||||||
|
this.change.emit()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', offHandler)
|
||||||
|
this.element.nativeElement.parentElement.addEventListener('mousemove', dragHandler)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges () {
|
||||||
|
this.isHorizontal = this.container.orientation === 'h'
|
||||||
|
this.isVertical = this.container.orientation === 'v'
|
||||||
|
if (this.isVertical) {
|
||||||
|
this.setDimensions(
|
||||||
|
this.container.x,
|
||||||
|
this.container.y + this.container.h * this.container.getOffsetRatio(this.index),
|
||||||
|
this.container.w,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.setDimensions(
|
||||||
|
this.container.x + this.container.w * this.container.getOffsetRatio(this.index),
|
||||||
|
this.container.y,
|
||||||
|
null,
|
||||||
|
this.container.h
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setDimensions (x: number, y: number, w: number, h: number) {
|
||||||
|
this.cssLeft = `${x}%`
|
||||||
|
this.cssTop = `${y}%`
|
||||||
|
this.cssWidth = w ? `${w}%` : null
|
||||||
|
this.cssHeight = h ? `${h}%` : null
|
||||||
|
}
|
||||||
|
}
|
@@ -19,6 +19,7 @@ import { ToggleComponent } from './components/toggle.component'
|
|||||||
import { WindowControlsComponent } from './components/windowControls.component'
|
import { WindowControlsComponent } from './components/windowControls.component'
|
||||||
import { RenameTabModalComponent } from './components/renameTabModal.component'
|
import { RenameTabModalComponent } from './components/renameTabModal.component'
|
||||||
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
import { SplitTabComponent, SplitTabRecoveryProvider } from './components/splitTab.component'
|
||||||
|
import { SplitTabSpannerComponent } from './components/splitTabSpanner.component'
|
||||||
|
|
||||||
import { AutofocusDirective } from './directives/autofocus.directive'
|
import { AutofocusDirective } from './directives/autofocus.directive'
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ const PROVIDERS = [
|
|||||||
SafeModeModalComponent,
|
SafeModeModalComponent,
|
||||||
AutofocusDirective,
|
AutofocusDirective,
|
||||||
SplitTabComponent,
|
SplitTabComponent,
|
||||||
|
SplitTabSpannerComponent,
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
RenameTabModalComponent,
|
RenameTabModalComponent,
|
||||||
|
@@ -37,7 +37,7 @@ export class ButtonProvider extends ToolbarButtonProvider {
|
|||||||
if (settingsTab) {
|
if (settingsTab) {
|
||||||
this.app.selectTab(settingsTab)
|
this.app.selectTab(settingsTab)
|
||||||
} else {
|
} else {
|
||||||
this.app.openNewTab(SettingsTabComponent)
|
this.app.openNewTabRaw(SettingsTabComponent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user