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

Commit c0eeebb

Browse files
Custom classes can be applied to UIElements
1 parent cf4e89a commit c0eeebb

8 files changed

Lines changed: 99 additions & 63 deletions

File tree

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package tyrian.ui
22

3-
trait UIElement[+Msg]:
4-
type T <: UIElement[Msg]
3+
trait UIElement[T, +Msg]:
54

6-
def _modifyTheme: Option[Theme => Theme]
5+
def classNames: Set[String]
6+
def withClassNames(classes: Set[String]): T
7+
def withClassNames(classes: String*): T = withClassNames(classes.toSet)
8+
def addClassNames(classes: Set[String]): T = withClassNames(classNames ++ classes)
9+
def addClassNames(classes: String*): T = addClassNames(classes.toSet)
710

11+
def _modifyTheme: Option[Theme => Theme]
812
def modifyTheme(f: Theme => Theme): T
913

1014
def toHtml: Theme ?=> tyrian.Html[Msg]

tyrian-ui/src/main/scala/tyrian/ui/button/Button.scala

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
package tyrian.ui.button
22

3+
import tyrian.EmptyAttribute
34
import tyrian.ui.Theme
45
import tyrian.ui.UIElement
56

67
final case class Button[Msg](
78
label: String,
89
onClick: Msg,
10+
classNames: Set[String],
911
_modifyTheme: Option[Theme => Theme]
10-
) extends UIElement[Msg]:
11-
type T = Button[Msg]
12+
) extends UIElement[Button[?], Msg]:
1213

1314
def withLabel(value: String): Button[Msg] =
1415
this.copy(label = value)
1516

1617
def withOnClick(value: Msg): Button[Msg] =
1718
this.copy(onClick = value)
1819

20+
def withClassNames(classes: Set[String]): Button[Msg] =
21+
this.copy(classNames = classes)
22+
1923
def modifyTheme(f: Theme => Theme): Button[Msg] =
2024
this.copy(_modifyTheme = Some(f))
2125

@@ -32,14 +36,19 @@ object Button:
3236
import tyrian.Html.*
3337

3438
def apply[Msg](onClick: Msg): Button[Msg] =
35-
Button("", onClick, None)
39+
Button("", onClick, Set(), None)
3640

3741
def toHtml[Msg](element: Button[Msg])(using theme: Theme): Html[Msg] =
3842
val t = element._modifyTheme match
3943
case Some(f) => f(theme)
4044
case None => theme
4145

46+
val classAttribute =
47+
if element.classNames.isEmpty then EmptyAttribute
48+
else cls := element.classNames.mkString(" ")
49+
4250
button(
4351
style(t.button.toStyles(t)),
52+
classAttribute,
4453
onClick(element.onClick)
4554
)(element.label)

tyrian-ui/src/main/scala/tyrian/ui/datatypes/FlexAlignment.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ enum FlexAlignment derives CanEqual:
1515
case Stretch => "stretch"
1616

1717
object FlexAlignment:
18-
18+
1919
val default: FlexAlignment = Start

tyrian-ui/src/main/scala/tyrian/ui/layout/Column.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ import tyrian.ui.datatypes.LayoutDirection
55

66
object Column:
77

8-
def apply[Msg](children: UIElement[Msg]*): Layout[Msg] =
8+
def apply[Msg](children: UIElement[?, Msg]*): Layout[Msg] =
99
Layout(LayoutDirection.Column, children.toList)

tyrian-ui/src/main/scala/tyrian/ui/layout/Container.scala

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
package tyrian.ui.layout
22

3+
import tyrian.EmptyAttribute
34
import tyrian.ui.Theme
45
import tyrian.ui.UIElement
56
import tyrian.ui.datatypes.RGBA
67
import tyrian.ui.datatypes.Spacing
78

89
final case class Container[+Msg](
9-
child: UIElement[Msg],
10+
child: UIElement[?, Msg],
1011
padding: Spacing,
1112
margin: Spacing,
1213
backgroundColor: Option[RGBA],
1314
width: Option[String],
1415
height: Option[String],
16+
classNames: Set[String],
1517
_modifyTheme: Option[Theme => Theme]
16-
) extends UIElement[Msg]:
17-
type T = Container[Nothing]
18+
) extends UIElement[Container[?], Msg]:
1819

