Skip to content

Commit c96cbf4

Browse files
authored
Merge pull request #81 from atom-community/dom-styles-reader
feat: add StyleReader to commons-ui
2 parents 874a898 + a471bac commit c96cbf4

4 files changed

Lines changed: 296 additions & 0 deletions

File tree

spec/dom-style-reader-spec.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"use babel"
2+
import { StyleReader } from "../commons-ui/dom-style-reader"
3+
4+
const styles = `
5+
atom-text-editor {
6+
position: relative;
7+
}
8+
9+
atom-text-editor-minimap[stand-alone] {
10+
width: 100px;
11+
height: 100px;
12+
}
13+
14+
atom-text-editor {
15+
line-height: 17px;
16+
}
17+
18+
atom-text-editor atom-text-editor-minimap {
19+
background: rgba(255,0,0,0.3);
20+
}
21+
22+
atom-text-editor atom-text-editor-minimap .minimap-scroll-indicator {
23+
background: rgba(0,0,255,0.3);
24+
}
25+
26+
atom-text-editor atom-text-editor-minimap .minimap-visible-area {
27+
background: rgba(0,255,0,0.3);
28+
opacity: 1;
29+
}
30+
31+
atom-text-editor atom-text-editor-minimap .open-minimap-quick-settings {
32+
opacity: 1 !important;
33+
}
34+
`
35+
36+
describe("StyleReader", () => {
37+
const styleReader = new StyleReader()
38+
39+
let body: HTMLElement
40+
let targetElement: HTMLElement
41+
42+
beforeEach(async () => {
43+
body = atom.workspace.getElement()
44+
jasmine.attachToDOM(body)
45+
targetElement = (await atom.workspace.open(__filename)).getElement()
46+
47+
const styleNode = document.createElement("style")
48+
styleNode.textContent = styles
49+
body.appendChild(styleNode)
50+
})
51+
52+
it("can get the color of the text", () => {
53+
expect(styleReader.retrieveStyleFromDom([".editor"], "color", targetElement, true)).toEqual(`rgb(157, 165, 180)`)
54+
})
55+
56+
describe("color rotation", () => {
57+
let additionnalStyleNode
58+
59+
function setup(color = "read") {
60+
styleReader.invalidateDOMStylesCache()
61+
62+
additionnalStyleNode = document.createElement("style")
63+
additionnalStyleNode.textContent = `
64+
atom-text-editor .editor, .editor {
65+
color: ${color};
66+
-webkit-filter: hue-rotate(180deg);
67+
}
68+
`
69+
70+
body.appendChild(additionnalStyleNode)
71+
}
72+
73+
it("when a hue-rotate filter is applied to a rgb color computes the new color by applying the hue rotation", () => {
74+
setup("red")
75+
expect(styleReader.retrieveStyleFromDom([".editor"], "color", targetElement, true)).toEqual(`rgb(0, 109, 109)`)
76+
})
77+
78+
it("computes the new color by applying the hue rotation", () => {
79+
setup("rgba(255, 0, 0, 0)")
80+
expect(styleReader.retrieveStyleFromDom([".editor"], "color", targetElement, true)).toEqual(
81+
`rgba(0, 109, 109, 0)`
82+
)
83+
})
84+
})
85+
})

spec/utils.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"use babel"
2+
3+
export function sleep(time: number) {
4+
return new Promise((resolve) => {
5+
setTimeout(resolve, time)
6+
})
7+
}

