Skip to content

Commit

Permalink
feat: Add left/right legend types and other positional aliases
Browse files Browse the repository at this point in the history
Co-authored-by: Cem Aksoylar <[email protected]>
  • Loading branch information
magicDGS and caksoylar committed Nov 26, 2024
1 parent d3331b0 commit b701cef
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 33 deletions.
4 changes: 2 additions & 2 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,15 @@ _Default:_ `true`

#### `small_pad`

Padding from edge of a key representation to top ("shifted") and bottom ("hold") legends.
Padding from edge of a key representation to top ("shifted"), bottom ("hold"), "left" and "right" legends.

_Type:_ `float`

_Default:_ `2`

#### `legend_rel_x`, `legend_rel_y`

Position of center ("tap") key legend relative to the center of the key.
Position of center ("tap") and "left"/"right" key legends relative to the center of the key.
Can be useful to tweak when `draw_key_sides` is used.

_Type:_ `float`
Expand Down
20 changes: 11 additions & 9 deletions KEYMAP_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,20 +151,22 @@ x x x x x x x x x
This field is an ordered mapping of layer names to a list of `LayoutKey` specs that represent the keys on that layer.
A `LayoutKey` can be defined with either a string value or with a mapping with the following fields:

| field name (alias) | type | default value | description |
| ------------------ | ----- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tap (t)` | `str` | `""` | the tap action of a key, drawn on the center of the key; spaces will be converted to line breaks[^3] |
| `hold (h)` | `str` | `""` | the hold action of a key, drawn on the bottom of the key |
| `shifted (s)` | `str` | `""` | the "shifted" action of a key, drawn on the top of the key |
| `type` | `str` | `""` | the styling of the key that corresponds to the [SVG class](CONFIGURATION.md#svg_style)[^4]. predefined types are `held` (a red shading to denote held down keys), `ghost` (dashed outline to denote optional keys in a layout), `trans` (lighter text for transparent keys) |
| field name (aliases) | type | default value | description |
| ------------------ | ----- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tap (t, center)` | `str` | `""` | the tap action of a key, drawn on the center of the key; spaces will be converted to line breaks[^3] |
| `hold (h, bottom)` | `str` | `""` | the hold action of a key, drawn on the bottom of the key |
| `shifted (s, top)` | `str` | `""` | the "shifted" action of a key, drawn on the top of the key |
| `left` | `str` | `""` | left legend, drawn on the left-center of the key |
| `right` | `str` | `""` | right legend, drawn on the right-center of the key |
| `type` | `str` | `""` | the styling of the key that corresponds to the [SVG class](CONFIGURATION.md#svg_style)[^4]. predefined types are `held` (a red shading to denote held down keys), `ghost` (dashed outline to denote optional keys in a layout), `trans` (lighter text for transparent keys) |

[^3]: You can prevent line breaks by using double spaces `" "` to denote a single non-breaking space.
[^4]: Text styling can be overridden in the `svg_extra_style` field under `draw_config` using the `"tap"`, `"hold"` and `"shifted"` CSS classes if desired.
[^4]: Text styling can be overridden in the `svg_extra_style` field under `draw_config` using the `"tap"`, `"hold"`, `"shifted"`, `"left"` and `"right"` CSS classes if desired.

Using a string value such as `"A"` for a key spec is equivalent to defining a mapping with only the tap field, i.e., `{tap: "A"}`.
It is meant to be used as a shortcut for keys that do not need `hold` or `type` fields.
It is meant to be used as a shortcut for keys that do not need any other fields.

You can use the special `$$..$$` syntax to refer to custom SVG glyphs in `tap`/`hold`/`shifted` fields, however note that they cannot be used with other text or glyphs inside the same field value.
You can use the special `$$..$$` syntax to refer to custom SVG glyphs in `tap`/`hold`/`shifted`/`left`/`right` fields, however note that they cannot be used with other text or glyphs inside the same field value.
See the [custom glyphs section](README.md#custom-glyphs) for more information.

`layers` field also flattens any lists that are contained in its value: This allows you to semantically divide keys to "rows," if you prefer to do so.
Expand Down
14 changes: 11 additions & 3 deletions keymap_drawer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ class KeySidePars(BaseModel):
paint-order: stroke;
}
/* styling for combo tap, and key hold/shifted label text */
text.combo, text.hold, text.shifted {
/* styling for combo tap, and key non-tap label text */
text.combo, text.hold, text.shifted, text.left, text.right {
font-size: 11px;
}
Expand All @@ -174,12 +174,20 @@ class KeySidePars(BaseModel):
dominant-baseline: hanging;
}
text.left {
text-anchor: start;
}
text.right {
text-anchor: end;
}
text.layer-activator {
text-decoration: underline;
}
/* styling for hold/shifted label text in combo box */
text.combo.hold, text.combo.shifted {
text.combo.hold, text.combo.shifted, text.combo.left, text.combo.right {
font-size: 8px;
}
Expand Down
12 changes: 12 additions & 0 deletions keymap_drawer/draw/combo.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ def print_combo(self, combo: ComboSpec, combo_ind: int) -> tuple[Point, Point]:
classes=["combo", combo.type, combo.key.type],
legend_type="shifted",
)
self._draw_legend(
p - Point(self.cfg.combo_w / 2 - self.cfg.small_pad, 0),
[combo.key.left],
classes=["combo", combo.type, combo.key.type],
legend_type="left",
)
self._draw_legend(
p + Point(self.cfg.combo_w / 2 - self.cfg.small_pad, 0),
[combo.key.right],
classes=["combo", combo.type, combo.key.type],
legend_type="right",
)
if combo.rotation != 0.0:
self.out.write("</g>\n")

Expand Down
12 changes: 12 additions & 0 deletions keymap_drawer/draw/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ def print_key(self, p_key: PhysicalKey, l_key: LayoutKey, key_ind: int) -> None:
classes=["key", l_key.type],
legend_type="shifted",
)
self._draw_legend(
tap_shift - Point(w / 2 - self.cfg.inner_pad_w - self.cfg.small_pad, 0),
[l_key.left],
classes=["key", l_key.type],
legend_type="left",
)
self._draw_legend(
tap_shift + Point(w / 2 - self.cfg.inner_pad_w - self.cfg.small_pad, 0),
[l_key.right],
classes=["key", l_key.type],
legend_type="right",
)

