Skip to content

Commit

Permalink
feat(map,ui): add right-click menu to the map canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
updraft0 committed Sep 24, 2024
1 parent 8b8aea9 commit 5c261df
Show file tree
Hide file tree
Showing 24 changed files with 897 additions and 178 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.8.2
version = 3.8.3
runner.dialect = scala3
align.preset = more
maxColumn = 120
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,14 @@ object MagicConstant:

// Images
val CharacterImageSize: Int = 32

// Systems
// TODO use
val Jita: SystemId = SystemId(30000142)

// UI elements
val DropdownDelayMs: Int = 500

// TODO: figure out an optimal value for this
val ConnectionCurviness: (Int, Int) = (25, 10)
val ConnectionEndRadius: Int = 4
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ object Users:
def refreshToken(refreshToken: Base64): ZIO[Env, Throwable, CharacterAuth] =
Esi
.refreshTokenAndUserData(refreshToken)
.mapError(_.asThrowable)
.mapError(_.asThrowable) // TODO: this is incorrect as we need to remove the token when it's an invalid_grant
.flatMap((jwt, tokenMeta) => refreshTokenMinimal(jwt, tokenMeta))

private def newUser(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ object Server extends ZIOAppDefault:
dbConfig,
esiClientConfig,
httpServerConfig,
locationTrackerConfig,
mapConfig,
metricsConfig,
locationTrackerConfig,
sdeClientConfig,
sdeConfig,
// ESI & SDE
Expand Down
4 changes: 3 additions & 1 deletion ui/src/main/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@import 'icon.css';

/* controls */
@import 'components/context-dropdown.css';
@import 'components/option-dropdown.css';
@import 'components/tooltip.css';

Expand All @@ -19,6 +20,7 @@
@import 'views/nav-top-view.css';
@import 'views/solar-system-info-view.css';
@import 'views/map-connection.css';
@import 'views/map-context-menu.css';
@import 'views/map-system-signature-view.css';
@import 'views/map-selection-view.css';
@import 'views/system-view.css';
Expand Down Expand Up @@ -216,7 +218,7 @@ $box-height: 40px;
.system {
font-family: Oxygen, Arial, sans-serif;
font-weight: 700;
font-size: 12px;
font-size: 0.75em;
background-color: $gray-dark;
border-radius: 5px;
/*transition: .5s linear;*/
Expand Down
76 changes: 76 additions & 0 deletions ui/src/main/css/components/context-dropdown.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
ul.context-menu {
list-style: none;
border-radius: 5px;
padding: 0.2em 0;
margin: 0.2em 0;

background-color: $gray-lighter;
color: $gray-darkest;
font-size: 0.75em;
font-weight: 600;

/* material design corners - TODO need to templatise this */
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);

& .ti {
align-content: end;
}

& li {
padding: 0 0.5em;
position: relative; /* required ofc to make height: 100% work on ::before */
/* TODO cursor not working */
cursor: pointer;

&.disabled {
color: $gray !important;
cursor: revert;
& * {
color: $gray !important;
}
}

&:hover:not(.disabled):not(.divider) {
background-color: $gray-light;
}

&:not(.divider)::before {
position: absolute;
opacity: 0;
content: '';
background-color: $green;
width: 2px;
left: -4px;
height: 100%;

transition: left 0.15s ease-out,opacity 0.15s ease-out;
}

&:hover:not(.disabled)::before {
opacity: 1;
}

&.divider {
height: 1px;
cursor: initial;
background-color: $gray-light;
margin: 0.2em 0;
}
}

& button {
display: inline-block;
background-color: transparent;
border: none;
text-align: left;
color: inherit;
font-weight: inherit;
width: 100%;
}

& i {
color: $gray-darkest;
padding-right: 0.2em;
}

}
35 changes: 35 additions & 0 deletions ui/src/main/css/views/map-context-menu.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#map-context-menu {
position: absolute;
z-index: 2000;

& ul {
min-width: 12em;

& li[data-action="remove-selected"], & li[data-action="disconnect"], & li[data-action="remove-current"] {
color: $red-darkest;
& i {
color: $red-dark;
}
}

& i.ti-circle-filled[data-mass="fresh"], & i.ti-circle-filled[data-stance="unknown"] {
color: $gray-light;
}

& i.ti-circle-filled[data-mass="reduced"] {
color: $orange;
}

& i.ti-circle-filled[data-mass="critical"] {
color: $red;
}

& i[data-stance="hostile"] {
color: $red;
}

& i[data-stance="friendly"] {
color: $blue;
}
}
}
33 changes: 33 additions & 0 deletions ui/src/main/scala/controltower/component/ContextMenu.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package controltower.component

