diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 98a90b3..eb0e5ee 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -117,7 +117,7 @@ _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` @@ -125,7 +125,7 @@ _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` diff --git a/KEYMAP_SPEC.md b/KEYMAP_SPEC.md index 050b532..b60d611 100644 --- a/KEYMAP_SPEC.md +++ b/KEYMAP_SPEC.md @@ -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. diff --git a/keymap_drawer/config.py b/keymap_drawer/config.py index 2ac9371..d053ff6 100644 --- a/keymap_drawer/config.py +++ b/keymap_drawer/config.py @@ -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; } @@ -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; } diff --git a/keymap_drawer/draw/combo.py b/keymap_drawer/draw/combo.py index 6d58cc0..d0033ca 100644 --- a/keymap_drawer/draw/combo.py +++ b/keymap_drawer/draw/combo.py @@ -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("\n") diff --git a/keymap_drawer/draw/draw.py b/keymap_drawer/draw/draw.py index 2d44141..5e4aab5 100644 --- a/keymap_drawer/draw/draw.py +++ b/keymap_drawer/draw/draw.py @@ -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("\n") diff --git a/keymap_drawer/draw/glyph.py b/keymap_drawer/draw/glyph.py index 0577310..f00c4b5 100644 --- a/keymap_drawer/draw/glyph.py +++ b/keymap_drawer/draw/glyph.py @@ -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() @@ -119,30 +123,43 @@ def get_glyph_defs(self) -> str: 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) diff --git a/keymap_drawer/draw/utils.py b/keymap_drawer/draw/utils.py index 071da60..26e1df1 100644 --- a/keymap_drawer/draw/utils.py +++ b/keymap_drawer/draw/utils.py @@ -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): @@ -109,11 +109,11 @@ def _draw_textblock(self, p: Point, words: Sequence[str], classes: Sequence[str] self.out.write("\n\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'\n' ) diff --git a/keymap_drawer/keymap.py b/keymap_drawer/keymap.py index 5e4b7ae..b3af722 100644 --- a/keymap_drawer/keymap.py +++ b/keymap_drawer/keymap.py @@ -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 @@ -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 @@ -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.""" @@ -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"):