src-commons-ui/dom-style-reader.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"use strict"
2+
3+
/**
4+
* This class is used to read the styles informations (e.g. color and background-color) from the DOM to use when
5+
* rendering canvas. This is used in Minimap and Terminal It attaches a dummyNode to the targetNode, renders them, and
6+
* finds the computed style back.
7+
*/
8+
export class StyleReader {
9+
/** The cache object */
10+
private domStylesCache = new Map<string, Record<string, string | undefined>>()
11+
12+
private dummyNode?: HTMLElement
13+
14+
/** Used to check if the dummyNode is on the current targetNode */
15+
private targetNode?: HTMLElement
16+
17+
/** Set to true once tokenized */
18+
// private hasTokenizedOnce = false
19+
20+
/**
21+
* Returns the computed values for the given property and scope in the DOM.
22+
*
23+
* This function insert a dummy element in the DOM to compute its style, return the specified property, and clear the
24+
* content of the dummy element.
25+
*
26+
* @param scopes A list of classes reprensenting the scope to build
27+
* @param property The name of the style property to compute
28+
* @param targetNode
29+
* @param getFromCache Whether to cache the computed value or not
30+
* @returns The computed property's value used in CanvasDrawer
31+
*/
32+
retrieveStyleFromDom(
33+
scopes: string[],
34+
property: string,
35+
targetNode: HTMLElement,
36+
getFromCache: boolean = true
37+
): string {
38+
if (scopes.length === 0) {
39+
return ""
40+
} // no scopes
41+
const key = scopes.join(" ")
42+
let cachedData = this.domStylesCache.get(key)
43+
44+
if (cachedData !== undefined) {
45+
if (getFromCache) {
46+
// if should get the value from the cache
47+
const value = cachedData[property]
48+
if (value !== undefined) {
49+
// value exists
50+
return value
51+
} // value not in the cache - get fresh value
52+
} // don't use cache - get fresh value
53+
} else {
54+
// key did not exist. create it
55+
cachedData = {}
56+
}
57+
58+
this.ensureDummyNodeExistence(targetNode)
59+
const dummyNode = this.dummyNode as HTMLElement
60+
61+
let parent = dummyNode
62+
for (let i = 0, len = scopes.length; i < len; i++) {
63+
const scope = scopes[i]
64+
const node = document.createElement("span")
65+
node.className = scope.replace(dotRegexp, " ") // TODO why replace is needed?
66+
parent.appendChild(node)
67+
parent = node
68+
}
69+
70+
const style = window.getComputedStyle(parent)
71+
let value = style.getPropertyValue(property)
72+
73+
// rotate hue if webkit-filter available
74+
const filter = style.getPropertyValue("-webkit-filter")
75+
if (filter.includes("hue-rotate")) {
76+
value = rotateHue(value, filter)
77+
}
78+
79+
if (value !== "") {
80+
cachedData[property] = value
81+
this.domStylesCache.set(key, cachedData)
82+
}
83+
84+
dummyNode.innerHTML = ""
85+
return value
86+
}
87+
88+
/**
89+
* Creates a DOM node container for all the operations that need to read styles properties from DOM.
90+
*
91+
* @param targetNode
92+
*/
93+
private ensureDummyNodeExistence(targetNode: HTMLElement) {
94+
if (this.targetNode !== targetNode || this.dummyNode === undefined) {
95+
this.dummyNode = document.createElement("span")
96+
this.dummyNode.style.visibility = "hidden"
97+
98+
// attach to the target node
99+
targetNode.appendChild(this.dummyNode)
100+
this.targetNode = targetNode
101+
}
102+
}
103+
104+
/** Invalidates the cache by emptying the cache object. used in MinimapElement */
105+
invalidateDOMStylesCache() {
106+
this.domStylesCache.clear()
107+
}
108+
109+
/** Invalidates the cache only for the first tokenization event. */
110+
/*
111+
private invalidateIfFirstTokenization () {
112+
if (this.hasTokenizedOnce) {
113+
return
114+
}
115+
this.invalidateDOMStylesCache()
116+
this.hasTokenizedOnce = true
117+
}
118+
*/
119+
}
120+
121+
// ## ## ######## ## ######## ######## ######## ######
122+
// ## ## ## ## ## ## ## ## ## ## ##
123+
// ## ## ## ## ## ## ## ## ## ##
124+
// ######### ###### ## ######## ###### ######## ######
125+
// ## ## ## ## ## ## ## ## ##
126+
// ## ## ## ## ## ## ## ## ## ##
127+
// ## ## ######## ######## ## ######## ## ## ######
128+
129+
const dotRegexp = /\.+/g
130+
const rgbExtractRegexp = /rgb(a?)\((\d+), (\d+), (\d+)(, (\d+(\.\d+)?))?\)/
131+
const hueRegexp = /hue-rotate\((-?\d+)deg\)/
132+
133+
/**
134+
* Computes the output color of `value` with a rotated hue defined in `filter`.
135+
*
136+
* @param value The CSS color to apply the rotation on
137+
* @param filter The CSS hue rotate filter declaration
138+
* @returns The rotated CSS color
139+
*/
140+
function rotateHue(value: string, filter: string): string {
141+
const match = value.match(rgbExtractRegexp)
142+
if (match === null) {
143+
return ""
144+
}
145+
const [, , rStr, gStr, bStr, , aStr] = match
146+
147+
const hueMatch = filter.match(hueRegexp)
148+
if (hueMatch === null) {
149+
return ""
150+
}
151+
152+
const [, hueStr] = hueMatch
153+
154+
let [r, g, b, a, hue] = [rStr, gStr, bStr, aStr, hueStr].map(Number)
155+
;[r, g, b] = rotate(r, g, b, hue)
156+
157+
if (isNaN(a)) {
158+
return `rgb(${r}, ${g}, ${b})`
159+
} else {
160+
return `rgba(${r}, ${g}, ${b}, ${a})`
161+
}
162+
}
163+
164+
/**
165+
* Computes the hue rotation on the provided `r`, `g` and `b` channels by the amount of `angle`.
166+
*
167+
* @param r The red channel of the color to rotate
168+
* @param g The green channel of the color to rotate
169+
* @param b The blue channel of the color to rotate
170+
* @param angle The angle to rotate the hue with
171+
* @returns The rotated color channels
172+
*/
173+
function rotate(r: number, g: number, b: number, angle: number): number[] {
174+
const matrix = [1, 0, 0, 0, 1, 0, 0, 0, 1]
175+
const lumR = 0.2126
176+
const lumG = 0.7152
177+
const lumB = 0.0722
178+
const hueRotateR = 0.143
179+
const hueRotateG = 0.14
180+
const hueRotateB = 0.283
181+
const cos = Math.cos((angle * Math.PI) / 180)
182+
const sin = Math.sin((angle * Math.PI) / 180)
183+
184+
matrix[0] = lumR + (1 - lumR) * cos - lumR * sin
185+
matrix[1] = lumG - lumG * cos - lumG * sin
186+
matrix[2] = lumB - lumB * cos + (1 - lumB) * sin
187+
matrix[3] = lumR - lumR * cos + hueRotateR * sin
188+
matrix[4] = lumG + (1 - lumG) * cos + hueRotateG * sin
189+
matrix[5] = lumB - lumB * cos - hueRotateB * sin
190+
matrix[6] = lumR - lumR * cos - (1 - lumR) * sin
191+
matrix[7] = lumG - lumG * cos + lumG * sin
192+
matrix[8] = lumB + (1 - lumB) * cos + lumB * sin
193+
194+
return [
195+
clamp(matrix[0] * r + matrix[1] * g + matrix[2] * b),
196+
clamp(matrix[3] * r + matrix[4] * g + matrix[5] * b),
197+
clamp(matrix[6] * r + matrix[7] * g + matrix[8] * b),
198+
]
199+
}
200+
201+
function clamp(num: number) {
202+
return Math.ceil(Math.max(0, Math.min(255, num)))
203+
}

src-commons-ui/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./float-pane/ViewContainer"
33
export * from "./float-pane/selectable-overlay"
44
export * from "./MarkdownRenderer"
55
export * from "./scrollIntoView"
6+
export * from "./dom-style-reader"

0 commit comments

Comments
 (0)