Skip to content
This repository was archived by the owner on Nov 20, 2025. It is now read-only.

Commit 909b158

Browse files
Canvas element, and optional Ids for all.
1 parent 0b00f83 commit 909b158

16 files changed

Lines changed: 398 additions & 80 deletions

File tree

tyrian-next/src/main/scala/tyrian/ui/UIElement.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ trait UIElement[Component, ComponentTheme]:
1313
def addClassNames(classes: Set[String]): Component = withClassNames(classNames ++ classes)
1414
def addClassNames(classes: String*): Component = addClassNames(classes.toSet)
1515

16+
def id: Option[String]
17+
def withId(id: String): Component
18+
1619
def themeOverride: ThemeOverride[ComponentTheme]
1720
def themeLens: Lens[Theme.Default, ComponentTheme]
1821
def withThemeOverride(value: ThemeOverride[ComponentTheme]): Component

tyrian-next/src/main/scala/tyrian/ui/elements/stateful/input/Input.scala

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package tyrian.ui.elements.stateful.input
22

33
import tyrian.Elem
44
import tyrian.EmptyAttribute
5-
import tyrian.Html.*
6-
import tyrian.Style
75
import tyrian.next.GlobalMsg
86
import tyrian.next.Outcome
97
import tyrian.ui
@@ -20,6 +18,7 @@ final case class Input(
2018
isReadOnly: Boolean,
2119
value: String,
2220
classNames: Set[String],
21+
id: Option[String],
2322
themeOverride: ThemeOverride[InputTheme]
2423
) extends UIElement.Stateful[Input, InputTheme]:
2524

