Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,18 @@ src/
media/
src/ one TypeScript entry point per page, shared code in src/lib
css/ one stylesheet, all colours from --vscode-* variables
icons/ container and panel icons
icons/ container and panel icons, plus vscodeos-icons.woff
```

`vscodeos-icons.woff` exists because **none of codicon's 753 icons is a power
symbol** — the nearest is `circle-slash`, a "no entry" sign — so the tray's power
button draws IEC 5009 from a one-glyph font of our own, registered under
`contributes.icons` and used as `$(vscodeos-power)`. It is generated by
`media/icons/build-font.py` to codicon's metrics (300 units per em, 19-unit
stroke) so it sits level with the `$(plug)` and `$(volume)` glyphs beside it. The
font is committed; the generator needs `fonttools` and only runs if the glyph
changes.

`src/webview/protocol.ts` is imported by both sides, so a change to a message
shape is a compile error in the webview that consumes it.

Expand Down
183 changes: 183 additions & 0 deletions extension/media/icons/build-font.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Generate vscodeos-icons.woff, the shell's own icon font.

VS Code's codicon set has no power symbol - the closest thing in all 753 names
is `circle-slash`, a "no entry" sign - so the tray's power button draws its
glyph from this font instead, registered through `contributes.icons` in
package.json. VS Code accepts woff, woff2 or ttf there.

The glyph is IEC 5009: a ring broken at the top with a vertical bar rising
through the gap. It is drawn to codicon's own metrics so it sits level with the
`$(plug)` and `$(volume)` glyphs beside it in the status bar:

units per em 300 (a 16 px icon box, so 18.75 units = 1 px)
ascender 300, descender 0
advance width 300
stroke 19 units, ~1 px, matching codicon's outlines
bounding box x 24..276, y 14..286, optically centred on (150, 150)

Arcs are approximated with quadratic Beziers in 15 degree steps, which puts the
worst-case radial error at 0.005 units - four thousandths of a pixel.

Run after changing anything above; the woff is committed, so a normal build
never needs this script or Python at all:

pip install fonttools brotli
python3 extension/media/icons/build-font.py
"""

from __future__ import annotations

import math
from pathlib import Path

from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen

UPM = 300
CENTER_X = 150.0
RING_CENTER_Y = 140.0
STROKE = 19.0
R_OUT = 126.0
R_IN = R_OUT - STROKE
R_MID = (R_OUT + R_IN) / 2
CAP = STROKE / 2

# Half-width of the gap at 12 o'clock. The bar subtends about 5 degrees at the
# inner radius, so 21 leaves a clear 1 px of daylight on either side of it.
GAP_HALF_DEG = 21.0

BAR_BOTTOM = 145.0 # centreline; the round cap reaches 9.5 further down
BAR_TOP = 276.5 # ...and 9.5 further up, to 286, clear of the ring at 266

STEP_DEG = 15.0 # maximum sweep of a single quadratic segment

# 2024-01-01, in the seconds-since-1904 that TrueType's head table counts in.
EPOCH = 1704067200 + 2082844800


def arc(pen_points: list, cx: float, cy: float, radius: float,
start_deg: float, end_deg: float) -> None:
"""Append a circular arc as (off-curve, on-curve) quadratic segments.

The control point of a segment sits where the two end tangents meet, at
radius / cos(half the sweep) - the standard quadratic arc approximation.
"""
sweep = end_deg - start_deg
steps = max(1, math.ceil(abs(sweep) / STEP_DEG))
delta = sweep / steps
half = math.radians(delta / 2)
control_radius = radius / math.cos(half)

for i in range(steps):
a0 = math.radians(start_deg + delta * i)
a1 = math.radians(start_deg + delta * (i + 1))
mid = (a0 + a1) / 2
pen_points.append(((cx + control_radius * math.cos(mid),
cy + control_radius * math.sin(mid)), False))
pen_points.append(((cx + radius * math.cos(a1),
cy + radius * math.sin(a1)), True))


def broken_ring() -> list:
"""The ring: outer edge round, a round cap, the inner edge back, a cap."""
start = 90 + GAP_HALF_DEG
end = 90 - GAP_HALF_DEG + 360

points: list = []
# Outer edge, anticlockwise from one side of the gap to the other.
points.append(((CENTER_X + R_OUT * math.cos(math.radians(start)),
RING_CENTER_Y + R_OUT * math.sin(math.radians(start))), True))
arc(points, CENTER_X, RING_CENTER_Y, R_OUT, start, end)

# Round the end: a half turn about the point on the centreline, taking the
# outline from the outer edge round to the inner one.
end_x = CENTER_X + R_MID * math.cos(math.radians(end))
end_y = RING_CENTER_Y + R_MID * math.sin(math.radians(end))
arc(points, end_x, end_y, CAP, end, end + 180)

# Inner edge, back the way we came.
arc(points, CENTER_X, RING_CENTER_Y, R_IN, end, start)

# ...and round the start the same way.
start_x = CENTER_X + R_MID * math.cos(math.radians(start))
start_y = RING_CENTER_Y + R_MID * math.sin(math.radians(start))
arc(points, start_x, start_y, CAP, start + 180, start + 360)

return points


def bar() -> list:
"""The vertical stroke: a stadium, drawn anticlockwise like the ring."""
points: list = [((CENTER_X + CAP, BAR_BOTTOM), True),
((CENTER_X + CAP, BAR_TOP), True)]
arc(points, CENTER_X, BAR_TOP, CAP, 0, 180)
points.append(((CENTER_X - CAP, BAR_BOTTOM), True))
arc(points, CENTER_X, BAR_BOTTOM, CAP, 180, 360)
return points


def draw(pen: TTGlyphPen, contour: list) -> None:
"""Feed one closed contour of (point, on_curve) pairs to a TrueType pen."""
pen.moveTo(contour[0][0])
segment: list = []
for point, on_curve in contour[1:]:
segment.append(point)
if on_curve:
if len(segment) == 1:
pen.lineTo(segment[0])
else:
pen.qCurveTo(*segment)
segment = []
if segment:
pen.qCurveTo(*segment, None)
pen.closePath()


def main() -> None:
pen = TTGlyphPen(None)
draw(pen, broken_ring())
draw(pen, bar())
power = pen.glyph()

blank = TTGlyphPen(None).glyph()

builder = FontBuilder(UPM, isTTF=True)
builder.setupGlyphOrder(['.notdef', 'power'])
builder.setupCharacterMap({0xE000: 'power'})
builder.setupGlyf({'.notdef': blank, 'power': power})

# A glyph's left side bearing has to be its own xMin: rasterisers phase the
# outline by the difference between the two, so getting this wrong slides
# the icon sideways rather than failing outright.
glyf = builder.font['glyf']
builder.setupHorizontalMetrics({
name: (UPM, getattr(glyf[name], 'xMin', 0)) for name in ('.notdef', 'power')
})
builder.setupHorizontalHeader(ascent=UPM, descent=0, lineGap=0)
builder.setupNameTable({
'familyName': 'VS Code OS Icons',
'styleName': 'Regular',
'psName': 'VSCodeOSIcons-Regular',
'version': '1.0',
'copyright': 'MIT licensed, part of VS Code OS',
})
builder.setupOS2(sTypoAscender=UPM, sTypoDescender=0, sTypoLineGap=0,
usWinAscent=UPM, usWinDescent=0, achVendID='VSOS')
# Format 2.0, so the file names its own glyph and stays inspectable.
builder.setupPost(keepGlyphNames=True)

# head stamps the build time by default, which would make every run produce
# a different binary for the same glyph. Pin it, so re-running this script
# after an unrelated edit shows up as no diff at all.
head = builder.font['head']
head.created = head.modified = EPOCH

builder.font.flavor = 'woff'
out = Path(__file__).with_name('vscodeos-icons.woff')
builder.save(out)
print(f'{out} ({out.stat().st_size} bytes)')


if __name__ == '__main__':
main()
Binary file added extension/media/icons/vscodeos-icons.woff
Binary file not shown.
11 changes: 10 additions & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@
}
},
"contributes": {
"icons": {
"vscodeos-power": {
"description": "Power (IEC 5009)",
"default": {
"fontPath": "./media/icons/vscodeos-icons.woff",
"fontCharacter": "\\E000"
}
}
},
"viewsContainers": {
"activitybar": [
{
Expand Down Expand Up @@ -70,7 +79,7 @@
"command": "vscodeos.power.menu",
"title": "Power…",
"category": "VS Code OS",
"icon": "$(circle-slash)"
"icon": "$(vscodeos-power)"
},
{
"command": "vscodeos.power.shutdown",
Expand Down
8 changes: 5 additions & 3 deletions extension/src/statusbar/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ export class StatusBar implements vscode.Disposable {
this.clock = this.create('clock', PRIORITY.clock, 'vscodeos.calendar.show', 'Date and time');
this.power = this.create('power', PRIORITY.power, 'vscodeos.power.menu', 'Power');

// No codicon is a power symbol; circle-slash is the closest that is
// guaranteed to exist in every VS Code build.
this.power.text = '$(circle-slash)';
// No codicon is a power symbol - the nearest of the 753 is circle-slash,
// which is a "no entry" sign - so this one comes from the extension's own
// icon font, registered under contributes.icons in package.json. It is
// drawn to codicon's metrics, so it sits level with its neighbours.
this.power.text = '$(vscodeos-power)';
this.power.tooltip = new vscode.MarkdownString('**Power** — sleep, restart or shut down');

this.music.on('change', (state: NowPlaying | undefined) => this.renderMedia(state));
Expand Down
31 changes: 24 additions & 7 deletions rootfs-common/etc/skel/.config/openbox/rc.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,31 @@
<dragThreshold>8</dragThreshold>
<doubleClickTime>200</doubleClickTime>
<screenEdgeWarpTime>0</screenEdgeWarpTime>
<context name="Frame">
<!-- Focus on click, and nothing else. No move, no resize, no menus. -->
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
</mousebind>
</context>
<!--
There is deliberately no <context name="Frame"> here, and binding a plain
button in one is what breaks left click in the editor.

Openbox grabs the buttons it has bindings for, and it grabs the two window
contexts differently (openbox/mouse.c, mouse_grab_for_client):

Frame -> grab on the frame window, GrabModeAsync
Client -> grab on the client window, GrabModeSync

Only the synchronous one reaches the application: openbox runs the binding
and then replays the pointer, so the click arrives in VS Code as well. The
asynchronous grab on the frame just consumes the event. Worse, the frame is
the client window's parent, and X activates the outermost of two competing
passive grabs - "a passive grab on the same button/key combination does not
exist on any ancestor of the grab-window" - so the Frame grab wins and the
Client one never fires at all.

A bare <mousebind button="Left"> under Frame therefore swallows every left
click in the editor while the right and middle buttons, bound only under
Client, carry on working. Anything bound here must be modifier-prefixed
(A-Left and friends), which is exactly what upstream's rc.xml does.
-->
<context name="Client">
<!-- Focus on click, and nothing else. No move, no resize, no menus. -->
<mousebind button="Left" action="Press">
<action name="Focus"/>
<action name="Raise"/>
Expand Down
4 changes: 4 additions & 0 deletions scripts/build-extension.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,11 @@ install -m 0644 "${SRC_DIR}"/dist/*.js "${TARGET}/dist/"
install -d -m 0755 "${TARGET}/media/dist" "${TARGET}/media/css" "${TARGET}/media/icons"
install -m 0644 "${SRC_DIR}"/media/dist/*.js "${TARGET}/media/dist/"
install -m 0644 "${SRC_DIR}"/media/css/*.css "${TARGET}/media/css/"
# The svg files are container/panel icons; the woff is the icon font behind
# $(vscodeos-power). build-font.py, which generates it, is deliberately not
# shipped - the font is committed and a build never regenerates it.
install -m 0644 "${SRC_DIR}"/media/icons/*.svg "${TARGET}/media/icons/"
install -m 0644 "${SRC_DIR}"/media/icons/*.woff "${TARGET}/media/icons/"

version="$(node -p "require('${TARGET}/package.json').version")"
msg "VsCodeOsCore ${version} staged in ${TARGET} ($(du -sh "${TARGET}" | cut -f1))"