self.out.write("</g>\n")

Expand Down
33 changes: 25 additions & 8 deletions keymap_drawer/draw/glyph.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ def init_glyphs(self) -> None:
"""Preprocess all glyphs in the keymap to get their name to SVG mapping."""

def find_key_glyph_names(key: LayoutKey) -> set[str]:
return {glyph for field in (key.tap, key.hold, key.shifted) if (glyph := self._legend_to_name(field))}
return {
glyph
for field in (key.tap, key.hold, key.shifted, key.left, key.right)
if (glyph := self._legend_to_name(field))
}

# find all named glyphs in the keymap
names = set()
Expand Down Expand Up @@ -119,30 +123,43 @@ def get_glyph_defs(self) -> str:
defs += "</defs>/* end glyphs */\n"
return defs

def get_glyph_dimensions(self, name: str, legend_type: str) -> tuple[float, float, float]:
def get_glyph_dimensions(self, name: str, legend_type: str) -> tuple[float, float, float, float]:
"""Given a glyph name, calculate and return its width, height and y-offset for drawing."""
view_box = self._view_box_dimensions_re.match(self.name_to_svg[name])
assert view_box is not None
_, _, w, h = (float(v) for v in view_box.groups())

# set height and y-offset from center
# set dimensions and offsets from center
match legend_type:
case "tap":
height = self.cfg.glyph_tap_size
width = w * height / h
d_x = 0.5 * width
d_y = 0.5 * height
case "hold":
height = self.cfg.glyph_hold_size
width = w * height / h
d_x = 0.5 * width
d_y = height
case "shifted":
height = self.cfg.glyph_shifted_size
width = w * height / h
d_x = 0.5 * width
d_y = 0
case "left":
height = self.cfg.glyph_shifted_size
width = w * height / h
d_x = 0
d_y = 0.5 * height
case "right":
height = self.cfg.glyph_shifted_size
width = w * height / h
d_x = width
d_y = 0.5 * height
case _:
raise ValueError("Unsupported legend_type for glyph")

# calculate final width to preserve aspect ratio
_, _, w, h = (float(v) for v in view_box.groups())
width = w * height / h

return width, height, d_y
return width, height, d_x, d_y