@@ -49,6 +48,9 @@ final case class Input(
4948
def withClassNames(classes: Set[String]): Input =
5049
this.copy(classNames = classes)
5150

51+
def withId(id: String): Input =
52+
this.copy(id = Some(id))
53+
5254
def themeLens: Lens[Theme.Default, InputTheme] =
5355
Lens(
5456
_.input,
@@ -69,14 +71,32 @@ final case class Input(
6971
Outcome(this)
7072

7173
def view: Theme ?=> Elem[GlobalMsg] =
72-
val theme = summon[Theme]
74+
Input.toHtml(this)
75+
76+
object Input:
77+
78+
import tyrian.Html.*
79+
import tyrian.Style
80+
81+
def apply(key: UIKey): Input =
82+
Input(
83+
key,
84+
placeholder = "",
85+
isDisabled = false,
86+
isReadOnly = false,
87+
value = "",
88+
Set.empty,
89+
id = None,
90+
ThemeOverride.NoOverride
91+
)
7392

93+
def toHtml(i: Input)(using theme: Theme): tyrian.Elem[GlobalMsg] =
7494
val disabledAttr =
75-
if isDisabled then attribute("disabled", "true")
95+
if i.isDisabled then attribute("disabled", "true")
7696
else EmptyAttribute
7797

7898
val readonlyAttr =
79-
if isReadOnly then attribute("readonly", "true")
99+
if i.isReadOnly then attribute("readonly", "true")
80100
else EmptyAttribute
81101

82102
val styles =
@@ -85,39 +105,30 @@ final case class Input(
85105
Style.empty
86106

87107
case tt: Theme.Default =>
88-
if isDisabled then tt.input.toDisabledStyles(theme)
108+
if i.isDisabled then tt.input.toDisabledStyles(theme)
89109
else tt.input.toStyles(theme)
90110

91111
val classAttribute =
92-
if classNames.isEmpty then EmptyAttribute
93-
else cls := classNames.mkString(" ")
112+
if i.classNames.isEmpty then EmptyAttribute
113+
else cls := i.classNames.mkString(" ")
114+
115+
val idAttribute =
116+
i.id.fold(EmptyAttribute)(id.:=.apply)
94117

95118
val inputAttrs = List(
96-
tyrian.Html.placeholder := placeholder,
97-
tyrian.Html.value := value,
119+
tyrian.Html.placeholder := i.placeholder,
120+
tyrian.Html.value := i.value,
98121
typ := "text",
99-
onInput((s: String) => TextInputMsg.Changed(key, s)),
122+
onInput((s: String) => TextInputMsg.Changed(i.key, s)),
100123
disabledAttr,
101124
readonlyAttr,
102125
style(styles),
103-
classAttribute
126+
classAttribute,
127+
idAttribute
104128
)
105129

106130
input(inputAttrs*)
107131

108-
object Input:
109-
110-
def apply(key: UIKey): Input =
111-
Input(
112-
key,
113-
placeholder = "",
114-
isDisabled = false,
115-
isReadOnly = false,
116-
value = "",
117-
Set.empty,
118-
ThemeOverride.NoOverride
119-
)
120-
121132
enum TextInputMsg extends GlobalMsg:
122133
case Changed(id: UIKey, value: String)
123134
case Clear(id: UIKey)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package tyrian.ui.elements.stateless.canvas
2+
3+
import tyrian.EmptyAttribute
4+
import tyrian.next.GlobalMsg
5+
import tyrian.ui.UIElement
6+
import tyrian.ui.datatypes.Extent
7+
import tyrian.ui.layout.ContainerTheme
8+
import tyrian.ui.theme.Theme
9+
import tyrian.ui.theme.ThemeOverride
10+
import tyrian.ui.utils.Lens
11+
12+
final case class Canvas(
13+
width: Option[Extent],
14+
height: Option[Extent],
15+
classNames: Set[String],
16+
id: Option[String],
17+
themeOverride: ThemeOverride[ContainerTheme]
18+
) extends UIElement[Canvas, ContainerTheme]:
19+
20+
def withWidth(width: Extent): Canvas =
21+
this.copy(width = Some(width))
22+
def fillWidth: Canvas = withWidth(Extent.Fill)
23+
24+
def withHeight(height: Extent): Canvas =
25+
this.copy(height = Some(height))
26+
def fillHeight: Canvas = withHeight(Extent.Fill)
27+
28+
def withSize(width: Extent, height: Extent): Canvas =
29+
this.copy(width = Some(width), height = Some(height))
30+
def fillContainer: Canvas = withSize(Extent.Fill, Extent.Fill)
31+
32+
def withClassNames(classes: Set[String]): Canvas =
33+
this.copy(classNames = classes)
34+
35+
def withId(id: String): Canvas =
36+
this.copy(id = Some(id))
37+
38+
def themeLens: Lens[Theme.Default, ContainerTheme] =
39+
Lens(
40+
_.image,
41+
(t, c) => t.copy(canvas = c)
42+
)
43+
44+
def withThemeOverride(value: ThemeOverride[ContainerTheme]): Canvas =
45+
this.copy(themeOverride = value)
46+
47+
def view: Theme ?=> tyrian.Elem[GlobalMsg] =
48+
Canvas.toHtml(this)
49+
50+
object Canvas:
51+
52+
import tyrian.Html.*
53+
import tyrian.Style
54+
55+
def apply(): Canvas =
56+
Canvas(
57+
width = None,
58+
height = None,
59+
classNames = Set.empty,
60+
id = None,
61+
themeOverride = ThemeOverride.NoOverride
62+
)
63+
64+
def apply(width: Extent, height: Extent): Canvas =
65+
Canvas(
66+
width = Some(width),
67+
height = Some(height),
68+
classNames = Set.empty,
69+
id = None,
70+
themeOverride = ThemeOverride.NoOverride
71+
)
72+
73+
def toHtml(c: Canvas)(using theme: Theme): tyrian.Elem[GlobalMsg] =
74+
val sizeAttributes = List(
75+
c.width.map(w => width := w.toCSSValue).toList,
76+
c.height.map(h => height := h.toCSSValue).toList
77+
).flatten
78+
79+
val canvasStyles =
80+
theme match
81+
case Theme.None =>
82+
Style.empty
83+
84+
case tt: Theme.Default =>
85+
tt.canvas.toStyle
86+
87+
val styles =
88+
if canvasStyles.isEmpty then Nil else List(style(canvasStyles))
89+
90+
val classAttribute =
91+
if c.classNames.isEmpty then EmptyAttribute
92+
else cls := c.classNames.mkString(" ")
93+
94+
val idAttribute =
95+
c.id.fold(EmptyAttribute)(id.:=.apply)
96+
97+
val allAttributes =
98+
sizeAttributes ++ styles ++ List(classAttribute, idAttribute)
99+
100+
canvas(allAttributes*)()

tyrian-next/src/main/scala/tyrian/ui/elements/stateless/html/HtmlElement.scala

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,21 @@ import tyrian.ui.theme.ThemeOverride
77
import tyrian.ui.utils.Lens
88

99
final case class HtmlElement(
10-
html: tyrian.Elem[GlobalMsg],
11-
classNames: Set[String],
12-
themeOverride: ThemeOverride[Unit]
10+
html: tyrian.Elem[GlobalMsg]
1311
) extends UIElement[HtmlElement, Unit]:
1412

13+
val classNames: Set[String] = Set()
14+
val id: Option[String] = None
15+
val themeOverride: ThemeOverride[Unit] = ThemeOverride.NoOverride
16+
1517
def withHtml(html: tyrian.Elem[GlobalMsg]): HtmlElement =
1618
this.copy(html = html)
1719

1820
def withClassNames(classes: Set[String]): HtmlElement =
19-
this.copy(classNames = classes)
21+
this
22+
23+
def withId(id: String): HtmlElement =
24+
this
2025

2126
def themeLens: Lens[Theme.Default, Unit] =
2227
Lens.unit
@@ -29,29 +34,13 @@ final case class HtmlElement(
2934

3035
object HtmlElement:
3136

32-
import tyrian.Html
3337
import tyrian.Html.*
3438

35-
def apply(html: Html[GlobalMsg]): HtmlElement =
36-
HtmlElement(
37-
html = html,
38-
classNames = Set.empty,
39-
themeOverride = ThemeOverride.NoOverride
40-
)
41-
4239
def raw(htmlString: String): HtmlElement =
43-
HtmlElement(
44-
html = div().innerHtml(htmlString),
45-
classNames = Set.empty,
46-
themeOverride = ThemeOverride.NoOverride
47-
)
40+
HtmlElement(html = div().innerHtml(htmlString))
4841

4942
def text(content: String): HtmlElement =
50-
HtmlElement(
51-
html = span(content),
52-
classNames = Set.empty,
53-
themeOverride = ThemeOverride.NoOverride
54-
)
43+
HtmlElement(html = span(content))
5544

5645
def toHtml(element: HtmlElement): tyrian.Elem[GlobalMsg] =
5746
element.html

tyrian-next/src/main/scala/tyrian/ui/elements/stateless/image/Image.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ final case class Image(
1717
height: Option[Extent],
1818
fit: ImageFit,
1919
classNames: Set[String],
20+
id: Option[String],
2021
themeOverride: ThemeOverride[ContainerTheme]
2122
) extends UIElement[Image, ContainerTheme]:
2223

@@ -48,6 +49,9 @@ final case class Image(
4849
def withClassNames(classes: Set[String]): Image =
4950
this.copy(classNames = classes)
5051

52+
def withId(id: String): Image =
53+
this.copy(id = Some(id))
54+
5155
def themeLens: Lens[Theme.Default, ContainerTheme] =
5256
Lens(
5357
_.image,
@@ -73,6 +77,7 @@ object Image:
7377
height = None,
7478
fit = ImageFit.default,
7579
classNames = Set.empty,
80+
id = None,
7681
themeOverride = ThemeOverride.NoOverride
7782
)
7883

@@ -84,6 +89,7 @@ object Image:
8489
height = None,
8590
fit = ImageFit.default,
8691
classNames = Set.empty,
92+
id = None,
8793
themeOverride = ThemeOverride.NoOverride
8894
)
8995

@@ -95,6 +101,7 @@ object Image:
95101
height = Some(height),
96102
fit = ImageFit.default,
97103
classNames = Set.empty,
104+
id = None,
98105
themeOverride = ThemeOverride.NoOverride
99106
)
100107

@@ -124,6 +131,9 @@ object Image:
124131
if image.classNames.isEmpty then EmptyAttribute
125132
else cls := image.classNames.mkString(" ")
126133

127-
val allAttributes = baseAttributes ++ sizeAttributes ++ List(style(styles), classAttribute)
134+
val idAttribute =
135+
image.id.fold(EmptyAttribute)(id.:=.apply)
136+
137+
val allAttributes = baseAttributes ++ sizeAttributes ++ List(style(styles), classAttribute, idAttribute)
128138

129139
img(allAttributes*)

tyrian-next/src/main/scala/tyrian/ui/elements/stateless/link/Link.scala

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ final case class Link(
1313
url: Option[String],
1414
click: Option[GlobalMsg],
1515
classNames: Set[String],
16+
id: Option[String],
1617
themeOverride: ThemeOverride[LinkTheme]
1718
) extends UIElement[Link, LinkTheme]:
1819

@@ -37,6 +38,9 @@ final case class Link(
3738
def withClassNames(classes: Set[String]): Link =
3839
this.copy(classNames = classes)
3940

41+
def withId(id: String): Link =
42+
this.copy(id = Some(id))
43+
4044
def themeLens: Lens[Theme.Default, LinkTheme] =
4145
Lens(
4246
_.link,
@@ -58,6 +62,7 @@ object Link:
5862
url = Some(url),
5963
click = None,
6064
classNames = Set(),
65+
id = None,
6166
themeOverride = ThemeOverride.NoOverride
6267
)
6368

@@ -68,6 +73,7 @@ object Link:
6873
url = None,
6974
click = Some(onClick),
7075
classNames = Set(),
76+
id = None,
7177
themeOverride = ThemeOverride.NoOverride
7278
)
7379

@@ -83,6 +89,9 @@ object Link:
8389
if link.classNames.isEmpty then EmptyAttribute
8490
else cls := link.classNames.mkString(" ")
8591

92+
val idAttribute =
93+
link.id.fold(EmptyAttribute)(id.:=.apply)
94+
8695
val styleAttribute =
8796
theme match
8897
case Theme.None =>
@@ -97,7 +106,8 @@ object Link:
97106
link.target.map(_.toAttribute).getOrElse(EmptyAttribute),
98107
link.click.map(msg => onClick(msg)).getOrElse(EmptyAttribute),
99108
styleAttribute,
100-
classAttribute
109+
classAttribute,
110+
idAttribute
101111
)
102112

103113
a(attributes)(

0 commit comments

Comments
 (0)