diff --git a/src/core/events/WindowFormEventManager.ts b/src/core/events/WindowFormEventManager.ts new file mode 100644 index 0000000..db82647 --- /dev/null +++ b/src/core/events/WindowFormEventManager.ts @@ -0,0 +1,57 @@ +import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts' +import type { IEventMap } from '@/core/events/IEventBuilder.ts' +import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts' + +/** + * 窗口的事件 + */ +export interface WindowFormEvent extends IEventMap { + /** + * 窗口最小化 + * @param id 窗口id + */ + windowFormMinimize: (id: string) => void; + /** + * 窗口最大化 + * @param id 窗口id + */ + windowFormMaximize: (id: string) => void; + /** + * 窗口还原 + * @param id 窗口id + */ + windowFormRestore: (id: string) => void; + /** + * 窗口关闭 + * @param id 窗口id + */ + windowFormClose: (id: string) => void; + /** + * 窗口聚焦 + * @param id 窗口id + */ + windowFormFocus: (id: string) => void; + /** + * 窗口数据更新 + * @param data 窗口数据 + */ + windowFormDataUpdate: (data: IWindowFormDataUpdateParams) => void; +} + +interface IWindowFormDataUpdateParams { + /** 窗口id */ + id: string; + /** 窗口状态 */ + state: TWindowFormState, + /** 窗口宽度 */ + width: number, + /** 窗口高度 */ + height: number, + /** 窗口x坐标(左上角) */ + x: number, + /** 窗口y坐标(左上角) */ + y: number +} + +/** 窗口事件管理器 */ +export const wfem = new EventBuilderImpl() diff --git a/src/core/process/IProcess.ts b/src/core/process/IProcess.ts index c70b8ca..28f61d0 100644 --- a/src/core/process/IProcess.ts +++ b/src/core/process/IProcess.ts @@ -1,7 +1,7 @@ import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' import type { IWindowForm } from '@/core/window/IWindowForm.ts' import type { IEventBuilder } from '@/core/events/IEventBuilder.ts' -import type { IProcessEvent } from '@/core/process/types/ProcessEvent.ts' +import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts' /** * 进程接口 diff --git a/src/core/process/impl/ProcessImpl.ts b/src/core/process/impl/ProcessImpl.ts index bbfe57f..64125cc 100644 --- a/src/core/process/impl/ProcessImpl.ts +++ b/src/core/process/impl/ProcessImpl.ts @@ -6,7 +6,7 @@ import type { IWindowForm } from '@/core/window/IWindowForm.ts' import { processManager } from '@/core/process/ProcessManager.ts' import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts' import type { IEventBuilder } from '@/core/events/IEventBuilder.ts' -import type { IProcessEvent } from '@/core/process/types/ProcessEvent.ts' +import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts' /** * 进程 diff --git a/src/core/process/types/ProcessEvent.ts b/src/core/process/types/ProcessEventTypes.ts similarity index 100% rename from src/core/process/types/ProcessEvent.ts rename to src/core/process/types/ProcessEventTypes.ts diff --git a/src/core/state/impl/ObservableImpl.ts b/src/core/state/impl/ObservableImpl.ts index 1dbbd49..c063e42 100644 --- a/src/core/state/impl/ObservableImpl.ts +++ b/src/core/state/impl/ObservableImpl.ts @@ -180,7 +180,7 @@ export class ObservableImpl> implements IObs } /** 订阅整个状态变化 */ - subscribe(fn: TObservableListener, options: { immediate?: boolean } = {}): () => void { + public subscribe(fn: TObservableListener, options: { immediate?: boolean } = {}): () => void { this.listeners.add(fn) if (options.immediate) fn(this.state) return () => { @@ -189,7 +189,7 @@ export class ObservableImpl> implements IObs } /** 订阅指定字段变化 */ - subscribeKey( + public subscribeKey( keys: K | K[], fn: TObservableKeyListener, options: { immediate?: boolean } = {} @@ -214,7 +214,7 @@ export class ObservableImpl> implements IObs } /** 批量更新状态 */ - patch(values: Partial): void { + public patch(values: Partial): void { for (const key in values) { if (Object.prototype.hasOwnProperty.call(values, key)) { const typedKey = key as keyof T @@ -226,7 +226,7 @@ export class ObservableImpl> implements IObs } /** 销毁 Observable 实例 */ - dispose(): void { + public dispose(): void { this.disposed = true this.listeners.clear() this.keyListeners.clear() @@ -234,7 +234,7 @@ export class ObservableImpl> implements IObs } /** 语法糖:返回一个可解构赋值的 Proxy */ - toRefsProxy(): { [K in keyof T]: T[K] } { + public toRefsProxy(): { [K in keyof T]: T[K] } { const self = this return new Proxy({} as T, { get(_, prop: string | symbol) { diff --git a/src/core/window/impl/WindowFormImpl.ts b/src/core/window/impl/WindowFormImpl.ts index b36a9b7..08f7aa9 100644 --- a/src/core/window/impl/WindowFormImpl.ts +++ b/src/core/window/impl/WindowFormImpl.ts @@ -9,91 +9,125 @@ import { DraggableResizableWindow } from '@/core/utils/DraggableResizableWindow. import '../css/window-form.scss' import { serviceManager } from '@/core/service/kernel/ServiceManager.ts' import '../ui/WindowFormElement.ts' +import { wfem } from '@/core/events/WindowFormEventManager.ts' +import type { IObservable } from '@/core/state/IObservable.ts' +import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts' export default class WindowFormImpl implements IWindowForm { - private readonly _id: string = uuidV4(); - private readonly _procId: string; - private dom: HTMLElement; - private drw: DraggableResizableWindow; - private pos: WindowFormPos = { x: 0, y: 0 }; - private width: number; - private height: number; + private readonly _id: string = uuidV4() + private readonly _procId: string + private dom: HTMLElement + private drw: DraggableResizableWindow + private pos: WindowFormPos = { x: 0, y: 0 } + private width: number + private height: number + private _state: IObservable<{ x: number, y: number }> = new ObservableImpl({ + x: 0, + y: 0, + }) public get id() { - return this._id; + return this._id } public get proc() { return processManager.findProcessById(this._procId) } private get desktopRootDom() { - return XSystem.instance.desktopRootDom; + return XSystem.instance.desktopRootDom } private get sm() { return serviceManager.getService('WindowForm') } public get windowFormEle() { - return this.dom; + return this.dom } public get windowFormState() { return this.drw.windowFormState } constructor(proc: IProcess, config: IWindowFormConfig) { - this._procId = proc.id; + this._procId = proc.id console.log('WindowForm') this.pos = { x: config.left ?? 0, - y: config.top ?? 0 + y: config.top ?? 0, } - this.width = config.width ?? 200; - this.height = config.height ?? 100; + this.width = config.width ?? 200 + this.height = config.height ?? 100 - this.createWindowFrom(); + this.createWindowFrom() + this.initEvent() + } + + private initEvent() { + wfem.addEventListener('windowFormClose', this.windowFormCloseFun) + } + + private windowFormCloseFun = (id: string) => { + if (id === this.id) { + this.closeWindowForm() + } } private createWindowFrom() { - this.dom = this.createWindowFormEle(); - this.dom.style.position = 'absolute'; - this.dom.style.width = `${this.width}px`; - this.dom.style.height = `${this.height}px`; - this.dom.style.zIndex = '10'; - - const header = this.dom.querySelector('.title-bar') as HTMLElement; - const content = this.dom.querySelector('.window-content') as HTMLElement; - this.drw = new DraggableResizableWindow({ - target: this.dom, - handle: header, - snapAnimation: true, - snapThreshold: 20, - boundaryElement: document.body, - taskbarElementId: '#taskbar', - onWindowStateChange: (state) => { - if (state === 'maximized') { - this.dom.style.borderRadius = '0px'; - } else { - this.dom.style.borderRadius = '5px'; - } - }, - }) + // this.dom = this.createWindowFormEle(); + // this.dom.style.position = 'absolute'; + // this.dom.style.width = `${this.width}px`; + // this.dom.style.height = `${this.height}px`; + // this.dom.style.zIndex = '10'; + // + // const header = this.dom.querySelector('.title-bar') as HTMLElement; + // const content = this.dom.querySelector('.window-content') as HTMLElement; + // this.drw = new DraggableResizableWindow({ + // target: this.dom, + // handle: header, + // snapAnimation: true, + // snapThreshold: 20, + // boundaryElement: document.body, + // taskbarElementId: '#taskbar', + // onWindowStateChange: (state) => { + // if (state === 'maximized') { + // this.dom.style.borderRadius = '0px'; + // } else { + // this.dom.style.borderRadius = '5px'; + // } + // }, + // }) // this.desktopRootDom.appendChild(this.dom); const wf = document.createElement('window-form-element') + wf.pos = this._state + wf.wid = this.id wf.dragContainer = document.body wf.snapDistance = 20 - wf.addEventListener('windowForm:dragStart', (e) => { - console.log('dragstart', e) + wf.taskbarElementId = '#taskbar' + wf.addManagedEventListener('windowForm:stateChange:minimize', (e) => { + console.log('windowForm:stateChange:minimize', e) }) - this.desktopRootDom.appendChild(wf) + wf.addManagedEventListener('windowForm:stateChange:maximize', (e) => { + console.log('windowForm:stateChange:maximize', e) + }) + wf.addManagedEventListener('windowForm:stateChange:restore', (e) => { + console.log('windowForm:stateChange:restore', e) + }) + wf.addManagedEventListener('windowForm:close', () => { + this.closeWindowForm() + }) + this.dom = wf + this.desktopRootDom.appendChild(this.dom) + wfem.notifyEvent('windowFormFocus', this.id) } public closeWindowForm() { - this.drw.destroy(); - this.desktopRootDom.removeChild(this.dom); + // this.drw.destroy(); + this.desktopRootDom.removeChild(this.dom) this.proc?.event.notifyEvent('onProcessWindowFormExit', this.id) + wfem.removeEventListener('windowFormClose', this.windowFormCloseFun) + // wfem.notifyEvent('windowFormClose', this.id) } private createWindowFormEle() { - const template = document.createElement('template'); + const template = document.createElement('template') template.innerHTML = `
@@ -107,23 +141,24 @@ export default class WindowFormImpl implements IWindowForm {
` - const fragment = template.content.cloneNode(true) as DocumentFragment; + const fragment = template.content.cloneNode(true) as DocumentFragment const windowElement = fragment.firstElementChild as HTMLElement - windowElement.querySelector('.btn.minimize') - ?.addEventListener('click', () => this.drw.minimize()); + windowElement + .querySelector('.btn.minimize') + ?.addEventListener('click', () => this.drw.minimize()) - windowElement.querySelector('.btn.maximize') - ?.addEventListener('click', () => { - if (this.drw.windowFormState === 'maximized') { - this.drw.restore() - } else { - this.drw.maximize() - } - }); + windowElement.querySelector('.btn.maximize')?.addEventListener('click', () => { + if (this.drw.windowFormState === 'maximized') { + this.drw.restore() + } else { + this.drw.maximize() + } + }) - windowElement.querySelector('.btn.close') - ?.addEventListener('click', () => this.closeWindowForm()); + windowElement + .querySelector('.btn.close') + ?.addEventListener('click', () => this.closeWindowForm()) return windowElement } diff --git a/src/core/window/ui/WindowFormElement.ts b/src/core/window/ui/WindowFormElement.ts index 04fc0d2..a818ccd 100644 --- a/src/core/window/ui/WindowFormElement.ts +++ b/src/core/window/ui/WindowFormElement.ts @@ -2,6 +2,8 @@ import { LitElement, html, css, unsafeCSS } from 'lit' import { customElement, property } from 'lit/decorators.js'; import wfStyle from './wf.scss?inline' import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts' +import { wfem } from '@/core/events/WindowFormEventManager.ts' +import type { IObservable } from '@/core/state/IObservable.ts' /** 拖拽移动开始的回调 */ type TDragStartCallback = (x: number, y: number) => void; @@ -55,161 +57,215 @@ export interface WindowFormEventMap { 'windowForm:resizeMove': CustomEvent; 'windowForm:resizeEnd': CustomEvent; 'windowForm:stateChange': CustomEvent<{ state: TWindowFormState }>; + 'windowForm:stateChange:minimize': CustomEvent<{ state: TWindowFormState }>; + 'windowForm:stateChange:maximize': CustomEvent<{ state: TWindowFormState }>; + 'windowForm:stateChange:restore': CustomEvent<{ state: TWindowFormState }>; + 'windowForm:close': CustomEvent; } @customElement('window-form-element') export class WindowFormElement extends LitElement { // ==== 公共属性 ==== - @property({ type: String }) override title = 'Window'; - @property({ type: Boolean }) resizable = true; - @property({ type: Boolean }) minimizable = true; - @property({ type: Boolean }) maximizable = true; - @property({ type: Boolean }) closable = true; - @property({ type: Boolean, reflect: true }) focused = false; - @property({ type: Object }) dragContainer?: HTMLElement; - @property({ type: Boolean }) allowOverflow = true; // 允许窗口超出容器 - @property({ type: Number }) snapDistance = 0; // 吸附距离 - @property({ type: Boolean }) snapAnimation = true; // 吸附动画 - @property({ type: Number }) snapAnimationDuration = 300; // 吸附动画时长 ms - @property({ type: Number }) maxWidth?: number; - @property({ type: Number }) minWidth?: number; - @property({ type: Number }) maxHeight?: number; - @property({ type: Number }) minHeight?: number; - @property({ type: String }) taskbarElementId?: string; + @property({ type: String }) wid: string + @property({ type: String }) override title = 'Window' + @property({ type: Boolean }) resizable = true + @property({ type: Boolean }) minimizable = true + @property({ type: Boolean }) maximizable = true + @property({ type: Boolean }) closable = true + @property({ type: Boolean, reflect: true }) focused: boolean = true + @property({ type: String, reflect: true }) windowFormState: TWindowFormState = 'default' + @property({ type: Object }) dragContainer?: HTMLElement + @property({ type: Boolean }) allowOverflow = true // 允许窗口超出容器 + @property({ type: Number }) snapDistance = 0 // 吸附距离 + @property({ type: Boolean }) snapAnimation = true // 吸附动画 + @property({ type: Number }) snapAnimationDuration = 300 // 吸附动画时长 ms + @property({ type: Number }) maxWidth?: number + @property({ type: Number }) minWidth?: number + @property({ type: Number }) maxHeight?: number + @property({ type: Number }) minHeight?: number + @property({ type: String }) taskbarElementId?: string + @property({ type: Object }) pos: IObservable<{ x: number, y: number }>; + + private _listeners: Array<{ type: string; handler: EventListener }> = [] // ==== 拖拽/缩放状态(内部变量,不触发渲染) ==== - private dragging = false; - private resizeDir: TResizeDirection | null = null; - private startX = 0; - private startY = 0; - private startWidth = 0; - private startHeight = 0; - private startX_host = 0; - private startY_host = 0; + private dragging = false + private resizeDir: TResizeDirection | null = null + private startX = 0 + private startY = 0 + private startWidth = 0 + private startHeight = 0 + private startX_host = 0 + private startY_host = 0 - private x = 0; - private y = 0; - private preX = 0; - private preY = 0; - private width = 640; - private height = 360; - private animationFrame?: number; - private resizing = false; + private x = 0 + private y = 0 + private preX = 0 + private preY = 0 + private width = 640 + private height = 360 + private animationFrame?: number + private resizing = false - private minimized = false; - private maximized = false; - private windowFormState: TWindowFormState = 'default'; + // private windowFormState: TWindowFormState = 'default'; /** 元素信息 */ - private targetBounds: IElementRect; + private targetBounds: IElementRect /** 最小化前的元素信息 */ - private targetPreMinimizeBounds?: IElementRect; + private targetPreMinimizeBounds?: IElementRect /** 最大化前的元素信息 */ - private targetPreMaximizedBounds?: IElementRect; + private targetPreMaximizedBounds?: IElementRect - static override styles = css`${unsafeCSS(wfStyle)}`; + static override styles = css` + ${unsafeCSS(wfStyle)} + ` protected override createRenderRoot() { - const root = this.attachShadow({ mode: 'closed' }); - const sheet = new CSSStyleSheet(); - sheet.replaceSync(wfStyle); - root.adoptedStyleSheets = [sheet]; + const root = this.attachShadow({ mode: 'closed' }) + const sheet = new CSSStyleSheet() + sheet.replaceSync(wfStyle) + root.adoptedStyleSheets = [sheet] return root } - override firstUpdated() { - window.addEventListener('pointerup', this.onPointerUp); - window.addEventListener('pointermove', this.onPointerMove); - this.addEventListener('pointerdown', () => this.bringToFront()); + /** + * 添加受管理的事件监听 + * @param type 事件类型 + * @param handler 事件处理函数 + */ + public addManagedEventListener(type: string, handler: EventListener) { + this.addEventListener(type, handler) + this._listeners.push({ type, handler }) + } - const container = this.dragContainer || document.body; - const containerRect = container.getBoundingClientRect(); - this.x = containerRect.width / 2 - this.width / 2; - this.y = containerRect.height / 2 - this.height / 2; - this.style.width = `${this.width}px`; - this.style.height = `${this.height}px`; - this.style.transform = `translate(${this.x}px, ${this.y}px)`; + /** + * 移除所有受管理事件监听 + */ + public removeAllManagedListeners() { + for (const { type, handler } of this._listeners) { + this.removeEventListener(type, handler) + } + this._listeners = [] + } + + override firstUpdated() { + wfem.addEventListener('windowFormFocus', this.windowFormFocusFun) + window.addEventListener('pointerup', this.onPointerUp) + window.addEventListener('pointermove', this.onPointerMove) + this.addEventListener('pointerdown', this.toggleFocus) + + const container = this.dragContainer || document.body + const containerRect = container.getBoundingClientRect() + this.x = containerRect.width / 2 - this.width / 2 + this.y = containerRect.height / 2 - this.height / 2 + this.style.width = `${this.width}px` + this.style.height = `${this.height}px` + this.style.transform = `translate(${this.x}px, ${this.y}px)` this.targetBounds = { width: this.offsetWidth, height: this.offsetHeight, top: this.x, left: this.y, - }; + } } override disconnectedCallback() { - super.disconnectedCallback(); - window.removeEventListener('pointerup', this.onPointerUp); - window.removeEventListener('pointermove', this.onPointerMove); + super.disconnectedCallback() + window.removeEventListener('pointerup', this.onPointerUp) + window.removeEventListener('pointermove', this.onPointerMove) + this.removeEventListener('pointerdown', this.toggleFocus) + wfem.removeEventListener('windowFormFocus', this.windowFormFocusFun) + this.removeAllManagedListeners() } - // 窗口聚焦、置顶 - private bringToFront() { - this.focused = true; + private windowFormFocusFun = (id: string) => { + if (id === this.wid) { + this.focused = true + } else { + this.focused = false + } + } + + private toggleFocus = () => { + this.focused = !this.focused + wfem.notifyEvent('windowFormFocus', this.wid) + this.dispatchEvent( + new CustomEvent('update:focused', { + detail: this.focused, + bubbles: true, + composed: true, + }), + ) } // ====== 拖拽 ====== private onTitlePointerDown = (e: PointerEvent) => { - if (e.pointerType === 'mouse' && e.button !== 0) return; - if ((e.target as HTMLElement).closest('.controls')) return; + if (e.pointerType === 'mouse' && e.button !== 0) return + if ((e.target as HTMLElement).closest('.controls')) return - e.preventDefault(); + e.preventDefault() - this.dragging = true; - this.startX = e.clientX; - this.startY = e.clientY; - this.preX = this.x; - this.preY = this.y; - this.setPointerCapture?.(e.pointerId); + this.dragging = true + this.startX = e.clientX + this.startY = e.clientY + this.preX = this.x + this.preY = this.y + this.setPointerCapture?.(e.pointerId) - this.dispatchEvent(new CustomEvent('windowForm:dragStart', { - detail: { x: this.x, y: this.y }, - bubbles: true, - composed: true - })); - }; + this.dispatchEvent( + new CustomEvent('windowForm:dragStart', { + detail: { x: this.x, y: this.y }, + bubbles: true, + composed: true, + }), + ) + } private onPointerMove = (e: PointerEvent) => { if (this.dragging) { - const dx = e.clientX - this.startX; - const dy = e.clientY - this.startY; + const dx = e.clientX - this.startX + const dy = e.clientY - this.startY - const x = this.preX + dx; - const y = this.preY + dy; + const x = this.preX + dx + const y = this.preY + dy - this.applyPosition(x, y, false); - this.dispatchEvent(new CustomEvent('windowForm:dragMove', { - detail: { x, y }, - bubbles: true, - composed: true - })); + this.applyPosition(x, y, false) + this.dispatchEvent( + new CustomEvent('windowForm:dragMove', { + detail: { x, y }, + bubbles: true, + composed: true, + }), + ) } else if (this.resizeDir) { - this.performResize(e); + this.performResize(e) } - }; + } private onPointerUp = (e: PointerEvent) => { if (this.dragging) { this.dragUp(e) } if (this.resizeDir) { - this.resizeUp(e); + this.resizeUp(e) } - this.dragging = false; - this.resizing = false; - this.resizeDir = null; + this.dragging = false + this.resizing = false + this.resizeDir = null document.body.style.cursor = '' - try { this.releasePointerCapture?.(e.pointerId); } catch {} - }; + try { + this.releasePointerCapture?.(e.pointerId) + } catch {} + } /** 获取所有吸附点 */ private getSnapPoints() { - const snapPoints = { x: [] as number[], y: [] as number[] }; - const containerRect = (this.dragContainer || document.body).getBoundingClientRect(); - const rect = this.getBoundingClientRect(); - snapPoints.x = [0, containerRect.width - rect.width]; - snapPoints.y = [0, containerRect.height - rect.height]; - return snapPoints; + const snapPoints = { x: [] as number[], y: [] as number[] } + const containerRect = (this.dragContainer || document.body).getBoundingClientRect() + const rect = this.getBoundingClientRect() + snapPoints.x = [0, containerRect.width - rect.width] + snapPoints.y = [0, containerRect.height - rect.height] + return snapPoints } /** @@ -218,147 +274,192 @@ export class WindowFormElement extends LitElement { * @param y 左上角起始点y */ private applySnapping(x: number, y: number) { - let snappedX = x, snappedY = y; - const containerSnap = this.getSnapPoints(); + let snappedX = x, + snappedY = y + const containerSnap = this.getSnapPoints() if (this.snapDistance > 0) { - for (const sx of containerSnap.x) if (Math.abs(x - sx) <= this.snapDistance) { snappedX = sx; break; } - for (const sy of containerSnap.y) if (Math.abs(y - sy) <= this.snapDistance) { snappedY = sy; break; } + for (const sx of containerSnap.x) + if (Math.abs(x - sx) <= this.snapDistance) { + snappedX = sx + break + } + for (const sy of containerSnap.y) + if (Math.abs(y - sy) <= this.snapDistance) { + snappedY = sy + break + } } - return { x: snappedX, y: snappedY }; + return { x: snappedX, y: snappedY } } private dragUp(e: PointerEvent) { - const snapped = this.applySnapping(this.x, this.y); + const snapped = this.applySnapping(this.x, this.y) if (this.snapAnimation) { this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => { - this.updateTargetBounds(snapped.x, snapped.y); - this.dispatchEvent(new CustomEvent('windowForm:dragEnd', { + this.updateTargetBounds(snapped.x, snapped.y) + this.dispatchEvent( + new CustomEvent('windowForm:dragEnd', { + detail: { x: snapped.x, y: snapped.y }, + bubbles: true, + composed: true, + }), + ) + }) + } else { + this.applyPosition(snapped.x, snapped.y, true) + this.updateTargetBounds(snapped.x, snapped.y) + this.dispatchEvent( + new CustomEvent('windowForm:dragEnd', { detail: { x: snapped.x, y: snapped.y }, bubbles: true, - composed: true - })); - }); - } else { - this.applyPosition(snapped.x, snapped.y, true); - this.updateTargetBounds(snapped.x, snapped.y); - this.dispatchEvent(new CustomEvent('windowForm:dragEnd', { - detail: { x: snapped.x, y: snapped.y }, - bubbles: true, - composed: true - })); + composed: true, + }), + ) } } private applyPosition(x: number, y: number, isFinal: boolean) { - this.x = x; - this.y = y; - this.style.transform = `translate(${x}px, ${y}px)`; - if (isFinal) this.applyBoundary(); + this.x = x + this.y = y + this.style.transform = `translate(${x}px, ${y}px)` + if (isFinal) this.applyBoundary() } private applyBoundary() { - if (this.allowOverflow) return; - let { x, y } = { x: this.x, y: this.y }; + if (this.allowOverflow) return + let { x, y } = { x: this.x, y: this.y } - const rect = this.getBoundingClientRect(); - const containerRect = (this.dragContainer || document.body).getBoundingClientRect(); - x = Math.min(Math.max(x, 0), containerRect.width - rect.width); - y = Math.min(Math.max(y, 0), containerRect.height - rect.height); + const rect = this.getBoundingClientRect() + const containerRect = (this.dragContainer || document.body).getBoundingClientRect() + x = Math.min(Math.max(x, 0), containerRect.width - rect.width) + y = Math.min(Math.max(y, 0), containerRect.height - rect.height) - this.applyPosition(x, y, false); + this.applyPosition(x, y, false) } private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) { - if (this.animationFrame) cancelAnimationFrame(this.animationFrame); - const startX = this.x; - const startY = this.y; - const deltaX = targetX - startX; - const deltaY = targetY - startY; - const startTime = performance.now(); + if (this.animationFrame) cancelAnimationFrame(this.animationFrame) + const startX = this.x + const startY = this.y + const deltaX = targetX - startX + const deltaY = targetY - startY + const startTime = performance.now() const step = (now: number) => { - const elapsed = now - startTime; - const progress = Math.min(elapsed / duration, 1); - const ease = 1 - Math.pow(1 - progress, 3); + const elapsed = now - startTime + const progress = Math.min(elapsed / duration, 1) + const ease = 1 - Math.pow(1 - progress, 3) - const x = startX + deltaX * ease; - const y = startY + deltaY * ease; + const x = startX + deltaX * ease + const y = startY + deltaY * ease - this.applyPosition(x, y, false); - this.dispatchEvent(new CustomEvent('windowForm:dragMove', { - detail: { x, y }, - bubbles: true, - composed: true - })); + this.applyPosition(x, y, false) + this.dispatchEvent( + new CustomEvent('windowForm:dragMove', { + detail: { x, y }, + bubbles: true, + composed: true, + }), + ) if (progress < 1) { - this.animationFrame = requestAnimationFrame(step); + this.animationFrame = requestAnimationFrame(step) } else { - this.applyPosition(targetX, targetY, true); - onComplete?.(); + this.applyPosition(targetX, targetY, true) + onComplete?.() } - }; - this.animationFrame = requestAnimationFrame(step); + } + this.animationFrame = requestAnimationFrame(step) } // ====== 缩放 ====== private startResize = (dir: TResizeDirection, e: PointerEvent) => { - if (!this.resizable) return; - if (e.pointerType === 'mouse' && e.button !== 0) return; + if (!this.resizable) return + if (e.pointerType === 'mouse' && e.button !== 0) return - e.preventDefault(); - e.stopPropagation(); - this.resizing = true; - this.resizeDir = dir; - this.startX = e.clientX; - this.startY = e.clientY; + e.preventDefault() + e.stopPropagation() + this.resizing = true + this.resizeDir = dir + this.startX = e.clientX + this.startY = e.clientY - const rect = this.getBoundingClientRect(); - this.startWidth = rect.width; - this.startHeight = rect.height; - this.startX_host = rect.left; - this.startY_host = rect.top; + const rect = this.getBoundingClientRect() + this.startWidth = rect.width + this.startHeight = rect.height + this.startX_host = rect.left + this.startY_host = rect.top - const target = e.target as HTMLElement; - document.body.style.cursor = target.style.cursor || window.getComputedStyle(target).cursor; + const target = e.target as HTMLElement + document.body.style.cursor = target.style.cursor || window.getComputedStyle(target).cursor - this.setPointerCapture?.(e.pointerId); - this.dispatchEvent(new CustomEvent('windowForm:resizeStart', { - detail: { x: this.x, y: this.y, width: this.width, height: this.height, dir }, - bubbles: true, - composed: true - })); - }; + this.setPointerCapture?.(e.pointerId) + this.dispatchEvent( + new CustomEvent('windowForm:resizeStart', { + detail: { x: this.x, y: this.y, width: this.width, height: this.height, dir }, + bubbles: true, + composed: true, + }), + ) + } private performResize(e: PointerEvent) { - if (!this.resizeDir || !this.resizing) return; + if (!this.resizeDir || !this.resizing) return - let newWidth = this.startWidth; - let newHeight = this.startHeight; - let newX = this.startX_host; - let newY = this.startY_host; + let newWidth = this.startWidth + let newHeight = this.startHeight + let newX = this.startX_host + let newY = this.startY_host - const dx = e.clientX - this.startX; - const dy = e.clientY - this.startY; + const dx = e.clientX - this.startX + const dy = e.clientY - this.startY switch (this.resizeDir) { - case 'r': newWidth += dx; break; - case 'b': newHeight += dy; break; - case 'l': newWidth -= dx; newX += dx; break; - case 't': newHeight -= dy; newY += dy; break; - case 'tl': newWidth -= dx; newX += dx; newHeight -= dy; newY += dy; break; - case 'tr': newWidth += dx; newHeight -= dy; newY += dy; break; - case 'bl': newWidth -= dx; newX += dx; newHeight += dy; break; - case 'br': newWidth += dx; newHeight += dy; break; + case 'r': + newWidth += dx + break + case 'b': + newHeight += dy + break + case 'l': + newWidth -= dx + newX += dx + break + case 't': + newHeight -= dy + newY += dy + break + case 'tl': + newWidth -= dx + newX += dx + newHeight -= dy + newY += dy + break + case 'tr': + newWidth += dx + newHeight -= dy + newY += dy + break + case 'bl': + newWidth -= dx + newX += dx + newHeight += dy + break + case 'br': + newWidth += dx + newHeight += dy + break } - const d = this.applyResizeBounds(newX, newY, newWidth, newHeight); + const d = this.applyResizeBounds(newX, newY, newWidth, newHeight) - this.dispatchEvent(new CustomEvent('windowForm:resizeMove', { - detail: { dir: this.resizeDir, width: d.width, height: d.height, left: d.left, top: d.top }, - bubbles: true, - composed: true - })); + this.dispatchEvent( + new CustomEvent('windowForm:resizeMove', { + detail: { dir: this.resizeDir, width: d.width, height: d.height, left: d.left, top: d.top }, + bubbles: true, + composed: true, + }), + ) } /** @@ -369,27 +470,32 @@ export class WindowFormElement extends LitElement { * @param newHeight 新的高度 * @private */ - private applyResizeBounds(newX: number, newY: number, newWidth: number, newHeight: number): { - left: number; - top: number; - width: number; - height: number; + private applyResizeBounds( + newX: number, + newY: number, + newWidth: number, + newHeight: number, + ): { + left: number + top: number + width: number + height: number } { // 最小/最大宽高限制 - if (this.minWidth != null) newWidth = Math.max(this.minWidth, newWidth); - if (this.maxWidth != null) newWidth = Math.min(this.maxWidth, newWidth); - if (this.minHeight != null) newHeight = Math.max(this.minHeight, newHeight); - if (this.maxHeight != null) newHeight = Math.min(this.maxHeight, newHeight); + if (this.minWidth != null) newWidth = Math.max(this.minWidth, newWidth) + if (this.maxWidth != null) newWidth = Math.min(this.maxWidth, newWidth) + if (this.minHeight != null) newHeight = Math.max(this.minHeight, newHeight) + if (this.maxHeight != null) newHeight = Math.min(this.maxHeight, newHeight) // 边界限制 if (this.allowOverflow) { - this.x = newX; - this.y = newY; - this.width = newWidth; - this.height = newHeight; - this.style.width = `${newWidth}px`; - this.style.height = `${newHeight}px`; - this.applyPosition(newX, newY, false); + this.x = newX + this.y = newY + this.width = newWidth + this.height = newHeight + this.style.width = `${newWidth}px` + this.style.height = `${newHeight}px` + this.applyPosition(newX, newY, false) return { left: newX, @@ -399,17 +505,17 @@ export class WindowFormElement extends LitElement { } } - const containerRect = (this.dragContainer || document.body).getBoundingClientRect(); - newX = Math.min(Math.max(0, newX), containerRect.width - newWidth); - newY = Math.min(Math.max(0, newY), containerRect.height - newHeight); + const containerRect = (this.dragContainer || document.body).getBoundingClientRect() + newX = Math.min(Math.max(0, newX), containerRect.width - newWidth) + newY = Math.min(Math.max(0, newY), containerRect.height - newHeight) - this.x = newX; - this.y = newY; - this.width = newWidth; - this.height = newHeight; - this.style.width = `${newWidth}px`; - this.style.height = `${newHeight}px`; - this.applyPosition(newX, newY, false); + this.x = newX + this.y = newY + this.width = newWidth + this.height = newHeight + this.style.width = `${newWidth}px` + this.style.height = `${newHeight}px` + this.applyPosition(newX, newY, false) return { left: newX, @@ -420,88 +526,167 @@ export class WindowFormElement extends LitElement { } private resizeUp(e: PointerEvent) { - if (!this.resizable) return; + if (!this.resizable) return - this.updateTargetBounds(this.x, this.y, this.width, this.height); - this.dispatchEvent(new CustomEvent('windowForm:resizeEnd', { - detail: { dir: this.resizeDir, width: this.width, height: this.height, left: this.x, top: this.y }, - bubbles: true, - composed: true - })); + this.updateTargetBounds(this.x, this.y, this.width, this.height) + this.dispatchEvent( + new CustomEvent('windowForm:resizeEnd', { + detail: { + dir: this.resizeDir, + width: this.width, + height: this.height, + left: this.x, + top: this.y, + }, + bubbles: true, + composed: true, + }), + ) } // ====== 窗口操作 ====== // 最小化到任务栏 private minimize() { - if (!this.taskbarElementId) return; - if (this.windowFormState === 'minimized') return; + if (!this.taskbarElementId) return + if (this.windowFormState === 'minimized') return this.targetPreMinimizeBounds = { ...this.targetBounds } - this.windowFormState = 'minimized'; + this.windowFormState = 'minimized' - const taskbar = document.querySelector(this.taskbarElementId); - if (!taskbar) throw new Error('任务栏元素未找到'); + const taskbar = document.querySelector(this.taskbarElementId) + if (!taskbar) throw new Error('任务栏元素未找到') - const rect = taskbar.getBoundingClientRect(); - const startX = this.x; - const startY = this.y; - const startW = this.offsetWidth; - const startH = this.offsetHeight; + const rect = taskbar.getBoundingClientRect() + const startX = this.x + const startY = this.y + const startW = this.offsetWidth + const startH = this.offsetHeight - this.animateWindow(startX, startY, startW, startH, rect.left, rect.top, rect.width, rect.height, 400, () => { - this.style.display = 'none'; - }); + this.animateWindow( + startX, + startY, + startW, + startH, + rect.left, + rect.top, + rect.width, + rect.height, + 400, + () => { + this.style.display = 'none' + this.dispatchEvent( + new CustomEvent('windowForm:stateChange:minimize', { + detail: { state: this.windowFormState }, + bubbles: true, + composed: true, + }), + ) + }, + ) } /** 最大化 */ private maximize() { if (this.windowFormState === 'maximized') { - this.restore(); - return; + this.restore() + return } this.targetPreMaximizedBounds = { ...this.targetBounds } - this.windowFormState = 'maximized'; + this.windowFormState = 'maximized' - const rect = this.getBoundingClientRect(); + const rect = this.getBoundingClientRect() - const startX = this.x; - const startY = this.y; - const startW = rect.width; - const startH = rect.height; + const startX = this.x + const startY = this.y + const startW = rect.width + const startH = rect.height - const targetX = 0; - const targetY = 0; - const containerRect = (this.dragContainer || document.body).getBoundingClientRect(); - const targetW = containerRect?.width ?? window.innerWidth; - const targetH = containerRect?.height ?? window.innerHeight; + const targetX = 0 + const targetY = 0 + const containerRect = (this.dragContainer || document.body).getBoundingClientRect() + const targetW = containerRect?.width ?? window.innerWidth + const targetH = containerRect?.height ?? window.innerHeight - this.animateWindow(startX, startY, startW, startH, targetX, targetY, targetW, targetH, 300); + this.animateWindow( + startX, + startY, + startW, + startH, + targetX, + targetY, + targetW, + targetH, + 300, + () => { + this.dispatchEvent( + new CustomEvent('windowForm:stateChange:maximize', { + detail: { state: this.windowFormState }, + bubbles: true, + composed: true, + }), + ) + }, + ) } /** 恢复到默认窗体状态 */ private restore(onComplete?: () => void) { - console.log(11) - if (this.windowFormState === 'default') return; - let b: IElementRect; - if ((this.windowFormState as TWindowFormState) === 'minimized' && this.targetPreMinimizeBounds) { + if (this.windowFormState === 'default') return + let b: IElementRect + if ( + (this.windowFormState as TWindowFormState) === 'minimized' && + this.targetPreMinimizeBounds + ) { // 最小化恢复,恢复到最小化前的状态 - b = this.targetPreMinimizeBounds; - } else if ((this.windowFormState as TWindowFormState) === 'maximized' && this.targetPreMaximizedBounds) { + b = this.targetPreMinimizeBounds + } else if ( + (this.windowFormState as TWindowFormState) === 'maximized' && + this.targetPreMaximizedBounds + ) { // 最大化恢复,恢复到最大化前的默认状态 - b = this.targetPreMaximizedBounds; + b = this.targetPreMaximizedBounds } else { - b = this.targetBounds; + b = this.targetBounds } - this.windowFormState = 'default'; + this.windowFormState = 'default' - this.style.display = 'block'; + this.style.display = 'block' - const startX = this.x; - const startY = this.y; - const startW = this.offsetWidth; - const startH = this.offsetHeight; + const startX = this.x + const startY = this.y + const startW = this.offsetWidth + const startH = this.offsetHeight - this.animateWindow(startX, startY, startW, startH, b.left, b.top, b.width, b.height, 300, onComplete); + this.animateWindow( + startX, + startY, + startW, + startH, + b.left, + b.top, + b.width, + b.height, + 300, + () => { + onComplete?.() + this.dispatchEvent( + new CustomEvent('windowForm:stateChange:restore', { + detail: { state: this.windowFormState }, + bubbles: true, + composed: true, + }), + ) + }, + ) + } + + private windowFormClose() { + this.dispatchEvent( + new CustomEvent('windowForm:close', { + bubbles: true, + composed: true, + }), + ) } /** @@ -528,85 +713,134 @@ export class WindowFormElement extends LitElement { targetW: number, targetH: number, duration: number, - onComplete?: () => void + onComplete?: () => void, ) { - const startTime = performance.now(); + const startTime = performance.now() const step = (now: number) => { - const elapsed = now - startTime; - const progress = Math.min(elapsed / duration, 1); - const ease = 1 - Math.pow(1 - progress, 3); + const elapsed = now - startTime + const progress = Math.min(elapsed / duration, 1) + const ease = 1 - Math.pow(1 - progress, 3) - const x = startX + (targetX - startX) * ease; - const y = startY + (targetY - startY) * ease; - const w = startW + (targetW - startW) * ease; - const h = startH + (targetH - startH) * ease; + const x = startX + (targetX - startX) * ease + const y = startY + (targetY - startY) * ease + const w = startW + (targetW - startW) * ease + const h = startH + (targetH - startH) * ease - this.style.width = `${w}px`; - this.style.height = `${h}px`; - this.applyPosition(x, y, false); + this.style.width = `${w}px` + this.style.height = `${h}px` + this.applyPosition(x, y, false) if (progress < 1) { - requestAnimationFrame(step); + requestAnimationFrame(step) } else { - this.style.width = `${targetW}px`; - this.style.height = `${targetH}px`; - this.applyPosition(targetX, targetY, true); - onComplete?.(); - this.dispatchEvent(new CustomEvent('windowForm:stateChange', { - detail: { state: this.windowFormState }, - bubbles: true, - composed: true - })); + this.style.width = `${targetW}px` + this.style.height = `${targetH}px` + this.applyPosition(targetX, targetY, true) + onComplete?.() + this.dispatchEvent( + new CustomEvent('windowForm:stateChange', { + detail: { state: this.windowFormState }, + bubbles: true, + composed: true, + }), + ) } - }; - requestAnimationFrame(step); + } + requestAnimationFrame(step) } private updateTargetBounds(left: number, top: number, width?: number, height?: number) { this.targetBounds = { - left, top, + left, + top, width: width ?? this.offsetWidth, - height: height ?? this.offsetHeight - }; + height: height ?? this.offsetHeight, + } } // ====== 渲染 ====== override render() { - if (this.minimized) { - return html` -
-
- ${this.title} -
-
- `; - } - return html` -
this.focused = true}> +
${this.title}
- ${this.minimizable ? html`` : null} - ${this.maximizable ? html`` : null} - ${this.closable ? html`` : null} + ${this.minimizable + ? html`` + : null} + ${this.maximizable + ? html`` + : null} + ${this.closable + ? html`` + : null}
- ${this.resizable ? html` -
this.startResize('t', e)}>
-
this.startResize('b', e)}>
-
this.startResize('r', e)}>
-
this.startResize('l', e)}>
-
this.startResize('tr', e)}>
-
this.startResize('tl', e)}>
-
this.startResize('br', e)}>
-
this.startResize('bl', e)}>
- ` : null} + ${this.resizable + ? html` +
this.startResize('t', e)} + >
+
this.startResize('b', e)} + >
+
this.startResize('r', e)} + >
+
this.startResize('l', e)} + >
+
this.startResize('tr', e)} + >
+
this.startResize('tl', e)} + >
+
this.startResize('br', e)} + >
+
this.startResize('bl', e)} + >
+ ` + : null}
- `; + ` } } diff --git a/src/core/window/ui/wf.scss b/src/core/window/ui/wf.scss index f6b99ca..562a23a 100644 --- a/src/core/window/ui/wf.scss +++ b/src/core/window/ui/wf.scss @@ -9,6 +9,21 @@ --shadow: 0 10px 30px rgba(0,0,0,0.25); font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial; } + +:host([focused]) { + z-index: 11; + .window { + border-color: #8338ec; + } +} + +:host([windowFormState='maximized']) { + .window { + border-radius: 0; + box-shadow: none; + } +} + .window { position: absolute; box-shadow: var(--shadow, 0 10px 30px rgba(0,0,0,0.25)); @@ -20,7 +35,12 @@ flex-direction: column; width: 100%; height: 100%; + + &.focus { + border-color: #3a86ff; + } } + .titlebar { height: var(--titlebar-height, 32px); display: flex; @@ -30,6 +50,7 @@ background: linear-gradient(#f2f2f2, #e9e9e9); border-bottom: 1px solid rgba(0,0,0,0.06); } + .title { font-size: 13px; font-weight: 600; @@ -39,7 +60,9 @@ text-overflow: ellipsis; color: #111; } + .controls { display: flex; gap: 6px; } + button.ctrl { width: 34px; height: 24px; @@ -50,7 +73,9 @@ button.ctrl { font-weight: 600; font-size: 12px; } + button.ctrl:hover { background: rgba(0,0,0,0.06); } + .content { flex: 1; overflow: auto; padding: 12px; background: transparent; } .resizer { position: absolute; z-index: 20; }