1920
def withPadding(padding: Spacing): Container[Msg] =
2021
this.copy(padding = padding)
@@ -35,8 +36,11 @@ final case class Container[+Msg](
3536
def fillHeight: Container[Msg] = withHeight("100%")
3637
def fill: Container[Msg] = fillWidth.fillHeight
3738

38-
def modifyTheme(f: Theme => Theme): T =
39-
this.copy(_modifyTheme = Some(f)).asInstanceOf[T]
39+
def withClassNames(classes: Set[String]): Container[Msg] =
40+
this.copy(classNames = classes)
41+
42+
def modifyTheme(f: Theme => Theme): Container[Msg] =
43+
this.copy(_modifyTheme = Some(f))
4044

4145
def toHtml: Theme ?=> tyrian.Html[Msg] =
4246
Container.toHtml(this)
@@ -47,31 +51,36 @@ object Container:
4751
import tyrian.Html.*
4852
import tyrian.Style
4953

50-
def apply[Msg](child: UIElement[Msg]): Container[Msg] =
54+
def apply[Msg](child: UIElement[?, Msg]): Container[Msg] =
5155
Container(
5256
child = child,
5357
padding = Spacing.None,
5458
margin = Spacing.None,
5559
backgroundColor = None,
5660
width = None,
5761
height = None,
62+
classNames = Set(),
5863
_modifyTheme = None
5964
)
6065

61-
def toHtml[Msg](el: Container[Msg])(using theme: Theme): Html[Msg] =
62-
val t = el._modifyTheme match
66+
def toHtml[Msg](container: Container[Msg])(using theme: Theme): Html[Msg] =
67+
val t = container._modifyTheme match
6368
case Some(f) => f(theme)
6469
case None => theme
6570

6671
val stylesList = List(
67-
Some("padding" -> el.padding.toCSSValue),
68-
Some("margin" -> el.margin.toCSSValue),
69-
el.backgroundColor.map(color => "background-color" -> color.toHexString("#")),
70-
el.width.map(w => "width" -> w),
71-
el.height.map(h => "height" -> h)
72+
Some("padding" -> container.padding.toCSSValue),
73+
Some("margin" -> container.margin.toCSSValue),
74+
container.backgroundColor.map(color => "background-color" -> color.toHexString("#")),
75+
container.width.map(w => "width" -> w),
76+
container.height.map(h => "height" -> h)
7277
).flatten
7378

79+
val classAttribute =
80+
if container.classNames.isEmpty then EmptyAttribute
81+
else cls := container.classNames.mkString(" ")
82+
7483
val baseStyles = Style(stylesList*)
75-
val childHtml = el.child.toHtml(using t)
84+
val childHtml = container.child.toHtml(using t)
7685

77-
div(style(baseStyles))(childHtml)
86+
div(style(baseStyles), classAttribute)(childHtml)

tyrian-ui/src/main/scala/tyrian/ui/layout/Layout.scala

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package tyrian.ui.layout
22

3+
import tyrian.EmptyAttribute
34
import tyrian.ui.Theme
45
import tyrian.ui.UIElement
56
import tyrian.ui.datatypes.FlexAlignment
@@ -10,14 +11,14 @@ import tyrian.ui.datatypes.Spacing
1011
/** A vertical layout container using flexbox. */
1112
final case class Layout[+Msg](
1213
direction: LayoutDirection,
13-
children: List[UIElement[Msg]],
14+
children: List[UIElement[?, Msg]],
1415
spacing: Spacing,
1516
justify: FlexAlignment,
1617
align: FlexAlignment,
1718
ratio: Ratio,
19+
classNames: Set[String],
1820
_modifyTheme: Option[Theme => Theme]
19-
) extends UIElement[Msg]:
20-
type T = Layout[Nothing]
21+
) extends UIElement[Layout[?], Msg]:
2122

