mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(mobile): preserve live palettes on identical themes (#31355)
fix(mobile): skip identical terminal theme assignments Preserve live TUI palette changes and contrast caches on repeated normalized host themes. Keep explicit foreground recovery repaints and published desktop contrast overrides. Co-authored-by: bbingz <zzb@gxsmjx.com>
This commit is contained in:
@@ -172,10 +172,15 @@ export type TerminalDocumentThemeTarget = {
|
||||
|
||||
export function applyTerminalTheme(
|
||||
scope: TerminalDocumentScope,
|
||||
input: TerminalDocumentThemeMessage
|
||||
input: TerminalDocumentThemeMessage,
|
||||
force = false
|
||||
) {
|
||||
scope.terminalThemeInput = input
|
||||
scope.terminalTheme = normalizeTerminalTheme(scope, input)
|
||||
const nextTheme = normalizeTerminalTheme(scope, input)
|
||||
const themeChanged = Object.keys(scope.defaultTheme).some(
|
||||
(key) => scope.terminalTheme[key] !== nextTheme[key]
|
||||
)
|
||||
scope.terminalTheme = nextTheme
|
||||
const background = scope.terminalTheme.background || TERMINAL_BACKGROUND_FALLBACK
|
||||
scope.paintDocumentBackground(background)
|
||||
// Why prefer the published value: the desktop user may have lowered or disabled the floor (#10754);
|
||||
@@ -186,7 +191,12 @@ export function applyTerminalTheme(
|
||||
scope.terminalMinimumContrastRatio =
|
||||
publishedFloor === null ? resolveTerminalContrastFloor(background) : publishedFloor
|
||||
if (scope.term) {
|
||||
scope.term.options.theme = scope.terminalTheme
|
||||
scope.term.options.minimumContrastRatio = scope.terminalMinimumContrastRatio
|
||||
// Reassigning the theme resets live OSC palette changes; visibility recovery still repaints.
|
||||
if (force || themeChanged) {
|
||||
scope.term.options.theme = scope.terminalTheme
|
||||
}
|
||||
if (scope.term.options.minimumContrastRatio !== scope.terminalMinimumContrastRatio) {
|
||||
scope.term.options.minimumContrastRatio = scope.terminalMinimumContrastRatio
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ function onDocumentVisibilityChange(scope: TerminalDocumentScope) {
|
||||
}
|
||||
// Why: iOS may restore the xterm model while discarding GPU pixels/theme
|
||||
// paint state, so visibility must rebuild the atlas and repaint every row.
|
||||
applyTerminalTheme(scope, scope.terminalThemeInput)
|
||||
applyTerminalTheme(scope, scope.terminalThemeInput, true)
|
||||
try {
|
||||
if (scope.webglAddon && scope.webglAddon.clearTextureAtlas) {
|
||||
scope.webglAddon.clearTextureAtlas()
|
||||
|
||||
@@ -7,6 +7,7 @@ import { XTERM_ENGINE_JS } from './terminal-webview-engine.generated'
|
||||
import { createTerminalDocumentScope } from './document/document-scope'
|
||||
import { attachWebglAddon, startWebglRecovery } from './document/webgl-recovery'
|
||||
import { XTERM_HTML } from './terminal-webview-html'
|
||||
import { applyTerminalTheme } from './document/terminal-theme'
|
||||
|
||||
function createWebglRecoveryHarness(failSecondAttach = false) {
|
||||
const timers: Array<() => void> = []
|
||||
@@ -209,17 +210,19 @@ describe('terminal WebView bundled engine', () => {
|
||||
|
||||
it('reapplies theme, clears the active atlas, and refreshes when visible', () => {
|
||||
const harness = createWebglRecoveryHarness()
|
||||
applyTerminalTheme(harness.scope, harness.scope.terminalThemeInput)
|
||||
const firstTheme = harness.term.options.theme
|
||||
|
||||
// Hidden: nothing is re-applied, because a repaint of an invisible terminal is wasted. happy-dom
|
||||
// reports a visible document, so the hidden arm is the stub and the visible one is the default.
|
||||
vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('hidden')
|
||||
harness.fireVisibilityChange()
|
||||
expect(harness.term.options.theme).toEqual({})
|
||||
expect(harness.term.options.theme).toBe(firstTheme)
|
||||
vi.restoreAllMocks()
|
||||
|
||||
harness.fireVisibilityChange()
|
||||
// The theme is re-applied through the document's own path, read off the terminal it wrote to.
|
||||
expect(harness.term.options.theme).not.toEqual({})
|
||||
expect(harness.term.options.theme).not.toBe(firstTheme)
|
||||
expect(harness.addons[0]?.clearTextureAtlas).toHaveBeenCalledTimes(1)
|
||||
expect(harness.term.refresh).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createTerminalDocumentScope } from './document/document-scope'
|
||||
import { applyTerminalTheme, resolveTerminalContrastFloor } from './document/terminal-theme'
|
||||
import type {
|
||||
@@ -113,3 +113,68 @@ describe('mobile terminal-webview contrast floor gate', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mobile terminal theme idempotence', () => {
|
||||
function countingThemeScope() {
|
||||
const term: TerminalDocumentThemeTarget = { options: { minimumContrastRatio: DARK_FLOOR } }
|
||||
const scope = themeScope(term)
|
||||
scope.terminalTheme = { ...scope.defaultTheme }
|
||||
term.options.theme = scope.terminalTheme
|
||||
const themeWrite = vi.fn()
|
||||
const contrastWrite = vi.fn()
|
||||
let theme = term.options.theme
|
||||
let contrast = term.options.minimumContrastRatio
|
||||
Object.defineProperty(term.options, 'theme', {
|
||||
get: () => theme,
|
||||
set: (next) => {
|
||||
themeWrite(next)
|
||||
theme = next
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
Object.defineProperty(term.options, 'minimumContrastRatio', {
|
||||
get: () => contrast,
|
||||
set: (next) => {
|
||||
contrastWrite(next)
|
||||
contrast = next
|
||||
},
|
||||
configurable: true
|
||||
})
|
||||
return { scope, term, themeWrite, contrastWrite }
|
||||
}
|
||||
|
||||
it('preserves a live palette through repeated equal normalized host themes', () => {
|
||||
const { scope, term, themeWrite, contrastWrite } = countingThemeScope()
|
||||
const livePalette = term.options.theme
|
||||
const second = { theme: { foreground: '#c0caf5', background: '#1a1b26', ignored: '#ffffff' } }
|
||||
applyTerminalTheme(scope, { theme: { background: '#1a1b26' } })
|
||||
applyTerminalTheme(scope, second)
|
||||
expect(themeWrite).not.toHaveBeenCalled()
|
||||
expect(contrastWrite).not.toHaveBeenCalled()
|
||||
expect(term.options.theme).toBe(livePalette)
|
||||
expect(scope.terminalThemeInput).toBe(second)
|
||||
})
|
||||
|
||||
it('changes palette and published contrast independently', () => {
|
||||
const { scope, themeWrite, contrastWrite } = countingThemeScope()
|
||||
applyTerminalTheme(scope, { theme: { background: '#282828' } })
|
||||
expect(themeWrite).toHaveBeenCalledTimes(1)
|
||||
expect(contrastWrite).not.toHaveBeenCalled()
|
||||
applyTerminalTheme(scope, { theme: { background: '#282828' }, minimumContrastRatio: 1 })
|
||||
expect(themeWrite).toHaveBeenCalledTimes(1)
|
||||
expect(contrastWrite).toHaveBeenCalledExactlyOnceWith(1)
|
||||
})
|
||||
|
||||
it('forces an identical palette for visibility recovery', () => {
|
||||
const { scope, themeWrite } = countingThemeScope()
|
||||
applyTerminalTheme(scope, { theme: { ...scope.defaultTheme } }, true)
|
||||
expect(themeWrite).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('retains theme and published contrast before terminal construction', () => {
|
||||
const scope = themeScope()
|
||||
applyTerminalTheme(scope, { theme: { background: '#282828' }, minimumContrastRatio: 1 })
|
||||
expect(scope.terminalTheme.background).toBe('#282828')
|
||||
expect(scope.terminalMinimumContrastRatio).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user