$ \newcommand{\bF}{\mathbb{F}} \newcommand{\HH}{\mathbb{H}} \newcommand{\NN}{\mathbb{N}} \newcommand{\RR}{\mathbb{R}} \newcommand{\RP}{\mathbb{R}P} \newcommand{\PP}{\mathbb{P}} \newcommand{\ZZ}{\mathbb{Z}} \newcommand{\calA}{\mathcal{A}} \newcommand{\calB}{\mathcal{B}} \newcommand{\calC}{\mathcal{C}} \newcommand{\calD}{\mathcal{D}} \newcommand{\calE}{\mathcal{E}} \newcommand{\calF}{\mathcal{F}} \newcommand{\calG}{\mathcal{G}} \newcommand{\calH}{\mathcal{H}} \newcommand{\calJ}{\mathcal{J}} \newcommand{\calL}{\mathcal{L}} \newcommand{\calM}{\mathcal{M}} \newcommand{\calN}{\mathcal{N}} \newcommand{\calO}{\mathcal{O}} \newcommand{\calP}{\mathcal{P}} \newcommand{\calQ}{\mathcal{Q}} \newcommand{\calS}{\mathcal{S}} \newcommand{\calT}{\mathcal{T}} \newcommand{\calV}{\mathcal{V}} \newcommand{\calX}{\mathcal{X}} \newcommand{\calZ}{\mathcal{Z}} \newcommand{\frakg}{\mathfrak{g}} \newcommand{\balpha}{{\boldsymbol \alpha}} \newcommand{\bbeta}{{\boldsymbol \beta}} \newcommand{\bgamma}{{\boldsymbol \gamma}} \newcommand{\bxi}{{\boldsymbol \xi}} \newcommand{\he}{\hat{e}} \newcommand{\hv}{\hat{v}} \newcommand{\hw}{\hat{w}} \newcommand{\oT}{\overline{T}} \newcommand{\oY}{\overline{Y}} \newcommand{\hDelta}{\widehat \Delta} \newcommand{\hA}{\widehat A} \newcommand{\hT}{\widehat T} \newcommand{\hU}{\widehat U} \newcommand{\hV}{\widehat V} \newcommand{\hX}{\widehat X} \newcommand{\hY}{\widehat Y} \newcommand{\hZ}{\widehat Z} \newcommand{\sfV}{\mathsf{V}} \newcommand{\talpha}{{\widetilde \alpha}} \newcommand{\tbeta}{{\widetilde \beta}} \newcommand{\tgamma}{{\widetilde \gamma}} \newcommand{\tsigma}{{\widetilde \sigma}} \newcommand{\txi}{{\widetilde \xi}} \newcommand{\tSigma}{{\widetilde \Sigma}} \newcommand{\ttau}{{\widetilde \tau}} \newcommand{\ta}{{\widetilde a}} \newcommand{\tb}{{\widetilde b}} \newcommand{\te}{{\widetilde e}} \newcommand{\tv}{{\widetilde v}} \newcommand{\tA}{{\widetilde A}} \newcommand{\tC}{{\widetilde C}} \newcommand{\tD}{{\widetilde D}} \newcommand{\tM}{{\widetilde M}} \newcommand{\tN}{{\widetilde N}} \newcommand{\tT}{{\widetilde T}} \newcommand{\tU}{{\widetilde U}} \newcommand{\tX}{{\widetilde X}} \newcommand{\tY}{{\widetilde Y}} % shortcuts \newcommand{\norm}[1]{\left\| {#1} \right\|} \newcommand{\abs}[1]{\left\lvert {#1} \right\rvert} \newcommand{\wght}[1]{\lvert {#1} \rvert} \newcommand{\I}[1]{\langle #1 \rangle} \newcommand{\bd}{\partial} \newcommand{\from}{\colon\thinspace} \newcommand{\inject}{\hookrightarrow} \newcommand{\inv}{^{-1}} \newcommand{\pmone}{\ensuremath{\{\pm 1\}}} \newcommand{\surject}{\twoheadrightarrow} \newcommand{\GP}[2]{\left( #1 \, . \, #2 \right)} \newcommand{\FN}{F_{N}} \newcommand{\dsym}{d^{\rm \, sym}} \newcommand{\margin}[1]{\marginpar{\scriptsize #1}} \newcommand{\dbtilde}[1]{\widetilde{\raisebox{0pt}[0.85\height]{$\widetilde{#1}$}}} \newcommand{\vsub}{\rotatebox[origin=c]{-90}{$\subseteq$}} % complexes \newcommand{\FF}{\mathrm{FF}} \newcommand{\ZF}{\mathcal{Z}\mathrm{F}} \newcommand{\coS}{\mathrm{CoS(\FN)}} $
#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 950
## file: app.R
# Two markings of the same torus
#
# Pick a lattice Z + tau Z and a matrix g in SL(2, Z). The app draws
#   - the parallelogram on the basis (1, tau), and
#   - the parallelogram on the new basis (c tau + d, a tau + b),
# both in the SAME lattice, without renormalizing. Both tile the same lattice
# points and have the same area: they are two ways of cutting open one torus.
# A "normalize" slider rotates and scales the plane by (c tau + d)^(-t); at t = 1
# the new first vector is 1 and the new parallelogram's far corner is g . tau.
# A second panel shows the normalized parallelograms (1, tau) and (1, g . tau)
# and the reduced point in the upper half-plane.
#
# Convention: g = [[a, b], [c, d]] acts on the column (omega2, omega1), so
#   new omega2 = a tau + b,  new omega1 = c tau + d,
#   g . tau = (a tau + b) / (c tau + d).
#
# TWISTS. A twist slider deforms the torus before the marking is changed:
#   twist along a (direction 1):    basis (1, tau) -> (1, tau + t)
#   twist along b (direction tau):  basis (1, tau) -> (1 + t tau, tau)
# For fractional t this is a different torus (a different point of Teichmüller
# space). At integer t = n the lattice is the original one again, and the
# twisted basis is the original basis changed by the Dehn twist T^n (for a) or
# [[1, 0], [n, 1]] (for b): an integer twist IS a change of marking. Everything
# else in the app (both parallelograms, g . tau, the reduction, the shortest
# curve) is computed for the twisted torus. The picture is drawn in the twisted
# basis's own coordinates, so the lattice dots visibly return at integer t.
# The original parallelogram (1, tau) is shown dashed for comparison.
#
# Run with: shiny::runApp("path/to/marking_app")

library(shiny)
library(bslib)
library(ggplot2)
library(dplyr)
library(tidyr)
library(tibble)
library(purrr)

col_orig <- "#1D9E75" # basis (1, tau)
col_new <- "#D85A30" # basis (c tau + d, a tau + b)
col_red <- "#185FA5" # reduced point

tau_presets <- list(
  "0.3 + 1.2i" = complex(real = 0.3, imaginary = 1.2),
  "i (square)" = 1i,
  "e^(pi i/3) (hexagonal)" = exp(1i * pi / 3),
  "e^(5 pi i/6)" = exp(1i * 5 * pi / 6),
  "1.6 + 0.2i" = complex(real = 1.6, imaginary = 0.2)
)

g_presets <- list(
  "I (identity)" = c(1, 0, 0, 1),
  "T = [[1,1],[0,1]]" = c(1, 1, 0, 1),
  "T^5" = c(1, 5, 0, 1),
  "S = [[0,-1],[1,0]]" = c(0, -1, 1, 0),
  "ST = [[0,-1],[1,1]]" = c(0, -1, 1, 1),
  "[[2,1],[1,1]]" = c(2, 1, 1, 1),
  "[[5,3],[3,2]]" = c(5, 3, 3, 2),
  "[[13,8],[8,5]]" = c(13, 8, 8, 5),
  "[[1,0],[4,1]]" = c(1, 0, 4, 1)
)

# ---- math helpers -----------------------------------------------------------

fmt_c <- function(z, digits = 3) {
  re <- round(Re(z), digits)
  im <- round(Im(z), digits)
  paste0(re, if (im < 0) " - " else " + ", abs(im), "i")
}

# Reduce tau to the standard fundamental domain, recording the moves.
reduce_tau <- function(tau, max_iter = 200, tol = 1e-12) {
  moves <- character()
  for (k in seq_len(max_iter)) {
    n <- round(Re(tau))
    if (n != 0) {
      tau <- tau - n
      moves <- c(moves, paste0("T^", -n))
    }
    if (Mod(tau) < 1 - tol) {
      tau <- -1 / tau
      moves <- c(moves, "S")
    } else {
      break
    }
  }
  list(tau = tau, moves = moves)
}

parallelogram <- function(v1, v2) {
  z <- c(0, v1, v1 + v2, v2)
  tibble(x = Re(z), y = Im(z))
}

# Lattice points m + n tau inside a box, or NULL if there would be too many.
lattice_points <- function(tau, xlim, ylim, max_pts = 40000) {
  n_rng <- seq(floor(ylim[1] / Im(tau)) - 1, ceiling(ylim[2] / Im(tau)) + 1)
  est <- length(n_rng) * (diff(xlim) + 3)
  if (est > max_pts) {
    return(NULL)
  }
  map_dfr(n_rng, function(n) {
    m <- seq(
      floor(xlim[1] - n * Re(tau)) - 1,
      ceiling(xlim[2] - n * Re(tau)) + 1
    )
    z <- m + n * tau
    tibble(x = Re(z), y = Im(z))
  }) |>
    filter(between(x, xlim[1], xlim[2]), between(y, ylim[1], ylim[2]))
}

# Shortest nonzero vector of the lattice Z + tau Z (Lagrange-Gauss reduction).
# For the square and hexagonal lattices there are ties; this returns one of them.
shortest_vector <- function(tau, max_iter = 200) {
  u <- 1 + 0i
  v <- tau
  for (k in seq_len(max_iter)) {
    if (Mod(v) < Mod(u)) {
      tmp <- u
      u <- v
      v <- tmp
    }
    mu <- round(Re(v * Conj(u)) / Mod(u)^2)
    if (mu == 0) {
      break
    }
    v <- v - mu * u
  }
  if (Mod(v) < Mod(u)) v else u
}

# Integer coordinates (s, t) with z = s p1 + t p2.
lattice_coords <- function(z, p1, p2) {
  M <- matrix(c(Re(p1), Im(p1), Re(p2), Im(p2)), 2)
  round(solve(M, c(Re(z), Im(z))))
}

# The closed geodesic in direction v, cut into pieces by the parallelogram on
# (p1, p2) and folded back into it. The curve passes through the parallelogram's
# center. Returns one row per piece with complex endpoints z0, z1.
folded_curve <- function(v, p1, p2) {
  st <- lattice_coords(v, p1, p2)
  S <- st[1]
  Tt <- st[2]
  s0 <- 0.5
  t0 <- 0.5
  cuts <- function(x0, D) {
    if (D == 0) {
      return(numeric())
    }
    r <- sort(c(x0, x0 + D))
    k <- seq(ceiling(r[1]), floor(r[2]))
    (k - x0) / D
  }
  u <- sort(unique(c(0, 1, cuts(s0, S), cuts(t0, Tt))))
  u <- u[u >= 0 & u <= 1]
  pieces <- map_dfr(seq_len(length(u) - 1), function(i) {
    if (u[i + 1] - u[i] < 1e-12) {
      return(NULL)
    }
    um <- (u[i] + u[i + 1]) / 2
    sh <- c(floor(s0 + um * S), floor(t0 + um * Tt))
    pt <- function(uu) (s0 + uu * S - sh[1]) * p1 + (t0 + uu * Tt - sh[2]) * p2
    tibble(z0 = pt(u[i]), z1 = pt(u[i + 1]))
  })
  # The start point (the center) is interior, so the last piece runs straight
  # into the first one: merge them. This leaves |S| + |T| pieces.
  n <- nrow(pieces)
  if (n > 1) {
    pieces$z0[1] <- pieces$z0[n]
    pieces <- pieces[-n, ]
  }
  pieces |> mutate(piece = row_number())
}

col_short <- "#2C2C2A" # shortest closed curve

# "2(cτ + d) − (aτ + b)" style formatting of an integer combination.
fmt_comb <- function(k, names) {
  terms <- character()
  for (i in seq_along(k)) {
    if (k[i] == 0) {
      next
    }
    coef <- if (abs(k[i]) == 1) "" else as.character(abs(k[i]))
    nm <- if (names[i] == "·1") {
      (if (coef == "") "1" else coef)
    } else {
      paste0(coef, names[i])
    }
    sgn <- if (k[i] < 0) "−" else "+"
    terms <- c(
      terms,
      if (length(terms) == 0) {
        paste0(if (k[i] < 0) "−" else "", nm)
      } else {
        paste(sgn, nm)
      }
    )
  }
  if (length(terms) == 0) "0" else paste(terms, collapse = " ")
}

# ---- twists ------------------------------------------------------------------
#
# The twisted basis (omega1, omega2) and its normalized form (1, tau_t):
#   a: (1, tau + t),        so tau_t = tau + t,             omega1 = 1
#   b: (1 + t tau, tau),    so tau_t = tau / (1 + t tau),   omega1 = 1 + t tau
# (1 + t tau is never 0, since Im tau > 0.) The app computes with the normalized
# lattice Z + tau_t Z and multiplies the picture by omega1 to draw it in the
# twisted basis's own coordinates.

twist_tau <- function(tau, curve, t) {
  if (curve == "a") {
    list(tau = tau + t, omega1 = 1 + 0i)
  } else {
    list(tau = tau / (1 + t * tau), omega1 = 1 + t * tau)
  }
}

col_twist <- "#C0392B" # the twist path in the upper half-plane

# ---- plots ------------------------------------------------------------------

# tau is the (twisted, normalized) tau_t; omega1 scales the picture into the
# twisted basis's own coordinates; tau0 (if given) is the untwisted tau, whose
# parallelogram (1, tau0) is drawn dashed.
lattice_plot <- function(
  tau,
  g,
  zoom = FALSE,
  t = 0,
  show_short = FALSE,
  omega1 = 1 + 0i,
  tau0 = NULL
) {
  a <- g[1]
  b <- g[2]
  c <- g[3]
  d <- g[4]
  w1 <- c * tau + d # new first vector
  w2 <- a * tau + b # new second vector
  gtau <- w2 / w1

  # Normalizing map, run partway. Points are stored in normalized coordinates
  # (first vector 1); the picture multiplies them by
  #   f(s) = omega1^(1 - s) * w1^(-s).
  # At s = 0 this is omega1: the twisted lattice in its own coordinates (for no
  # twist, omega1 = 1 and nothing moves). At s = 1 it is 1 / w1: the new first
  # vector lands on 1 and the new second vector on g.tau.
  f_at <- function(s) {
    exp(
      (1 - s) *
        complex(real = log(Mod(omega1)), imaginary = Arg(omega1)) -
        s * complex(real = log(Mod(w1)), imaginary = Arg(w1))
    )
  }
  f <- f_at(t)

  lv <- c("(1, τ)", "(cτ + d, aτ + b)")
  para_z <- function(v1, v2, s) tibble(z = c(0, v1, v1 + v2, v2) * f_at(s))
  paras <- bind_rows(
    para_z(1, tau, t) |> mutate(basis = lv[1]),
    para_z(w1, w2, t) |> mutate(basis = lv[2])
  ) |>
    mutate(x = Re(z), y = Im(z), basis = factor(basis, levels = lv))

  # Fixed window over the whole slider range, so the picture doesn't jump.
  ss <- seq(0, 1, by = 0.1)
  frame_z <- if (zoom) {
    c(unlist(map(ss, \(s) para_z(1, tau, s)$z)), 1, gtau)
  } else {
    c(unlist(map(ss, \(s) c(para_z(1, tau, s)$z, para_z(w1, w2, s)$z))))
  }
  # the original parallelogram (1, tau0), in normalized coordinates: divide by omega1
  orig_norm <- if (!is.null(tau0)) c(0, 1, 1 + tau0, tau0) / omega1 else NULL
  if (!is.null(orig_norm) && !zoom) {
    frame_z <- c(frame_z, unlist(map(ss, \(s) orig_norm * f_at(s))))
  }
  fx <- range(Re(frame_z))
  fy <- range(Im(frame_z))
  pad <- if (zoom) 0.6 else 0.12 * max(diff(fx), diff(fy), 1)
  xlim <- fx + c(-pad, pad)
  ylim <- fy + c(-pad, pad)

  # Lattice points: find them in the original plane, then move them by f.
  corners <- complex(
    real = xlim[c(1, 2, 2, 1)],
    imaginary = ylim[c(1, 1, 2, 2)]
  ) /
    f
  pts <- lattice_points(tau, range(Re(corners)), range(Im(corners)))
  if (!is.null(pts)) {
    pts <- tibble(z = complex(real = pts$x, imaginary = pts$y) * f) |>
      mutate(x = Re(z), y = Im(z)) |>
      filter(between(x, xlim[1], xlim[2]), between(y, ylim[1], ylim[2]))
  }

  vec_z <- c(1, tau, w1, w2) * f
  vecs <- tibble(
    xend = Re(vec_z),
    yend = Im(vec_z),
    basis = factor(lv[c(1, 1, 2, 2)], levels = lv)
  )

  # Where the orange corner ends up when fully normalized
  target <- tibble(x = Re(gtau), y = Im(gtau))

  sub <- if (t == 0) {
    if (!is.null(tau0)) {
      "Twisted lattice (dashed: original)"
    } else {
      "Both bases in the same lattice (not normalized)"
    }
  } else if (t == 1) {
    "Normalized: cτ + d → 1, aτ + b → g·τ"
  } else {
    paste0("Normalizing: plane multiplied by (cτ + d)^(−", t, ")")
  }
  if (is.null(pts)) {
    sub <- paste0(sub, "\nToo many lattice points to draw")
  }

  p <- ggplot() +
    geom_hline(yintercept = 0, color = "grey80") +
    geom_vline(xintercept = 0, color = "grey80")
  if (!is.null(pts)) {
    p <- p + geom_point(data = pts, aes(x, y), size = 0.9, color = "grey35")
  }
  if (!is.null(orig_norm)) {
    oz <- c(orig_norm, orig_norm[1]) * f
    p <- p +
      geom_path(
        data = tibble(z = oz),
        aes(Re(z), Im(z)),
        colour = col_orig,
        linetype = "22",
        linewidth = 0.6
      )
  }
  p <- p +
    geom_polygon(
      data = paras,
      aes(x, y, fill = basis, color = basis),
      alpha = 0.22,
      linewidth = 0.7
    ) +
    geom_segment(
      data = vecs,
      aes(0, 0, xend = xend, yend = yend, color = basis),
      linewidth = 1,
      arrow = arrow(length = unit(0.1, "inches"))
    )
  if (show_short) {
    v <- shortest_vector(tau)
    curve <- bind_rows(folded_curve(v, 1, tau), folded_curve(v, w1, w2)) |>
      mutate(z0 = z0 * f, z1 = z1 * f)
    p <- p +
      geom_segment(
        data = curve,
        aes(Re(z0), Im(z0), xend = Re(z1), yend = Im(z1)),
        color = col_short,
        linewidth = 1.5,
        lineend = "round"
      ) +
      geom_point(
        data = curve,
        aes(Re(z0), Im(z0)),
        color = col_short,
        size = 1.6
      ) +
      geom_point(
        data = curve,
        aes(Re(z1), Im(z1)),
        color = col_short,
        size = 1.6
      )
  }
  p +
    geom_point(
      data = target,
      aes(x, y),
      shape = 21,
      size = 4,
      stroke = 1,
      color = col_new,
      fill = NA
    ) +
    geom_text(
      data = target,
      aes(x, y),
      label = "g·τ",
      color = col_new,
      vjust = -1.3,
      size = 4.5
    ) +
    scale_fill_manual(values = c(col_orig, col_new), name = "Basis") +
    scale_color_manual(values = c(col_orig, col_new), name = "Basis") +
    coord_fixed(xlim = xlim, ylim = ylim, expand = FALSE) +
    labs(x = "Re", y = "Im", subtitle = sub) +
    theme_minimal(base_size = 13) +
    theme(legend.position = "bottom", panel.grid.minor = element_blank())
}

# twist_path (if given): the path tau_s, s from 0 to the current twist, drawn
# from the untwisted tau to the twisted one.
h_plot <- function(tau, g, red, show_short = FALSE, twist_path = NULL) {
  a <- g[1]
  b <- g[2]
  c <- g[3]
  d <- g[4]
  gtau <- (a * tau + b) / (c * tau + d)
  w1 <- c * tau + d

  # Normalized parallelograms: (1, tau) and (1, g.tau). Each point is a corner.
  lv <- c("(1, τ)", "(1, g·τ)")
  paras <- bind_rows(
    parallelogram(1, tau) |> mutate(basis = lv[1]),
    parallelogram(1, gtau) |> mutate(basis = lv[2])
  ) |>
    mutate(basis = factor(basis, levels = lv))

  pts <- tibble(
    z = c(tau, gtau, red),
    label = c("τ", "g·τ", "reduced"),
    col = c(col_orig, col_new, col_red)
  ) |>
    mutate(x = Re(z), y = Im(z)) |>
    # merge points that coincide (e.g. tau already reduced) into one label
    mutate(key = paste(round(x, 6), round(y, 6))) |>
    group_by(key) |>
    summarise(
      x = first(x),
      y = first(y),
      col = first(col),
      label = paste(label, collapse = " = "),
      .groups = "drop"
    )

  extra <- if (is.null(twist_path)) complex() else twist_path
  ymax <- max(2.2, max(paras$y, pts$y, Im(extra)) * 1.15)
  xlim <- c(
    min(-1.6, min(paras$x, pts$x, Re(extra)) - 0.4),
    max(1.6, max(paras$x, pts$x, Re(extra)) + 0.4)
  )

  fd <- tibble(theta = seq(pi / 3, 2 * pi / 3, length.out = 100)) |>
    mutate(x = cos(theta), y = sin(theta))
  fd_poly <- bind_rows(
    tibble(x = -0.5, y = ymax),
    fd |> arrange(desc(theta)) |> select(x, y),
    tibble(x = 0.5, y = ymax)
  )
  circle <- tibble(theta = seq(0, pi, length.out = 200)) |>
    mutate(x = cos(theta), y = sin(theta))

  p <- ggplot() +
    geom_polygon(
      data = fd_poly,
      aes(x, y),
      fill = "grey92",
      color = "grey55",
      linetype = "dashed"
    ) +
    geom_path(data = circle, aes(x, y), color = "grey75", linetype = "dotted") +
    geom_hline(yintercept = 0, color = "grey40") +
    geom_polygon(
      data = paras,
      aes(x, y, fill = basis, color = basis),
      alpha = 0.18,
      linewidth = 0.6
    )
  if (show_short) {
    # The normalized new parallelogram is the old one divided by w1, so the
    # shortest curve on it is the folded curve divided by w1 too.
    v <- shortest_vector(tau)
    curve <- bind_rows(
      folded_curve(v, 1, tau),
      folded_curve(v / w1, 1, gtau)
    )
    p <- p +
      geom_segment(
        data = curve,
        aes(Re(z0), Im(z0), xend = Re(z1), yend = Im(z1)),
        color = col_short,
        linewidth = 1.2,
        lineend = "round"
      )
  }
  if (!is.null(twist_path)) {
    tp <- tibble(z = twist_path)
    p <- p +
      geom_path(
        data = tp,
        aes(Re(z), Im(z)),
        colour = col_twist,
        linewidth = 1,
        arrow = arrow(length = unit(0.08, "inches"))
      ) +
      geom_point(
        data = tp[1, ],
        aes(Re(z), Im(z)),
        colour = col_twist,
        size = 2.2,
        shape = 21,
        fill = "white",
        stroke = 1
      )
  }
  p +
    geom_point(data = pts, aes(x, y), color = pts$col, size = 3) +
    geom_text(
      data = pts,
      aes(x, y, label = label),
      color = pts$col,
      vjust = -1,
      size = 4.5
    ) +
    scale_fill_manual(values = c(col_orig, col_new), name = NULL) +
    scale_color_manual(values = c(col_orig, col_new), name = NULL) +
    coord_fixed(xlim = xlim, ylim = c(-0.05, ymax), expand = FALSE) +
    labs(x = "Re", y = "Im") +
    theme_minimal(base_size = 13) +
    theme(legend.position = "bottom", panel.grid.minor = element_blank())
}

# ---- UI -----------------------------------------------------------------------

# The controls are grouped into tabs on one card in the sidebar (τ, Twist, g,
# View), so only one group is open at a time and the sidebar doesn't scroll.
# The plots and the table each sit on their own card. Inputs on a hidden tab
# keep their values, so switching tabs never changes the picture.

ui <- page_sidebar(
  title = "Twists of a marked torus",
  sidebar = sidebar(
    width = 340,
    navset_card_tab(
      id = "controls",
      nav_panel(
        "τ",
        selectInput(
          "tau_preset",
          "Preset τ",
          choices = c("(custom)", names(tau_presets)),
          selected = "0.3 + 1.2i"
        ),
        layout_columns(
          numericInput("re_tau", "Re τ", value = 0.3, step = 0.05),
          numericInput("im_tau", "Im τ", value = 1.2, min = 0.05, step = 0.05)
        ),
        helpText("The lattice Z + τZ: the torus before any twist.")
      ),
      nav_panel(
        "Twist",
        radioButtons(
          "tw_curve",
          "Twist along",
          inline = TRUE,
          choiceNames = list("a (direction 1)", "b (direction τ)"),
          choiceValues = c("a", "b")
        ),
        sliderInput(
          "tw",
          "Twist amount",
          min = -2,
          max = 2,
          value = 0,
          step = 0.01,
          animate = animationOptions(interval = 100)
        ),
        helpText(
          "Deforms the torus: a sends τ to τ + t; b sends the basis (1, τ) to",
          "(1 + tτ, τ). At integer t the lattice dots return to the original",
          "lattice: an integer twist is a change of marking by a Dehn twist."
        )
      ),
      nav_panel(
        "g",
        selectInput(
          "g_preset",
          "Preset g ∈ SL(2, Z)",
          choices = c("(custom)", names(g_presets)),
          selected = "[[2,1],[1,1]]"
        ),
        layout_columns(
          numericInput("a", "a", value = 2, step = 1),
          numericInput("b", "b", value = 1, step = 1)
        ),
        layout_columns(
          numericInput("c", "c", value = 1, step = 1),
          numericInput("d", "d", value = 1, step = 1)
        ),
        uiOutput("det_msg"),
        helpText(
          "g acts on the column (ω₂, ω₁): the new basis is",
          "(cτ + d, aτ + b), and g·τ = (aτ + b)/(cτ + d)."
        )
      ),
      nav_panel(
        "View",
        sliderInput(
          "t",
          "Normalize the new basis",
          min = 0,
          max = 1,
          value = 0,
          step = 0.02,
          animate = animationOptions(interval = 120)
        ),
        helpText(
          "Slides the plane by (cτ + d)^(−t). At t = 1 the new first",
          "vector is 1 and the orange corner sits on g·τ."
        ),
        checkboxInput(
          "zoom",
          "Zoom lattice view to the original parallelogram",
          FALSE
        ),
        checkboxInput("show_short", "Show the shortest closed curve", FALSE),
        helpText(
          "Drawn through the center of each parallelogram and folded back",
          "in by the side gluings. The pieces join into one closed curve."
        )
      )
    )
  ),
  layout_columns(
    col_widths = c(7, 5),
    card(
      full_screen = TRUE,
      card_header("Same lattice, two parallelograms"),
      plotOutput("lattice", height = "520px")
    ),
    layout_columns(
      col_widths = 12,
      card(
        full_screen = TRUE,
        card_header("Upper half-plane"),
        plotOutput("hplane", height = "380px")
      ),
      card(
        card_header("Details"),
        tableOutput("info")
      )
    )
  )
)

# ---- server -------------------------------------------------------------------

server <- function(input, output, session) {
  observeEvent(input$tau_preset, {
    z <- tau_presets[[input$tau_preset]]
    if (!is.null(z)) {
      updateNumericInput(session, "re_tau", value = round(Re(z), 4))
      updateNumericInput(session, "im_tau", value = round(Im(z), 4))
    }
  })

  observeEvent(input$g_preset, {
    m <- g_presets[[input$g_preset]]
    if (!is.null(m)) {
      updateNumericInput(session, "a", value = m[1])
      updateNumericInput(session, "b", value = m[2])
      updateNumericInput(session, "c", value = m[3])
      updateNumericInput(session, "d", value = m[4])
    }
  })

  # tau0: the untwisted tau from the inputs. tw_state: the twisted torus.
  # tau(): the twisted, normalized tau_t, which everything else uses.
  tau0 <- reactive({
    req(is.numeric(input$re_tau), is.numeric(input$im_tau))
    validate(need(input$im_tau > 0, "Im τ must be positive."))
    complex(real = input$re_tau, imaginary = input$im_tau)
  })
  tw_amount <- reactive(if (is.null(input$tw)) 0 else input$tw)
  tw_curve <- reactive(if (is.null(input$tw_curve)) "a" else input$tw_curve)
  tw_state <- reactive(twist_tau(tau0(), tw_curve(), tw_amount()))
  tau <- reactive(tw_state()$tau)
  twist_path <- reactive({
    if (tw_amount() == 0) {
      return(NULL)
    }
    vapply(
      seq(0, tw_amount(), length.out = 121),
      \(s) twist_tau(tau0(), tw_curve(), s)$tau,
      complex(1)
    )
  })

  g_raw <- reactive({
    req(
      is.numeric(input$a),
      is.numeric(input$b),
      is.numeric(input$c),
      is.numeric(input$d)
    )
    c(input$a, input$b, input$c, input$d)
  })

  g_ok <- reactive({
    g <- g_raw()
    all(g == round(g)) && (g[1] * g[4] - g[2] * g[3]) == 1
  })

  g <- reactive({
    validate(need(g_ok(), "Enter integers a, b, c, d with ad − bc = 1."))
    g_raw()
  })

  output$det_msg <- renderUI({
    gr <- g_raw()
    det <- gr[1] * gr[4] - gr[2] * gr[3]
    if (g_ok()) {
      tags$p(style = "color:#0F6E56;", paste0("det = ", det, "  ✓"))
    } else if (any(gr != round(gr))) {
      tags$p(style = "color:#A32D2D;", "Entries must be integers.")
    } else {
      tags$p(style = "color:#A32D2D;", paste0("det = ", det, ". It must be 1."))
    }
  })

  reduced <- reactive(reduce_tau(tau()))

  output$lattice <- renderPlot(
    lattice_plot(
      tau(),
      g(),
      isTRUE(input$zoom),
      if (is.null(input$t)) 0 else input$t,
      isTRUE(input$show_short),
      omega1 = tw_state()$omega1,
      tau0 = if (tw_amount() != 0) tau0() else NULL
    ),
    res = 96
  )
  output$hplane <- renderPlot(
    h_plot(tau(), g(), reduced()$tau, isTRUE(input$show_short), twist_path()),
    res = 96
  )

  output$info <- renderTable(
    {
      t <- tau()
      m <- g()
      w1 <- m[3] * t + m[4]
      w2 <- m[1] * t + m[2]
      gt <- (m[1] * t + m[2]) / (m[3] * t + m[4])
      red <- reduced()
      v <- shortest_vector(t)
      st_old <- lattice_coords(v, 1, t)
      st_new <- lattice_coords(v, w1, w2)
      tw_t <- tw_amount()
      n_int <- abs(tw_t - round(tw_t)) < 1e-9
      twist_rows <- if (tw_t == 0) {
        NULL
      } else {
        tibble(
          Quantity = c(
            "Twist",
            "Twisted τ (normalized)",
            "Back to the original lattice?"
          ),
          Value = c(
            sprintf("along %s by t = %s", tw_curve(), format(tw_t)),
            fmt_c(t),
            if (n_int) {
              sprintf(
                "yes: the twist is the change of marking %s",
                if (tw_curve() == "a") {
                  sprintf("T^%d = [[1,%d],[0,1]]", round(tw_t), round(tw_t))
                } else {
                  sprintf("[[1,0],[%d,1]]", round(tw_t))
                }
              )
            } else {
              "no: fractional t gives a different torus"
            }
          )
        )
      }
      bind_rows(
        twist_rows,
        tibble(
          Quantity = c(
            if (tw_t == 0) "τ" else "τ (twisted)",
            "g·τ",
            "Original side lengths",
            "New side lengths",
            "Area (both)",
            "Shortest curve (as a lattice vector)",
            "  in the new basis",
            "  pieces in (1, τ) / in new parallelogram",
            "(shortest)² / area",
            "Reduced τ",
            "Reduction steps from τ"
          ),
          Value = c(
            fmt_c(t),
            fmt_c(gt),
            paste0("1, ", round(Mod(t), 3)),
            paste0(round(Mod(w1), 3), ", ", round(Mod(w2), 3)),
            as.character(round(Im(t), 3)),
            paste0(
              fmt_comb(st_old, c("·1", "τ")),
              ",  length ",
              round(Mod(v), 3)
            ),
            fmt_comb(st_new, c("(cτ + d)", "(aτ + b)")),
            paste0(
              nrow(folded_curve(v, 1, t)),
              " / ",
              nrow(folded_curve(v, w1, w2))
            ),
            as.character(round(Mod(v)^2 / Im(t), 3)),
            fmt_c(red$tau),
            if (length(red$moves)) {
              paste(red$moves, collapse = ", ")
            } else {
              "none (already reduced)"
            }
          )
        )
      )
    },
    striped = TRUE,
    spacing = "s"
  )
}

shinyApp(ui, server)