@lru_cache(maxsize=128)
Expand Down
6 changes: 3 additions & 3 deletions keymap_drawer/draw/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from keymap_drawer.draw.glyph import GlyphMixin
from keymap_drawer.physical_layout import Point

LegendType = Literal["tap", "hold", "shifted"]
LegendType = Literal["tap", "hold", "shifted", "left", "right"]


class UtilsMixin(GlyphMixin):
Expand Down Expand Up @@ -109,11 +109,11 @@ def _draw_textblock(self, p: Point, words: Sequence[str], classes: Sequence[str]
self.out.write("\n</text>\n")

def _draw_glyph(self, p: Point, name: str, legend_type: LegendType, classes: Sequence[str]) -> None:
width, height, d_y = self.get_glyph_dimensions(name, legend_type)
width, height, d_x, d_y = self.get_glyph_dimensions(name, legend_type)

classes = [*classes, "glyph", name]
self.out.write(
f'<use href="#{name}" xlink:href="#{name}" x="{round(p.x - (width / 2))}" y="{round(p.y - d_y)}" '
f'<use href="#{name}" xlink:href="#{name}" x="{round(p.x - d_x)}" y="{round(p.y - d_y)}" '
f'height="{height}" width="{width}"{self._to_class_str(classes)}/>\n'
)

Expand Down
34 changes: 26 additions & 8 deletions keymap_drawer/keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from itertools import chain
from typing import Callable, Iterable, Literal

from pydantic import BaseModel, Field, field_validator, model_serializer, model_validator
from pydantic import AliasChoices, BaseModel, Field, field_validator, model_serializer, model_validator

from keymap_drawer.config import Config
from keymap_drawer.physical_layout import PhysicalLayout, layout_factory
Expand All @@ -17,12 +17,15 @@
class LayoutKey(BaseModel, populate_by_name=True, coerce_numbers_to_str=True, extra="forbid"):
"""
Represents a binding in the keymap, which has a tap property by default and
can optionally have hold or shifted properties, or be "held" or be a "ghost" key.
can optionally have hold or shifted properties, left or right labels, or be "held" or be a "ghost" key.
"""

tap: str = Field(alias="t", default="")
hold: str = Field(alias="h", default="")
shifted: str = Field(alias="s", default="")
tap: str = Field(validation_alias=AliasChoices("center", "t"), serialization_alias="t", default="")
hold: str = Field(validation_alias=AliasChoices("bottom", "h"), serialization_alias="h", default="")
shifted: str = Field(validation_alias=AliasChoices("shifted", "s"), serialization_alias="s", default="")
left: str = ""
right: str = ""

type: str = "" # pre-defined types: "held" | "ghost"

@classmethod
Expand All @@ -42,13 +45,24 @@ def from_key_spec(cls, key_spec: dict | str | int | None) -> "LayoutKey":
@model_serializer
def serialize_model(self) -> str | dict[str, str]:
"""Custom serializer to output string-only for simple legends."""
if self.hold or self.shifted or self.type:
return {k: v for k, v in (("t", self.tap), ("h", self.hold), ("s", self.shifted), ("type", self.type)) if v}
if self.hold or self.shifted or self.left or self.right or self.type:
return {
k: v
for k, v in (
("t", self.tap),
("h", self.hold),
("s", self.shifted),
("left", self.left),
("right", self.right),
("type", self.type),
)
if v
}
return self.tap

def full_serializer(self) -> dict[str, str]:
"""Custom serializer that always outputs a dict."""
return {k: v for k in ("tap", "hold", "shifted", "type") if (v := getattr(self, k))}
return {k: v for k in ("tap", "hold", "shifted", "left", "right", "type") if (v := getattr(self, k))}

def apply_formatter(self, formatter: Callable[[str], str]) -> None:
"""Add a formatter function (str -> str) to all non-empty fields."""
Expand All @@ -58,6 +72,10 @@ def apply_formatter(self, formatter: Callable[[str], str]) -> None:
self.hold = formatter(self.hold)
if self.shifted:
self.shifted = formatter(self.shifted)
if self.left:
self.left = formatter(self.left)
if self.right:
self.right = formatter(self.right)


class ComboSpec(BaseModel, populate_by_name=True, extra="forbid"):
Expand Down

0 comments on commit b701cef

Please sign in to comment.