import com.raquo.laminar.api.L.*

type Action = String

enum ContextMenuElement derives CanEqual:
case MenuItem(
action: Action,
view: Element,
disabled: Signal[Boolean] = Val(false),
mod: Modifier[Element] = emptyMod
)
case Divider

final class ContextMenu(id: String, els: ContextMenuElement*):
def view: Element = ul(
idAttr := id,
cls := "context-menu",
role := "menu",
els.map:
case cmi: ContextMenuElement.MenuItem =>
li(
cls := "menu-item",
role := "menuitem",
cls("disabled") <-- cmi.disabled,
dataAttr("action") := cmi.action,
cmi.mod,
cmi.view
)
case ContextMenuElement.Divider =>
li(cls := "divider")
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package controltower.component

import com.raquo.laminar.api.L.*
import org.updraft0.controltower.constant.MagicConstant

import scala.concurrent.duration.{Duration, given}
import scala.collection.mutable.ArrayBuffer

Expand All @@ -14,7 +16,7 @@ trait DropdownItem[E]:
class OptionDropdown[E](
options: Seq[E],
current: Var[E],
mouseLeaveDelay: Duration = 500.millis,
mouseLeaveDelay: Duration = MagicConstant.DropdownDelayMs.millis,
isDisabled: Observable[Boolean] = Val(false)
)(using D: DropdownItem[E], @scala.annotation.unused _ce: CanEqual[E, E]):

Expand Down
2 changes: 1 addition & 1 deletion ui/src/main/scala/controltower/db/reference.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class IdbReferenceDataStore(db: Database, maxSearchHits: Int = 10) extends Refer
override def searchSystemName(value: String): Future[List[SolarSystem]] =
for
trx <- solarSystemTx
nextValue = s"${value.dropRight(1)}${(value.charAt(value.length - 1) + 1).toChar}"
nextValue = if (value.nonEmpty) s"${value.dropRight(1)}${(value.charAt(value.length - 1) + 1).toChar}" else value
values <- readCursor[SolarSystem, Index](
trx,
SolarSystem,
Expand Down
54 changes: 46 additions & 8 deletions ui/src/main/scala/controltower/page/map/MapAction.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,44 @@
package controltower.page.map

import org.updraft0.controltower.constant.*
import org.updraft0.controltower.protocol.{IntelStance, MapRequest, NewSystemSignature}
import org.updraft0.controltower.protocol.{
IntelStance,
MapRequest,
NewSystemSignature,
WormholeMassSize,
WormholeMassStatus
}

sealed trait SingleSystemAction:
val systemId: SystemId

sealed trait SingleConnectionAction:
val connectionId: ConnectionId

/** All actions on the map are represented by a case here. These actions are then transformed to map requests (if they
* are valid)
*/
enum MapAction:
enum MapAction derives CanEqual:

/** Add a connection between two systems
*/
case AddConnection(fromSystemId: SystemId, toSystemId: SystemId) extends MapAction
case AddConnection(fromSystemId: SystemId, toSystemId: SystemId)

/** Change mass status (crit/reduced/etc.) on a connection
*/
case ConnectionMassStatusChange(connectionId: ConnectionId, status: WormholeMassStatus)
extends MapAction
with SingleConnectionAction

/** Change mass size (S, M, L, XL) on a connection
*/
case ConnectionMassSizeChange(connectionId: ConnectionId, size: WormholeMassSize)
extends MapAction
with SingleConnectionAction

/** Toggle the EOL status of a connection
*/
case ConnectionEolToggle(connectionId: ConnectionId) extends MapAction with SingleConnectionAction

/** Directly send a request to the server (fallback case)
*/
Expand All @@ -33,11 +58,11 @@ enum MapAction:

/** Remove a system from the map
*/
case RemoveMultiple(systemIds: Array[SystemId]) extends MapAction
case RemoveMultiple(systemIds: Array[SystemId])

/** Remove a single connection from the map
*/
case RemoveConnection(connectionId: ConnectionId) extends MapAction
case RemoveConnection(connectionId: ConnectionId)

/** Reposition a system (e.g. via dragging)
*/
Expand All @@ -47,9 +72,13 @@ enum MapAction:
*/
case Select(systemId: Option[SystemId])

/** Select unpinned systems
*/
case SelectUnpinned

/** Toggle bulk selection for a system
*/
case ToggleBulkSelection(systemId: SystemId)
case ToggleBulkSelection(systemId: SystemId) extends MapAction with SingleSystemAction

/** Toggle whether a system is pinned on the map
*/
Expand All @@ -62,11 +91,20 @@ enum MapAction:
/** Update signatures
*/
case UpdateSignatures(systemId: SystemId, replaceAll: Boolean, signatures: Array[NewSystemSignature])
extends MapAction
with SingleSystemAction

/** Remove subset of signatures in system
*/
case RemoveSignatures(systemId: SystemId, signatureIds: Set[SigId])
case RemoveSignatures(systemId: SystemId, signatureIds: Set[SigId]) extends MapAction with SingleSystemAction

/** Clear all signatures in system
*/
case RemoveAllSignatures(systemId: SystemId)
case RemoveAllSignatures(systemId: SystemId) extends MapAction with SingleSystemAction

/** Error shown to user before they can perform an action
*/
enum MapActionError:
/** Connection changes without a linked signature cannot be made (due to choice of storage model)
*/
case UnableToChangeConnectionNoLinkedSignature
15 changes: 7 additions & 8 deletions ui/src/main/scala/controltower/page/map/PositionController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import com.raquo.laminar.api.L
import com.raquo.laminar.api.L.*
import org.updraft0.controltower.constant.SystemId
import org.updraft0.controltower.protocol.*
import controltower.ui.Coord

import scala.collection.mutable

case class Coord(x: Double, y: Double) derives CanEqual
export controltower.ui.Coord

object Coord:
val Hidden = Coord(-1, -1)
val Origin = Coord(0, 0)
val Hidden: Coord = Coord(-1, -1)

/** Determines positions on the map
*/
Expand All @@ -36,16 +35,16 @@ class VarPositionController(map: mutable.Map[SystemId, Var[Coord]], boxSize: Coo
override def newSystemDisplay: SystemDisplayData = SystemDisplayData.Manual(0, 0)

override def systemPosition(systemId: SystemId): Var[Coord] =
map.getOrElseUpdate(systemId, Var(Coord.Hidden))
map.getOrElseUpdate(systemId, Var(Hidden))

override def systemDisplayData(systemId: SystemId)(using Owner): Var[Option[SystemDisplayData]] =
systemPosition(systemId).zoom(c => Option.when(c != Coord.Hidden)(SystemDisplayData.Manual(c.x.toInt, c.y.toInt))) {
systemPosition(systemId).zoom(c => Option.when(c != Hidden)(SystemDisplayData.Manual(c.x.toInt, c.y.toInt))) {
case (coord, Some(m: SystemDisplayData.Manual)) => Coord(m.x, m.y)
case (_, _) => Coord.Hidden
case (_, _) => Hidden
}

override def clear(): Unit =
Var.set(map.view.values.map(v => (v -> Coord.Hidden): VarTuple[_]).toSeq*)
Var.set(map.view.values.map(v => (v -> Hidden): VarTuple[_]).toSeq*)
map.clear()

override def pointInsideBox(coord: Coord): Option[SystemId] =
Expand Down
Loading

0 comments on commit 5c261df

Please sign in to comment.