All files / src/ui hoverLocus.ts

62.5% Statements 15/24
83.33% Branches 10/12
50% Functions 2/4
62.5% Lines 15/24

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64                                                              2x   2x 2x     2x 86x 86x 8x 86x 8x     2x         2x 342x 342x 342x 342x                  
/**
 * The place the pointer is on, wherever it is (ยง25).
 *
 * A store rather than a callback threaded through the panels, for the reason
 * `ui/readout.ts` is one: the surface that produces the place and the surfaces
 * that light it are on opposite sides of the app, and neither should have to
 * know the other exists. A map cell names *two* places โ€” a contact is a pair โ€”
 * and a track, a bead or the 1D browser's axis names one, so the store carries
 * one or two loci and says which surface they came from, and every subscriber
 * ignores its own.
 *
 * The deck's own cell-to-cell echo (`panels/PanelDeck.ts`) is untouched by
 * this and deliberately so: it carries a pointer between maps in cells,
 * converted through base pairs, and a structure has no cells to convert into.
 * What a structure needs is the place itself, which is what this carries โ€” the
 * same decision the deck took, one level up.
 */
 
import type { Locus } from '../render/locus'
 
export interface HoverPlace {
  /** One place, or the two ends of a contact. */
  loci: readonly [Locus] | readonly [Locus, Locus]
  /**
   * Who is pointing: `map` for any contact map (the deck already echoes
   * between maps), `browser` for the 1D browser, or a structure cell's own id.
   */
  source: string
}
 
/** What a contact map calls itself when it publishes the place under its pointer. */
export const HOVER_SOURCE_MAP = 'map'
 
let current: HoverPlace | null = null
const listeners = new Set<(place: HoverPlace | null) => void>()
 
/** Publish where the pointer is, or `null` for nowhere. */
export function setHoverPlace(place: HoverPlace | null): void {
  if (place === null && current === null) return
  if (place !== null && current !== null && samePlace(place, current)) return
  current = place
  for (const listener of listeners) listener(current)
}
 
/** Where the pointer is now. */
export function getHoverPlace(): HoverPlace | null {
  return current
}
 
/** Subscribe, receiving the current value immediately. Returns the teardown. */
export function subscribeHoverPlace(listener: (place: HoverPlace | null) => void): () => void {
  listeners.add(listener)
  listener(current)
  return () => listeners.delete(listener)
}
 
function samePlace(a: HoverPlace, b: HoverPlace): boolean {
  if (a.source !== b.source || a.loci.length !== b.loci.length) return false
  return a.loci.every((locus, at) => {
    const other = b.loci[at]
    return other !== undefined && other.contig === locus.contig && other.bp === locus.bp
  })
}