2223
def withDirection(value: LayoutDirection): Layout[Msg] =
2324
this.copy(direction = value)
@@ -81,15 +82,15 @@ final case class Layout[+Msg](
8182
case LayoutDirection.Column => withJustify(FlexAlignment.End)
8283
case LayoutDirection.Row => withAlign(FlexAlignment.End)
8384

84-
// def centerX: Layout[Msg] = withAlign(FlexAlignment.Center)
85-
// def centerY: Layout[Msg] = withJustify(FlexAlignment.Center)
86-
// def center: Layout[Msg] = centerX.centerY
8785
def spaceBetween: Layout[Msg] = withJustify(FlexAlignment.SpaceBetween)
8886
def spaceAround: Layout[Msg] = withJustify(FlexAlignment.SpaceAround)
8987
def spaceEvenly: Layout[Msg] = withJustify(FlexAlignment.SpaceEvenly)
9088

91-
def modifyTheme(f: Theme => Theme): T =
92-
this.copy(_modifyTheme = Some(f)).asInstanceOf[T]
89+
def withClassNames(classes: Set[String]): Layout[Msg] =
90+
this.copy(classNames = classes)
91+
92+
def modifyTheme(f: Theme => Theme): Layout[Msg] =
93+
this.copy(_modifyTheme = Some(f))
9394

9495
def toHtml: Theme ?=> tyrian.Html[Msg] =
9596
Layout.toHtml(this)
@@ -100,27 +101,28 @@ object Layout:
100101
import tyrian.Html.*
101102
import tyrian.Style
102103

103-
def apply[Msg](children: UIElement[Msg]*): Layout[Msg] =
104+
def apply[Msg](children: UIElement[?, Msg]*): Layout[Msg] =
104105
Layout(LayoutDirection.Row, children.toList)
105106

106-
def apply[Msg](direction: LayoutDirection, children: List[UIElement[Msg]]): Layout[Msg] =
107+
def apply[Msg](direction: LayoutDirection, children: List[UIElement[?, Msg]]): Layout[Msg] =
107108
Layout(
108109
direction = direction,
109110
children = children,
110111
spacing = Spacing.None,
111112
justify = FlexAlignment.Start,
112113
align = FlexAlignment.Start,
113114
ratio = Ratio.default,
115+
classNames = Set(),
114116
_modifyTheme = None
115117
)
116118

117-
def apply[Msg](direction: LayoutDirection, children: UIElement[Msg]*): Layout[Msg] =
119+
def apply[Msg](direction: LayoutDirection, children: UIElement[?, Msg]*): Layout[Msg] =
118120
Layout(direction, children.toList)
119121

120-
def row[Msg](children: UIElement[Msg]*): Layout[Msg] =
122+
def row[Msg](children: UIElement[?, Msg]*): Layout[Msg] =
121123
Layout(LayoutDirection.Row, children.toList)
122124

123-
def column[Msg](children: UIElement[Msg]*): Layout[Msg] =
125+
def column[Msg](children: UIElement[?, Msg]*): Layout[Msg] =
124126
Layout(LayoutDirection.Column, children.toList)
125127

126128
def toHtml[Msg](layout: Layout[Msg])(using theme: Theme): Html[Msg] =
@@ -135,6 +137,10 @@ object Layout:
135137
"gap" -> layout.spacing.toCSSValue
136138
) |+| layout.direction.toStyle |+| layout.ratio.toStyle
137139

140+
val classAttribute =
141+
if layout.classNames.isEmpty then EmptyAttribute
142+
else cls := layout.classNames.mkString(" ")
143+
138144
val childrenHtml = layout.children.map(child => child.toHtml(using t))
139145

140-
div(style(baseStyles))(childrenHtml*)
146+
div(style(baseStyles), classAttribute)(childrenHtml*)

tyrian-ui/src/main/scala/tyrian/ui/layout/Row.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ import tyrian.ui.datatypes.LayoutDirection
55

66
object Row:
77

8-
def apply[Msg](children: UIElement[Msg]*): Layout[Msg] =
8+
def apply[Msg](children: UIElement[?, Msg]*): Layout[Msg] =
99
Layout(LayoutDirection.Row, children.toList)

tyrian-ui/src/main/scala/tyrian/ui/text/Text.scala

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package tyrian.ui.text
22

3+
import tyrian.EmptyAttribute
34
import tyrian.ui.Theme
45
import tyrian.ui.UIElement
56
import tyrian.ui.datatypes.FontSize
@@ -13,9 +14,9 @@ import tyrian.ui.datatypes.TextStyle
1314
final case class Text(
1415
value: String,
1516
variant: TextVariant,
17+
classNames: Set[String],
1618
_modifyTheme: Option[Theme => Theme]
17-
) extends UIElement[Nothing]:
18-
type T = Text
19+
) extends UIElement[Text, Nothing]:
1920

