mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Chat - implement session picker in the chat panel title (#293426)
* refactor layout and layout2d into base common * support anchored quick pick * wip: use anchored quick pick in scm * almost there * undo scm history view pane * Chat - implement session picker in the chat panel title * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Port changes again, and clean things up * Pull request feedback * missing changes * cleanup * fix bad merge * reduce complexity * polish * update title * Adopt the anchor * Fix compilation error * Fix monaco editor check * Enhance drag-and-drop functionality in QuickInput: add cursor style for no-drag state and enable/disable control * Fix positioning bug * fix change of behavior of layout2d * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com> Co-authored-by: Tyler James Leonhardt <2644648+TylerLeonhardt@users.noreply.github.com>
This commit is contained in:
@@ -7,11 +7,13 @@ import { BrowserFeatures } from '../../canIUse.js';
|
||||
import * as DOM from '../../dom.js';
|
||||
import { StandardMouseEvent } from '../../mouseEvent.js';
|
||||
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js';
|
||||
import { AnchorAlignment, AnchorAxisAlignment, AnchorPosition, IRect, layout2d } from '../../../common/layout.js';
|
||||
import * as platform from '../../../common/platform.js';
|
||||
import { Range } from '../../../common/range.js';
|
||||
import { OmitOptional } from '../../../common/types.js';
|
||||
import './contextview.css';
|
||||
|
||||
export { AnchorAlignment, AnchorAxisAlignment, AnchorPosition } from '../../../common/layout.js';
|
||||
|
||||
export const enum ContextViewDOMPosition {
|
||||
ABSOLUTE = 1,
|
||||
FIXED,
|
||||
@@ -31,18 +33,6 @@ export function isAnchor(obj: unknown): obj is IAnchor | OmitOptional<IAnchor> {
|
||||
return !!anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number';
|
||||
}
|
||||
|
||||
export const enum AnchorAlignment {
|
||||
LEFT, RIGHT
|
||||
}
|
||||
|
||||
export const enum AnchorPosition {
|
||||
BELOW, ABOVE
|
||||
}
|
||||
|
||||
export const enum AnchorAxisAlignment {
|
||||
VERTICAL, HORIZONTAL
|
||||
}
|
||||
|
||||
export interface IDelegate {
|
||||
/**
|
||||
* The anchor where to position the context view.
|
||||
@@ -73,66 +63,40 @@ export interface IContextViewProvider {
|
||||
layout(): void;
|
||||
}
|
||||
|
||||
export interface IPosition {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
export function getAnchorRect(anchor: HTMLElement | StandardMouseEvent | IAnchor): IRect {
|
||||
// Get the element's position and size (to anchor the view)
|
||||
if (DOM.isHTMLElement(anchor)) {
|
||||
const elementPosition = DOM.getDomNodePagePosition(anchor);
|
||||
|
||||
export interface ISize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
|
||||
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
|
||||
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
|
||||
const zoom = DOM.getDomNodeZoomLevel(anchor);
|
||||
|
||||
export interface IView extends IPosition, ISize { }
|
||||
|
||||
export const enum LayoutAnchorPosition {
|
||||
Before,
|
||||
After
|
||||
}
|
||||
|
||||
export enum LayoutAnchorMode {
|
||||
AVOID,
|
||||
ALIGN
|
||||
}
|
||||
|
||||
export interface ILayoutAnchor {
|
||||
offset: number;
|
||||
size: number;
|
||||
mode?: LayoutAnchorMode; // default: AVOID
|
||||
position: LayoutAnchorPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays out a one dimensional view next to an anchor in a viewport.
|
||||
*
|
||||
* @returns The view offset within the viewport.
|
||||
*/
|
||||
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): number {
|
||||
const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size;
|
||||
const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset;
|
||||
|
||||
if (anchor.position === LayoutAnchorPosition.Before) {
|
||||
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
|
||||
return layoutAfterAnchorBoundary; // happy case, lay it out after the anchor
|
||||
}
|
||||
|
||||
if (viewSize <= layoutBeforeAnchorBoundary) {
|
||||
return layoutBeforeAnchorBoundary - viewSize; // ok case, lay it out before the anchor
|
||||
}
|
||||
|
||||
return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor
|
||||
return {
|
||||
top: elementPosition.top * zoom,
|
||||
left: elementPosition.left * zoom,
|
||||
width: elementPosition.width * zoom,
|
||||
height: elementPosition.height * zoom
|
||||
};
|
||||
} else if (isAnchor(anchor)) {
|
||||
return {
|
||||
top: anchor.y,
|
||||
left: anchor.x,
|
||||
width: anchor.width || 1,
|
||||
height: anchor.height || 2
|
||||
};
|
||||
} else {
|
||||
if (viewSize <= layoutBeforeAnchorBoundary) {
|
||||
return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before the anchor
|
||||
}
|
||||
|
||||
|
||||
if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) {
|
||||
return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor
|
||||
}
|
||||
|
||||
|
||||
return 0; // sad case, lay it over the anchor
|
||||
return {
|
||||
top: anchor.posy,
|
||||
left: anchor.posx,
|
||||
// We are about to position the context view where the mouse
|
||||
// cursor is. To prevent the view being exactly under the mouse
|
||||
// when showing and thus potentially triggering an action within,
|
||||
// we treat the mouse location like a small sized block element.
|
||||
width: 2,
|
||||
height: 2
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,82 +234,14 @@ export class ContextView extends Disposable {
|
||||
}
|
||||
|
||||
// Get anchor
|
||||
const anchor = this.delegate!.getAnchor();
|
||||
|
||||
// Compute around
|
||||
let around: IView;
|
||||
|
||||
// Get the element's position and size (to anchor the view)
|
||||
if (DOM.isHTMLElement(anchor)) {
|
||||
const elementPosition = DOM.getDomNodePagePosition(anchor);
|
||||
|
||||
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
|
||||
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
|
||||
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
|
||||
const zoom = DOM.getDomNodeZoomLevel(anchor);
|
||||
|
||||
around = {
|
||||
top: elementPosition.top * zoom,
|
||||
left: elementPosition.left * zoom,
|
||||
width: elementPosition.width * zoom,
|
||||
height: elementPosition.height * zoom
|
||||
};
|
||||
} else if (isAnchor(anchor)) {
|
||||
around = {
|
||||
top: anchor.y,
|
||||
left: anchor.x,
|
||||
width: anchor.width || 1,
|
||||
height: anchor.height || 2
|
||||
};
|
||||
} else {
|
||||
around = {
|
||||
top: anchor.posy,
|
||||
left: anchor.posx,
|
||||
// We are about to position the context view where the mouse
|
||||
// cursor is. To prevent the view being exactly under the mouse
|
||||
// when showing and thus potentially triggering an action within,
|
||||
// we treat the mouse location like a small sized block element.
|
||||
width: 2,
|
||||
height: 2
|
||||
};
|
||||
}
|
||||
|
||||
const viewSizeWidth = DOM.getTotalWidth(this.view);
|
||||
const viewSizeHeight = DOM.getTotalHeight(this.view);
|
||||
|
||||
const anchorPosition = this.delegate!.anchorPosition ?? AnchorPosition.BELOW;
|
||||
const anchorAlignment = this.delegate!.anchorAlignment ?? AnchorAlignment.LEFT;
|
||||
const anchorAxisAlignment = this.delegate!.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL;
|
||||
|
||||
let top: number;
|
||||
let left: number;
|
||||
|
||||
const anchor = getAnchorRect(this.delegate!.getAnchor());
|
||||
const activeWindow = DOM.getActiveWindow();
|
||||
if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) {
|
||||
const verticalAnchor: ILayoutAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
|
||||
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
|
||||
|
||||
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
|
||||
|
||||
// if view intersects vertically with anchor, we must avoid the anchor
|
||||
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
|
||||
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
|
||||
}
|
||||
|
||||
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
|
||||
} else {
|
||||
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
|
||||
const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
|
||||
|
||||
left = layout(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
|
||||
|
||||
// if view intersects horizontally with anchor, we must avoid the anchor
|
||||
if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
|
||||
verticalAnchor.mode = LayoutAnchorMode.AVOID;
|
||||
}
|
||||
|
||||
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
|
||||
}
|
||||
const viewport = { top: activeWindow.pageYOffset, left: activeWindow.pageXOffset, width: activeWindow.innerWidth, height: activeWindow.innerHeight };
|
||||
const view = { width: DOM.getTotalWidth(this.view), height: DOM.getTotalHeight(this.view) };
|
||||
const anchorPosition = this.delegate!.anchorPosition;
|
||||
const anchorAlignment = this.delegate!.anchorAlignment;
|
||||
const anchorAxisAlignment = this.delegate!.anchorAxisAlignment;
|
||||
const { top, left } = layout2d(viewport, view, anchor, { anchorAlignment, anchorPosition, anchorAxisAlignment });
|
||||
|
||||
this.view.classList.remove('top', 'bottom', 'left', 'right');
|
||||
this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
|
||||
|
||||
@@ -11,7 +11,6 @@ import { StandardKeyboardEvent } from '../../keyboardEvent.js';
|
||||
import { StandardMouseEvent } from '../../mouseEvent.js';
|
||||
import { ActionBar, ActionsOrientation, IActionViewItemProvider } from '../actionbar/actionbar.js';
|
||||
import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js';
|
||||
import { AnchorAlignment, layout, LayoutAnchorPosition } from '../contextview/contextview.js';
|
||||
import { DomScrollableElement } from '../scrollbar/scrollableElement.js';
|
||||
import { EmptySubmenuAction, IAction, IActionRunner, Separator, SubmenuAction } from '../../../common/actions.js';
|
||||
import { RunOnceScheduler } from '../../../common/async.js';
|
||||
@@ -26,6 +25,7 @@ import { DisposableStore } from '../../../common/lifecycle.js';
|
||||
import { isLinux, isMacintosh } from '../../../common/platform.js';
|
||||
import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';
|
||||
import * as strings from '../../../common/strings.js';
|
||||
import { AnchorAlignment, layout, LayoutAnchorPosition } from '../../../common/layout.js';
|
||||
|
||||
export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
|
||||
export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g;
|
||||
@@ -859,7 +859,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
const ret = { top: 0, left: 0 };
|
||||
|
||||
// Start with horizontal
|
||||
ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width });
|
||||
ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection.horizontal === HorizontalDirection.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width }).position;
|
||||
|
||||
// We don't have enough room to layout the menu fully, so we are overlapping the menu
|
||||
if (ret.left >= entry.left && ret.left < entry.left + entry.width) {
|
||||
@@ -872,7 +872,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}
|
||||
|
||||
// Now that we have a horizontal position, try layout vertically
|
||||
ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 });
|
||||
ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 }).position;
|
||||
|
||||
// We didn't have enough room below, but we did above, so we shift down to align the menu
|
||||
if (ret.top + submenu.height === entry.top && ret.top + entry.height + submenu.height <= windowDimensions.height) {
|
||||
|
||||
166
src/vs/base/common/layout.ts
Normal file
166
src/vs/base/common/layout.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Range } from './range.js';
|
||||
|
||||
export interface IAnchor {
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export const enum AnchorAlignment {
|
||||
LEFT, RIGHT
|
||||
}
|
||||
|
||||
export const enum AnchorPosition {
|
||||
BELOW, ABOVE
|
||||
}
|
||||
|
||||
export const enum AnchorAxisAlignment {
|
||||
VERTICAL, HORIZONTAL
|
||||
}
|
||||
|
||||
interface IPosition {
|
||||
readonly top: number;
|
||||
readonly left: number;
|
||||
}
|
||||
|
||||
interface ISize {
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
}
|
||||
|
||||
export interface IRect extends IPosition, ISize { }
|
||||
|
||||
export const enum LayoutAnchorPosition {
|
||||
Before,
|
||||
After
|
||||
}
|
||||
|
||||
export enum LayoutAnchorMode {
|
||||
AVOID,
|
||||
ALIGN
|
||||
}
|
||||
|
||||
export interface ILayoutAnchor {
|
||||
offset: number;
|
||||
size: number;
|
||||
mode?: LayoutAnchorMode; // default: AVOID
|
||||
position: LayoutAnchorPosition;
|
||||
}
|
||||
|
||||
export interface ILayoutResult {
|
||||
position: number;
|
||||
result: 'ok' | 'flipped' | 'overlap';
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays out a one dimensional view next to an anchor in a viewport.
|
||||
*
|
||||
* @returns The view offset within the viewport.
|
||||
*/
|
||||
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): ILayoutResult {
|
||||
const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size;
|
||||
const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset;
|
||||
|
||||
if (anchor.position === LayoutAnchorPosition.Before) {
|
||||
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
|
||||
return { position: layoutAfterAnchorBoundary, result: 'ok' }; // happy case, lay it out after the anchor
|
||||
}
|
||||
|
||||
if (viewSize <= layoutBeforeAnchorBoundary) {
|
||||
return { position: layoutBeforeAnchorBoundary - viewSize, result: 'flipped' }; // ok case, lay it out before the anchor
|
||||
}
|
||||
|
||||
return { position: Math.max(viewportSize - viewSize, 0), result: 'overlap' }; // sad case, lay it over the anchor
|
||||
} else {
|
||||
if (viewSize <= layoutBeforeAnchorBoundary) {
|
||||
return { position: layoutBeforeAnchorBoundary - viewSize, result: 'ok' }; // happy case, lay it out before the anchor
|
||||
}
|
||||
|
||||
if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) {
|
||||
return { position: layoutAfterAnchorBoundary, result: 'flipped' }; // ok case, lay it out after the anchor
|
||||
}
|
||||
|
||||
return { position: 0, result: 'overlap' }; // sad case, lay it over the anchor
|
||||
}
|
||||
}
|
||||
|
||||
interface ILayout2DOptions {
|
||||
readonly anchorAlignment?: AnchorAlignment; // default: left
|
||||
readonly anchorPosition?: AnchorPosition; // default: above
|
||||
readonly anchorAxisAlignment?: AnchorAxisAlignment; // default: vertical
|
||||
}
|
||||
|
||||
export interface ILayout2DResult {
|
||||
top: number;
|
||||
left: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
anchorAlignment: AnchorAlignment;
|
||||
anchorPosition: AnchorPosition;
|
||||
}
|
||||
|
||||
export function layout2d(viewport: IRect, view: ISize, anchor: IRect, options?: ILayout2DOptions): ILayout2DResult {
|
||||
let anchorAlignment = options?.anchorAlignment ?? AnchorAlignment.LEFT;
|
||||
let anchorPosition = options?.anchorPosition ?? AnchorPosition.BELOW;
|
||||
const anchorAxisAlignment = options?.anchorAxisAlignment ?? AnchorAxisAlignment.VERTICAL;
|
||||
|
||||
let top: number;
|
||||
let left: number;
|
||||
|
||||
if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) {
|
||||
const verticalAnchor: ILayoutAnchor = { offset: anchor.top - viewport.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
|
||||
const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
|
||||
|
||||
const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor);
|
||||
top = verticalLayoutResult.position + viewport.top;
|
||||
|
||||
if (verticalLayoutResult.result === 'flipped') {
|
||||
anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW;
|
||||
}
|
||||
|
||||
// if view intersects vertically with anchor, we must avoid the anchor
|
||||
if (Range.intersects({ start: top, end: top + view.height }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
|
||||
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
|
||||
}
|
||||
|
||||
const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor);
|
||||
left = horizontalLayoutResult.position;
|
||||
|
||||
if (horizontalLayoutResult.result === 'flipped') {
|
||||
anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT;
|
||||
}
|
||||
} else {
|
||||
const horizontalAnchor: ILayoutAnchor = { offset: anchor.left, size: anchor.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
|
||||
const verticalAnchor: ILayoutAnchor = { offset: anchor.top, size: anchor.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
|
||||
|
||||
const horizontalLayoutResult = layout(viewport.width, view.width, horizontalAnchor);
|
||||
left = horizontalLayoutResult.position;
|
||||
|
||||
if (horizontalLayoutResult.result === 'flipped') {
|
||||
anchorAlignment = anchorAlignment === AnchorAlignment.LEFT ? AnchorAlignment.RIGHT : AnchorAlignment.LEFT;
|
||||
}
|
||||
|
||||
// if view intersects horizontally with anchor, we must avoid the anchor
|
||||
if (Range.intersects({ start: left, end: left + view.width }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
|
||||
verticalAnchor.mode = LayoutAnchorMode.AVOID;
|
||||
}
|
||||
|
||||
const verticalLayoutResult = layout(viewport.height, view.height, verticalAnchor);
|
||||
top = verticalLayoutResult.position + viewport.top;
|
||||
|
||||
if (verticalLayoutResult.result === 'flipped') {
|
||||
anchorPosition = anchorPosition === AnchorPosition.BELOW ? AnchorPosition.ABOVE : AnchorPosition.BELOW;
|
||||
}
|
||||
}
|
||||
|
||||
const right = viewport.width - (left + view.width);
|
||||
const bottom = viewport.height - (top + view.height);
|
||||
|
||||
return { top, left, bottom, right, anchorAlignment, anchorPosition };
|
||||
}
|
||||
@@ -4,27 +4,26 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { layout, LayoutAnchorPosition } from '../../../../browser/ui/contextview/contextview.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../common/utils.js';
|
||||
import { layout, LayoutAnchorPosition } from '../../common/layout.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js';
|
||||
|
||||
suite('Contextview', function () {
|
||||
suite('Layout', function () {
|
||||
|
||||
test('layout', () => {
|
||||
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }), 0);
|
||||
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }), 50);
|
||||
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }), 180);
|
||||
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.Before }).position, 0);
|
||||
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.Before }).position, 50);
|
||||
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.Before }).position, 180);
|
||||
|
||||
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }), 0);
|
||||
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }), 30);
|
||||
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }), 180);
|
||||
assert.strictEqual(layout(200, 20, { offset: 0, size: 0, position: LayoutAnchorPosition.After }).position, 0);
|
||||
assert.strictEqual(layout(200, 20, { offset: 50, size: 0, position: LayoutAnchorPosition.After }).position, 30);
|
||||
assert.strictEqual(layout(200, 20, { offset: 200, size: 0, position: LayoutAnchorPosition.After }).position, 180);
|
||||
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }).position, 50);
|
||||
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }).position, 100);
|
||||
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }).position, 130);
|
||||
|
||||
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.Before }), 50);
|
||||
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.Before }), 100);
|
||||
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.Before }), 130);
|
||||
|
||||
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }), 50);
|
||||
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }), 30);
|
||||
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }), 130);
|
||||
assert.strictEqual(layout(200, 20, { offset: 0, size: 50, position: LayoutAnchorPosition.After }).position, 50);
|
||||
assert.strictEqual(layout(200, 20, { offset: 50, size: 50, position: LayoutAnchorPosition.After }).position, 30);
|
||||
assert.strictEqual(layout(200, 20, { offset: 150, size: 50, position: LayoutAnchorPosition.After }).position, 130);
|
||||
});
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
@@ -20,6 +20,11 @@
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
|
||||
.quick-input-widget.no-drag .quick-input-titlebar,
|
||||
.quick-input-widget.no-drag .quick-input-title,
|
||||
.quick-input-widget.no-drag .quick-input-header {
|
||||
cursor: default;
|
||||
}
|
||||
.quick-input-widget .monaco-inputbox .monaco-action-bar {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import { TriStateCheckbox, createToggleActionViewItemProvider } from '../../../b
|
||||
import { defaultCheckboxStyles } from '../../theme/browser/defaultStyles.js';
|
||||
import { QuickInputTreeController } from './tree/quickInputTreeController.js';
|
||||
import { QuickTree } from './tree/quickTree.js';
|
||||
import { AnchorAlignment, AnchorPosition, layout2d } from '../../../base/common/layout.js';
|
||||
import { getAnchorRect } from '../../../base/browser/ui/contextview/contextview.js';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
@@ -541,6 +543,7 @@ export class QuickInputController extends Disposable {
|
||||
input.quickNavigate = options.quickNavigate;
|
||||
input.hideInput = !!options.hideInput;
|
||||
input.contextKey = options.contextKey;
|
||||
input.anchor = options.anchor;
|
||||
input.busy = true;
|
||||
Promise.all([picks, options.activeItem])
|
||||
.then(([items, _activeItem]) => {
|
||||
@@ -710,6 +713,7 @@ export class QuickInputController extends Disposable {
|
||||
|
||||
ui.container.style.display = '';
|
||||
this.updateLayout();
|
||||
this.dndController?.setEnabled(!controller.anchor);
|
||||
this.dndController?.layoutContainer();
|
||||
ui.inputBox.setFocus();
|
||||
this.quickInputTypeContext.set(controller.type);
|
||||
@@ -861,16 +865,52 @@ export class QuickInputController extends Disposable {
|
||||
private updateLayout() {
|
||||
if (this.ui && this.isVisible()) {
|
||||
const style = this.ui.container.style;
|
||||
const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH);
|
||||
let width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH);
|
||||
style.width = width + 'px';
|
||||
|
||||
let listHeight = this.dimension && this.dimension.height * 0.4;
|
||||
|
||||
// Position
|
||||
style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`;
|
||||
style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`;
|
||||
if (this.controller?.anchor) {
|
||||
const container = this.layoutService.getContainer(dom.getActiveWindow()).getBoundingClientRect();
|
||||
const anchor = getAnchorRect(this.controller.anchor);
|
||||
width = 380;
|
||||
listHeight = this.dimension ? Math.min(this.dimension.height * 0.2, 200) : 200;
|
||||
|
||||
// Beware:
|
||||
// We need to add some extra pixels to the height to account for the input and padding.
|
||||
const containerHeight = Math.floor(listHeight) + 6 + 26 + 16;
|
||||
const { top, left, right, bottom, anchorAlignment, anchorPosition } = layout2d(container, { width, height: containerHeight }, anchor, { anchorPosition: AnchorPosition.ABOVE });
|
||||
|
||||
if (anchorAlignment === AnchorAlignment.RIGHT) {
|
||||
style.right = `${right}px`;
|
||||
style.left = 'initial';
|
||||
} else {
|
||||
style.left = `${left}px`;
|
||||
style.right = 'initial';
|
||||
}
|
||||
|
||||
if (anchorPosition === AnchorPosition.ABOVE) {
|
||||
style.bottom = `${bottom}px`;
|
||||
style.top = 'initial';
|
||||
} else {
|
||||
style.top = `${top}px`;
|
||||
style.bottom = 'initial';
|
||||
}
|
||||
|
||||
style.width = `${width}px`;
|
||||
style.height = '';
|
||||
} else {
|
||||
style.top = `${this.viewState?.top ? Math.round(this.dimension!.height * this.viewState.top) : this.titleBarOffset}px`;
|
||||
style.left = `${Math.round((this.dimension!.width * (this.viewState?.left ?? 0.5 /* center */)) - (width / 2))}px`;
|
||||
style.right = '';
|
||||
style.bottom = '';
|
||||
style.height = '';
|
||||
}
|
||||
|
||||
this.ui.inputBox.layout();
|
||||
this.ui.list.layout(this.dimension && this.dimension.height * 0.4);
|
||||
this.ui.tree.layout(this.dimension && this.dimension.height * 0.4);
|
||||
this.ui.list.layout(listHeight);
|
||||
this.ui.tree.layout(listHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -965,6 +1005,8 @@ export interface IQuickInputControllerHost extends ILayoutService { }
|
||||
class QuickInputDragAndDropController extends Disposable {
|
||||
readonly dndViewState = observableValue<{ top?: number; left?: number; done: boolean } | undefined>(this, undefined);
|
||||
|
||||
private _enabled = true;
|
||||
|
||||
private readonly _snapThreshold = 20;
|
||||
private readonly _snapLineHorizontalRatio = 0.25;
|
||||
|
||||
@@ -1000,6 +1042,10 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
}
|
||||
|
||||
layoutContainer(dimension = this._layoutService.activeContainerDimension): void {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = this.dndViewState.get();
|
||||
const dragAreaRect = this._quickInputContainer.getBoundingClientRect();
|
||||
if (state?.top && state?.left) {
|
||||
@@ -1011,6 +1057,11 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean): void {
|
||||
this._enabled = enabled;
|
||||
this._quickInputContainer.classList.toggle('no-drag', !enabled);
|
||||
}
|
||||
|
||||
setAlignment(alignment: 'top' | 'center' | { top: number; left: number }, done = true): void {
|
||||
if (alignment === 'top') {
|
||||
this.dndViewState.set({
|
||||
@@ -1041,6 +1092,10 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
|
||||
// Double click
|
||||
this._register(dom.addDisposableGenericMouseUpListener(dragArea, (event: MouseEvent) => {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originEvent = new StandardMouseEvent(dom.getWindow(dragArea), event);
|
||||
if (originEvent.detail !== 2) {
|
||||
return;
|
||||
@@ -1057,6 +1112,10 @@ class QuickInputDragAndDropController extends Disposable {
|
||||
|
||||
// Mouse down
|
||||
this._register(dom.addDisposableGenericMouseDownListener(dragArea, (e: MouseEvent) => {
|
||||
if (!this._enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeWindow = dom.getWindow(this._layoutService.activeContainer);
|
||||
const originEvent = new StandardMouseEvent(activeWindow, e);
|
||||
|
||||
|
||||
@@ -197,6 +197,11 @@ export interface IPickOptions<T extends IQuickPickItem> {
|
||||
*/
|
||||
activeItem?: Promise<T> | T;
|
||||
|
||||
/**
|
||||
* an optional anchor for the picker
|
||||
*/
|
||||
anchor?: HTMLElement | { x: number; y: number };
|
||||
|
||||
onKeyMods?: (keyMods: IKeyMods) => void;
|
||||
onDidFocus?: (entry: T) => void;
|
||||
onDidTriggerItemButton?: (context: IQuickPickItemButtonContext<T>) => void;
|
||||
@@ -353,6 +358,11 @@ export interface IQuickInput extends IDisposable {
|
||||
*/
|
||||
ignoreFocusOut: boolean;
|
||||
|
||||
/**
|
||||
* An optional anchor for the quick input.
|
||||
*/
|
||||
anchor?: HTMLElement | { x: number; y: number };
|
||||
|
||||
/**
|
||||
* Shows the quick input.
|
||||
*/
|
||||
|
||||
@@ -149,7 +149,7 @@ export class PickAgentSessionAction extends Action2 {
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
|
||||
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker);
|
||||
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, undefined);
|
||||
await agentSessionsPicker.pickAgentSession();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ export class AgentSessionsPicker {
|
||||
private readonly sorter = new AgentSessionsSorter();
|
||||
|
||||
constructor(
|
||||
private readonly anchor: HTMLElement | undefined,
|
||||
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
|
||||
@IQuickInputService private readonly quickInputService: IQuickInputService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@@ -78,6 +79,7 @@ export class AgentSessionsPicker {
|
||||
const picker = disposables.add(this.quickInputService.createQuickPick<ISessionPickItem>({ useSeparators: true }));
|
||||
const filter = disposables.add(this.instantiationService.createInstance(AgentSessionsFilter, {}));
|
||||
|
||||
picker.anchor = this.anchor;
|
||||
picker.items = this.createPickerItems(filter);
|
||||
picker.canAcceptInBackground = true;
|
||||
picker.placeholder = localize('chatAgentPickerPlaceholder', "Search agent sessions by name");
|
||||
|
||||
@@ -59,6 +59,8 @@ export class ChatViewTitleControl extends Disposable {
|
||||
}
|
||||
|
||||
private registerActions(): void {
|
||||
const that = this;
|
||||
|
||||
this._register(registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
@@ -76,7 +78,7 @@ export class ChatViewTitleControl extends Disposable {
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
|
||||
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker);
|
||||
const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker, that.titleLabel.value?.element);
|
||||
await agentSessionsPicker.pickAgentSession();
|
||||
}
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user