From 4d43a34e4ab09f350906d7f6459a33cecf2bdb8e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cl=C3=A9ment=20Pit-Claudel?= <clement.pit-claudel@epfl.ch>
Date: Sun, 8 Dec 2024 01:18:08 +0100
Subject: [PATCH] client: Clean up TextClientAppInstance implementation

---
 .../graphics/TextClientAppInstance.scala      | 96 +++++++++----------
 1 file changed, 48 insertions(+), 48 deletions(-)

diff --git a/js/src/main/scala/cs214/webapp/client/graphics/TextClientAppInstance.scala b/js/src/main/scala/cs214/webapp/client/graphics/TextClientAppInstance.scala
index 9511474..95b889c 100644
--- a/js/src/main/scala/cs214/webapp/client/graphics/TextClientAppInstance.scala
+++ b/js/src/main/scala/cs214/webapp/client/graphics/TextClientAppInstance.scala
@@ -48,7 +48,33 @@ case class TextSegment(
     cssProperties: CSSProperties = Map(),
     htmlAttributes: Map[HTMLAttribute, String] = Map(),
     modifiers: Modifier*
-)
+):
+  def toFrag: Frag =
+    val css = cssProperties.toCSS()
+    span(
+      text,
+      conditionalModifier(HTMLAttribute.style, style, if css.isEmpty then None else Some(css)),
+      conditionalModifier(HTMLAttribute.cls, cls, None),
+      modifiers,
+      bindMouseEvents(onMouseEvent)
+    )
+
+  private def bindMouseEvents[T](handler: Option[MouseEvent => Unit]): Modifier =
+    handler match
+      case None => frag()
+      case Some(fn) => Seq(
+        onclick := { (e: dom.MouseEvent) => fn(MouseEvent.Click(MouseButton.from(e.button))) },
+        onmousedown := { (e: dom.MouseEvent) => fn(MouseEvent.MouseDown(MouseButton.from(e.button))) },
+        onmouseup := { (e: dom.MouseEvent) => fn(MouseEvent.MouseUp(MouseButton.from(e.button))) },
+        onmouseover := { (e: dom.MouseEvent) => fn(MouseEvent.HoverEnter) },
+        onmouseout := { (e: dom.MouseEvent) => fn(MouseEvent.HoverLeave) }
+      )
+
+  private def conditionalModifier(attr: HTMLAttribute, target: Attr, default: => Option[String] = None): Modifier =
+    htmlAttributes.get(attr).orElse(default) match
+      case None => frag()
+      case Some(v) => target := v
+
 
 /** Text-based UI. Detects mouse events and has an integrated text field. */
 abstract class TextClientAppInstance[Event, View](
@@ -87,38 +113,7 @@ abstract class TextClientAppInstance[Event, View](
         cls := "textapp",
         div(
           cls := "console",
-          pre(
-            style := "white-space: pre-wrap;",
-            for textSegment <- renderView(userId, view)
-            yield span(
-              textSegment.text,
-              style := textSegment.htmlAttributes.getOrElse(HTMLAttribute.style, textSegment.cssProperties.toCSS()),
-              cls := textSegment.htmlAttributes.getOrElse(HTMLAttribute.cls, ""),
-              textSegment.modifiers,
-              margin := "0",
-              height := "1em",
-              onclick := {
-                (e: dom.MouseEvent) =>
-                  if textSegment.onMouseEvent.isDefined then
-                    textSegment.onMouseEvent.get(MouseEvent.Click(MouseButton.from(e.button)))
-              },
-              onmousedown := {
-                (e: dom.MouseEvent) => 
-                  if textSegment.onMouseEvent.isDefined then
-                    textSegment.onMouseEvent.get(MouseEvent.MouseDown(MouseButton.from(e.button)))
-              },
-              onmouseup := { (e: dom.MouseEvent) =>
-                if textSegment.onMouseEvent.isDefined then
-                  textSegment.onMouseEvent.get(MouseEvent.MouseUp(MouseButton.from(e.button)))
-              },
-              onmouseover := { (_: dom.MouseEvent) => 
-                if textSegment.onMouseEvent.isDefined then
-                  textSegment.onMouseEvent.get(MouseEvent.HoverEnter) },
-              onmouseout := { (_: dom.MouseEvent) => 
-                if textSegment.onMouseEvent.isDefined then
-                  textSegment.onMouseEvent.get(MouseEvent.HoverLeave) }
-            )
-          )
+          pre(renderView(userId, view).map(_.toFrag))
         ),
         input(
           `type` := "text",
@@ -160,19 +155,24 @@ abstract class TextClientAppInstance[Event, View](
     |   border: solid 1px black;
     |   box-sizing: border-box;
     | }
-    |  
-    |  .textapp .console {
-    |      overflow: hidden;
-    |      padding: 0.5em;
-    |      font-size: 1em;
-    |      display: flex;
-    |      flex-direction: column;
-    |      justify-content: center;
-    |      align-items: start;
-    |      border: solid 1px black;
-    |  }
-    |  
-    |  .textapp .console span {
-    |      box-sizing: border-box;
-    |  }
+    |
+    | .textapp .console {
+    |     overflow: hidden;
+    |     padding: 0.5em;
+    |     font-size: 1em;
+    |     display: flex;
+    |     flex-direction: column;
+    |     justify-content: center;
+    |     align-items: start;
+    |     border: solid 1px black;
+    | }
+    |
+    | .textapp .console pre {
+    |   white-space: pre-wrap;
+    |   margin: 0;
+    | }
+    |
+    | .textapp .console span {
+    |     box-sizing: border-box;
+    | }
     """.stripMargin
-- 
GitLab