All Downloads are FREE. Search and download functionalities are using the official Maven repository.

lucuma.ui.enums.Theme.scala Maven / Gradle / Ivy

There is a newer version: 0.123.0
Show newest version
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package lucuma.ui.enums

import cats.syntax.all.*
import japgolly.scalajs.react.ReactCats.*
import japgolly.scalajs.react.Reusability
import japgolly.scalajs.react.util.DefaultEffects.Sync as DefaultS
import lucuma.core.util.Display
import lucuma.core.util.Enumerated
import lucuma.react.common.style.Css
import mouse.boolean.given
import org.scalajs.dom

private def bodyClassList = dom.document.body.classList

private val LightThemeClass  = Css("light-theme")
private val DarkThemeClass   = Css("dark-theme")
private val SystemThemeClass = Css("system-theme")

enum Theme(private val tag: String, val name: String) derives Enumerated:
  case Light  extends Theme("light", "Light")
  case Dark   extends Theme("dark", "Dark")
  case System extends Theme("system", "System")

  lazy val mount: DefaultS[Unit] = this match
    case Light  => Theme.mountLight(false)
    case Dark   => Theme.mountDark(false)
    case System =>
      Theme.preferredLight.flatMap(_.fold(Theme.mountLight(true), Theme.mountDark(true)))

object Theme:
  private lazy val preferredLightQuery: DefaultS[dom.MediaQueryList] =
    DefaultS.delay:
      dom.window.matchMedia("(prefers-color-scheme: light)")

  private lazy val preferredLight: DefaultS[Boolean] =
    preferredLightQuery.map(_.matches)

  private def adjustClasses(add: Css, remove: Css, systemPreferred: Boolean): DefaultS[Unit] =
    DefaultS.delay:
      bodyClassList.remove(SystemThemeClass.htmlClass)
      (add |+| SystemThemeClass.when_(systemPreferred)).value.foreach(bodyClassList.add)
      remove.value.foreach(bodyClassList.remove)

  private def mountLight(systemPreferred: Boolean): DefaultS[Unit] =
    adjustClasses(LightThemeClass, DarkThemeClass, systemPreferred)

  private def mountDark(systemPreferred: Boolean): DefaultS[Unit] =
    adjustClasses(DarkThemeClass, LightThemeClass, systemPreferred)

  private lazy val currentClasses: DefaultS[Option[(Boolean, Boolean)]] = // (isLight, isSystem)
    DefaultS.delay:
      val isLight = bodyClassList.contains(LightThemeClass.htmlClass)
      val isDark  = bodyClassList.contains(DarkThemeClass.htmlClass)
      if (isLight || isDark)
        (isLight, bodyClassList.contains(SystemThemeClass.htmlClass)).some
      else
        none

  private lazy val mountListener: DefaultS[Unit] =
    preferredLightQuery.flatMap: query =>
      DefaultS.delay:
        query.addEventListener(
          "change",
          (e: dom.MediaQueryList) =>
            DefaultS.runSync:
              currentClasses.flatMap:
                case Some((_, isSystem)) =>
                  isSystem.fold(e.matches.fold(mountLight(true), mountDark(true)), DefaultS.empty)
                case _                   => DefaultS.empty
        )

  lazy val current: DefaultS[Option[Theme]] =
    currentClasses.map:
      _.map: (isLight, isSystem) =>
        if (isSystem) Theme.System
        else if (isLight) Theme.Light
        else Theme.Dark

  def init(default: Theme = Theme.System): DefaultS[Theme] =
    current.flatMap:
      _.fold(mountListener >> default.mount.as(default))(DefaultS.pure(_))

  given Display[Theme] = Display.byShortName(_.name)

  given Reusability[Theme] = Reusability.byEq




© 2015 - 2024 Weber Informatics LLC | Privacy Policy