2021
def withValue(value: String): Text =
2122
this.copy(value = value)
@@ -50,6 +51,9 @@ final case class Text(
5051
def alignRight: Text = modifyTextTheme(_.withAlignment(TextAlignment.Right))
5152
def alignJustify: Text = modifyTextTheme(_.withAlignment(TextAlignment.Justify))
5253

54+
def withClassNames(classes: Set[String]): Text =
55+
this.copy(classNames = classes)
56+
5357
def modifyTheme(f: Theme => Theme): Text =
5458
this.copy(_modifyTheme = Some(f))
5559

@@ -97,18 +101,18 @@ object Text:
97101
import tyrian.Html.*
98102

99103
def apply(value: String): Text =
100-
Text(value, TextVariant.Normal, None)
101-
102-
def body(value: String): Text = Text(value, TextVariant.Normal, None)
103-
def heading1(value: String): Text = Text(value, TextVariant.Heading1, None)
104-
def heading2(value: String): Text = Text(value, TextVariant.Heading2, None)
105-
def heading3(value: String): Text = Text(value, TextVariant.Heading3, None)
106-
def heading4(value: String): Text = Text(value, TextVariant.Heading4, None)
107-
def heading5(value: String): Text = Text(value, TextVariant.Heading5, None)
108-
def heading6(value: String): Text = Text(value, TextVariant.Heading6, None)
109-
def caption(value: String): Text = Text(value, TextVariant.Caption, None)
110-
def code(value: String): Text = Text(value, TextVariant.Code, None)
111-
def label(value: String): Text = Text(value, TextVariant.Label, None)
104+
Text(value, TextVariant.Normal, Set(), None)
105+
106+
def body(value: String): Text = Text(value, TextVariant.Normal, Set(), None)
107+
def heading1(value: String): Text = Text(value, TextVariant.Heading1, Set(), None)
108+
def heading2(value: String): Text = Text(value, TextVariant.Heading2, Set(), None)
109+
def heading3(value: String): Text = Text(value, TextVariant.Heading3, Set(), None)
110+
def heading4(value: String): Text = Text(value, TextVariant.Heading4, Set(), None)
111+
def heading5(value: String): Text = Text(value, TextVariant.Heading5, Set(), None)
112+
def heading6(value: String): Text = Text(value, TextVariant.Heading6, Set(), None)
113+
def caption(value: String): Text = Text(value, TextVariant.Caption, Set(), None)
114+
def code(value: String): Text = Text(value, TextVariant.Code, Set(), None)
115+
def label(value: String): Text = Text(value, TextVariant.Label, Set(), None)
112116

113117
def toHtml(element: Text)(using theme: Theme): Html[Nothing] =
114118
val t = element._modifyTheme match
@@ -118,18 +122,22 @@ object Text:
118122
val textTheme = getVariantTheme(element.variant, t)
119123
val styles = textTheme.toStyles(t)
120124

125+
val classAttribute =
126+
if element.classNames.isEmpty then EmptyAttribute
127+
else cls := element.classNames.mkString(" ")
128+
121129
element.variant match
122-
case TextVariant.Normal => span(style(styles))(element.value)
123-
case TextVariant.Paragraph => p(style(styles))(element.value)
124-
case TextVariant.Heading1 => h1(style(styles))(element.value)
125-
case TextVariant.Heading2 => h2(style(styles))(element.value)
126-
case TextVariant.Heading3 => h3(style(styles))(element.value)
127-
case TextVariant.Heading4 => h4(style(styles))(element.value)
128-
case TextVariant.Heading5 => h5(style(styles))(element.value)
129-
case TextVariant.Heading6 => h6(style(styles))(element.value)
130-
case TextVariant.Caption => span(style(styles))(element.value)
131-
case TextVariant.Code => tyrian.Html.code(style(styles))(element.value)
132-
case TextVariant.Label => tyrian.Html.label(style(styles))(element.value)
130+
case TextVariant.Normal => span(style(styles), classAttribute)(element.value)
131+
case TextVariant.Paragraph => p(style(styles), classAttribute)(element.value)
132+
case TextVariant.Heading1 => h1(style(styles), classAttribute)(element.value)
133+
case TextVariant.Heading2 => h2(style(styles), classAttribute)(element.value)
134+
case TextVariant.Heading3 => h3(style(styles), classAttribute)(element.value)
135+
case TextVariant.Heading4 => h4(style(styles), classAttribute)(element.value)
136+
case TextVariant.Heading5 => h5(style(styles), classAttribute)(element.value)
137+
case TextVariant.Heading6 => h6(style(styles), classAttribute)(element.value)
138+
case TextVariant.Caption => span(style(styles), classAttribute)(element.value)
139+
case TextVariant.Code => tyrian.Html.code(style(styles), classAttribute)(element.value)
140+
case TextVariant.Label => tyrian.Html.label(style(styles), classAttribute)(element.value)
133141

134142
private def getVariantTheme(variant: TextVariant, theme: Theme): TextTheme =
135143
variant match

0 commit comments

Comments
 (0)