viewport.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. package viewport
  2. import (
  3. "math"
  4. "strings"
  5. "github.com/charmbracelet/bubbles/v2/key"
  6. tea "github.com/charmbracelet/bubbletea/v2"
  7. "github.com/charmbracelet/lipgloss/v2"
  8. "github.com/charmbracelet/x/ansi"
  9. )
  10. const (
  11. defaultHorizontalStep = 6
  12. )
  13. // Option is a configuration option that works in conjunction with [New]. For
  14. // example:
  15. //
  16. // timer := New(WithWidth(10, WithHeight(5)))
  17. type Option func(*Model)
  18. // WithWidth is an initialization option that sets the width of the
  19. // viewport. Pass as an argument to [New].
  20. func WithWidth(w int) Option {
  21. return func(m *Model) {
  22. m.width = w
  23. }
  24. }
  25. // WithHeight is an initialization option that sets the height of the
  26. // viewport. Pass as an argument to [New].
  27. func WithHeight(h int) Option {
  28. return func(m *Model) {
  29. m.height = h
  30. }
  31. }
  32. // New returns a new model with the given width and height as well as default
  33. // key mappings.
  34. func New(opts ...Option) (m Model) {
  35. for _, opt := range opts {
  36. opt(&m)
  37. }
  38. m.setInitialValues()
  39. m.memo = &Memo{}
  40. return m
  41. }
  42. type Memo struct {
  43. dirty bool
  44. cache string
  45. }
  46. func (m *Memo) View(render func() string) string {
  47. if m.dirty {
  48. // slog.Debug("memo dirty")
  49. m.cache = render()
  50. m.dirty = false
  51. return m.cache
  52. }
  53. // slog.Debug("memo cache")
  54. return m.cache
  55. }
  56. func (m *Memo) Invalidate() {
  57. m.dirty = true
  58. }
  59. // Model is the Bubble Tea model for this viewport element.
  60. type Model struct {
  61. memo *Memo
  62. width int
  63. height int
  64. KeyMap KeyMap
  65. // Whether or not to wrap text. If false, it'll allow horizontal scrolling
  66. // instead.
  67. SoftWrap bool
  68. // Whether or not to fill to the height of the viewport with empty lines.
  69. FillHeight bool
  70. // Whether or not to respond to the mouse. The mouse must be enabled in
  71. // Bubble Tea for this to work. For details, see the Bubble Tea docs.
  72. MouseWheelEnabled bool
  73. // The number of lines the mouse wheel will scroll. By default, this is 3.
  74. MouseWheelDelta int
  75. // YOffset is the vertical scroll position.
  76. YOffset int
  77. // xOffset is the horizontal scroll position.
  78. xOffset int
  79. // horizontalStep is the number of columns we move left or right during a
  80. // default horizontal scroll.
  81. horizontalStep int
  82. // YPosition is the position of the viewport in relation to the terminal
  83. // window. It's used in high performance rendering only.
  84. YPosition int
  85. // Style applies a lipgloss style to the viewport. Realistically, it's most
  86. // useful for setting borders, margins and padding.
  87. Style lipgloss.Style
  88. // LeftGutterFunc allows to define a [GutterFunc] that adds a column into
  89. // the left of the viewport, which is kept when horizontal scrolling.
  90. // This can be used for things like line numbers, selection indicators,
  91. // show statuses, etc.
  92. LeftGutterFunc GutterFunc
  93. initialized bool
  94. lines []string
  95. longestLineWidth int
  96. // HighlightStyle highlights the ranges set with [SetHighligths].
  97. HighlightStyle lipgloss.Style
  98. // SelectedHighlightStyle highlights the highlight range focused during
  99. // navigation.
  100. // Use [SetHighligths] to set the highlight ranges, and [HightlightNext]
  101. // and [HihglightPrevious] to navigate.
  102. SelectedHighlightStyle lipgloss.Style
  103. // StyleLineFunc allows to return a [lipgloss.Style] for each line.
  104. // The argument is the line index.
  105. StyleLineFunc func(int) lipgloss.Style
  106. highlights []highlightInfo
  107. hiIdx int
  108. }
  109. // GutterFunc can be implemented and set into [Model.LeftGutterFunc].
  110. //
  111. // Example implementation showing line numbers:
  112. //
  113. // func(info GutterContext) string {
  114. // if info.Soft {
  115. // return " │ "
  116. // }
  117. // if info.Index >= info.TotalLines {
  118. // return " ~ │ "
  119. // }
  120. // return fmt.Sprintf("%4d │ ", info.Index+1)
  121. // }
  122. type GutterFunc func(GutterContext) string
  123. // NoGutter is the default gutter used.
  124. var NoGutter = func(GutterContext) string { return "" }
  125. // GutterContext provides context to a [GutterFunc].
  126. type GutterContext struct {
  127. Index int
  128. TotalLines int
  129. Soft bool
  130. }
  131. func (m *Model) setInitialValues() {
  132. m.KeyMap = DefaultKeyMap()
  133. m.MouseWheelEnabled = true
  134. m.MouseWheelDelta = 3
  135. m.initialized = true
  136. m.horizontalStep = defaultHorizontalStep
  137. m.LeftGutterFunc = NoGutter
  138. }
  139. // Init exists to satisfy the tea.Model interface for composability purposes.
  140. func (m Model) Init() tea.Cmd {
  141. return nil
  142. }
  143. // Height returns the height of the viewport.
  144. func (m Model) Height() int {
  145. return m.height
  146. }
  147. // SetHeight sets the height of the viewport.
  148. func (m *Model) SetHeight(h int) {
  149. m.height = h
  150. m.memo.Invalidate()
  151. }
  152. // Width returns the width of the viewport.
  153. func (m Model) Width() int {
  154. return m.width
  155. }
  156. // SetWidth sets the width of the viewport.
  157. func (m *Model) SetWidth(w int) {
  158. m.width = w
  159. m.memo.Invalidate()
  160. }
  161. // AtTop returns whether or not the viewport is at the very top position.
  162. func (m Model) AtTop() bool {
  163. return m.YOffset <= 0
  164. }
  165. // AtBottom returns whether or not the viewport is at or past the very bottom
  166. // position.
  167. func (m Model) AtBottom() bool {
  168. return m.YOffset >= m.maxYOffset()
  169. }
  170. // PastBottom returns whether or not the viewport is scrolled beyond the last
  171. // line. This can happen when adjusting the viewport height.
  172. func (m Model) PastBottom() bool {
  173. return m.YOffset > m.maxYOffset()
  174. }
  175. // ScrollPercent returns the amount scrolled as a float between 0 and 1.
  176. func (m Model) ScrollPercent() float64 {
  177. count := m.lineCount()
  178. if m.Height() >= count {
  179. return 1.0
  180. }
  181. y := float64(m.YOffset)
  182. h := float64(m.Height())
  183. t := float64(count)
  184. v := y / (t - h)
  185. return math.Max(0.0, math.Min(1.0, v))
  186. }
  187. // HorizontalScrollPercent returns the amount horizontally scrolled as a float
  188. // between 0 and 1.
  189. func (m Model) HorizontalScrollPercent() float64 {
  190. if m.xOffset >= m.longestLineWidth-m.Width() {
  191. return 1.0
  192. }
  193. y := float64(m.xOffset)
  194. h := float64(m.Width())
  195. t := float64(m.longestLineWidth)
  196. v := y / (t - h)
  197. return math.Max(0.0, math.Min(1.0, v))
  198. }
  199. // SetContent set the pager's text content.
  200. // Line endings will be normalized to '\n'.
  201. func (m *Model) SetContent(s string) {
  202. s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
  203. m.SetContentLines(strings.Split(s, "\n"))
  204. m.memo.Invalidate()
  205. }
  206. // SetContentLines allows to set the lines to be shown instead of the content.
  207. // If a given line has a \n in it, it'll be considered a [Model.SoftWrap].
  208. // See also [Model.SetContent].
  209. func (m *Model) SetContentLines(lines []string) {
  210. // if there's no content, set content to actual nil instead of one empty
  211. // line.
  212. m.lines = lines
  213. if len(m.lines) == 1 && ansi.StringWidth(m.lines[0]) == 0 {
  214. m.lines = nil
  215. }
  216. m.longestLineWidth = maxLineWidth(m.lines)
  217. m.ClearHighlights()
  218. if m.YOffset > m.maxYOffset() {
  219. m.GotoBottom()
  220. }
  221. m.memo.Invalidate()
  222. }
  223. // GetContent returns the entire content as a single string.
  224. // Line endings are normalized to '\n'.
  225. func (m Model) GetContent() string {
  226. return strings.Join(m.lines, "\n")
  227. }
  228. // calculateLine taking soft wrapping into account, returns the total viewable
  229. // lines and the real-line index for the given yoffset.
  230. func (m Model) calculateLine(yoffset int) (total, idx int) {
  231. if !m.SoftWrap {
  232. for i, line := range m.lines {
  233. adjust := max(1, lipgloss.Height(line))
  234. if yoffset >= total && yoffset < total+adjust {
  235. idx = i
  236. }
  237. total += adjust
  238. }
  239. if yoffset >= total {
  240. idx = len(m.lines)
  241. }
  242. return total, idx
  243. }
  244. maxWidth := m.maxWidth()
  245. var gutterSize int
  246. if m.LeftGutterFunc != nil {
  247. gutterSize = lipgloss.Width(m.LeftGutterFunc(GutterContext{}))
  248. }
  249. for i, line := range m.lines {
  250. adjust := max(1, lipgloss.Width(line)/(maxWidth-gutterSize))
  251. if yoffset >= total && yoffset < total+adjust {
  252. idx = i
  253. }
  254. total += adjust
  255. }
  256. if yoffset >= total {
  257. idx = len(m.lines)
  258. }
  259. return total, idx
  260. }
  261. // lineToIndex taking soft wrappign into account, return the real line index
  262. // for the given line.
  263. func (m Model) lineToIndex(y int) int {
  264. _, idx := m.calculateLine(y)
  265. return idx
  266. }
  267. // lineCount taking soft wrapping into account, return the total viewable line
  268. // count (real lines + soft wrapped line).
  269. func (m Model) lineCount() int {
  270. total, _ := m.calculateLine(0)
  271. return total
  272. }
  273. // maxYOffset returns the maximum possible value of the y-offset based on the
  274. // viewport's content and set height.
  275. func (m Model) maxYOffset() int {
  276. return max(0, m.lineCount()-m.Height()+m.Style.GetVerticalFrameSize())
  277. }
  278. // maxXOffset returns the maximum possible value of the x-offset based on the
  279. // viewport's content and set width.
  280. func (m Model) maxXOffset() int {
  281. return max(0, m.longestLineWidth-m.Width())
  282. }
  283. func (m Model) maxWidth() int {
  284. var gutterSize int
  285. if m.LeftGutterFunc != nil {
  286. gutterSize = lipgloss.Width(m.LeftGutterFunc(GutterContext{}))
  287. }
  288. return m.Width() -
  289. m.Style.GetHorizontalFrameSize() -
  290. gutterSize
  291. }
  292. func (m Model) maxHeight() int {
  293. return m.Height() - m.Style.GetVerticalFrameSize()
  294. }
  295. // visibleLines returns the lines that should currently be visible in the
  296. // viewport.
  297. func (m Model) visibleLines() (lines []string) {
  298. maxHeight := m.maxHeight()
  299. maxWidth := m.maxWidth()
  300. if m.lineCount() > 0 {
  301. pos := m.lineToIndex(m.YOffset)
  302. top := max(0, pos)
  303. bottom := clamp(pos+maxHeight, top, len(m.lines))
  304. lines = make([]string, bottom-top)
  305. copy(lines, m.lines[top:bottom])
  306. lines = m.styleLines(lines, top)
  307. lines = m.highlightLines(lines, top)
  308. }
  309. for m.FillHeight && len(lines) < maxHeight {
  310. lines = append(lines, "")
  311. }
  312. // if longest line fit within width, no need to do anything else.
  313. if (m.xOffset == 0 && m.longestLineWidth <= maxWidth) || maxWidth == 0 {
  314. return m.setupGutter(lines)
  315. }
  316. if m.SoftWrap {
  317. return m.softWrap(lines, maxWidth)
  318. }
  319. for i, line := range lines {
  320. sublines := strings.Split(line, "\n") // will only have more than 1 if caller used [Model.SetContentLines].
  321. for j := range sublines {
  322. sublines[j] = ansi.Cut(sublines[j], m.xOffset, m.xOffset+maxWidth)
  323. }
  324. lines[i] = strings.Join(sublines, "\n")
  325. }
  326. return m.setupGutter(lines)
  327. }
  328. // styleLines styles the lines using [Model.StyleLineFunc].
  329. func (m Model) styleLines(lines []string, offset int) []string {
  330. if m.StyleLineFunc == nil {
  331. return lines
  332. }
  333. for i := range lines {
  334. lines[i] = m.StyleLineFunc(i + offset).Render(lines[i])
  335. }
  336. return lines
  337. }
  338. // highlightLines highlights the lines with [Model.HighlightStyle] and
  339. // [Model.SelectedHighlightStyle].
  340. func (m Model) highlightLines(lines []string, offset int) []string {
  341. if len(m.highlights) == 0 {
  342. return lines
  343. }
  344. for i := range lines {
  345. ranges := makeHighlightRanges(
  346. m.highlights,
  347. i+offset,
  348. m.HighlightStyle,
  349. )
  350. lines[i] = lipgloss.StyleRanges(lines[i], ranges...)
  351. if m.hiIdx < 0 {
  352. continue
  353. }
  354. sel := m.highlights[m.hiIdx]
  355. if hi, ok := sel.lines[i+offset]; ok {
  356. lines[i] = lipgloss.StyleRanges(lines[i], lipgloss.NewRange(
  357. hi[0],
  358. hi[1],
  359. m.SelectedHighlightStyle,
  360. ))
  361. }
  362. }
  363. return lines
  364. }
  365. func (m Model) softWrap(lines []string, maxWidth int) []string {
  366. var wrappedLines []string
  367. total := m.TotalLineCount()
  368. for i, line := range lines {
  369. idx := 0
  370. for ansi.StringWidth(line) >= idx {
  371. truncatedLine := ansi.Cut(line, idx, maxWidth+idx)
  372. if m.LeftGutterFunc != nil {
  373. truncatedLine = m.LeftGutterFunc(GutterContext{
  374. Index: i + m.YOffset,
  375. TotalLines: total,
  376. Soft: idx > 0,
  377. }) + truncatedLine
  378. }
  379. wrappedLines = append(wrappedLines, truncatedLine)
  380. idx += maxWidth
  381. }
  382. }
  383. return wrappedLines
  384. }
  385. // setupGutter sets up the left gutter using [Moddel.LeftGutterFunc].
  386. func (m Model) setupGutter(lines []string) []string {
  387. if m.LeftGutterFunc == nil {
  388. return lines
  389. }
  390. offset := max(0, m.lineToIndex(m.YOffset))
  391. total := m.TotalLineCount()
  392. result := make([]string, len(lines))
  393. for i := range lines {
  394. var line []string
  395. for j, realLine := range strings.Split(lines[i], "\n") {
  396. line = append(line, m.LeftGutterFunc(GutterContext{
  397. Index: i + offset,
  398. TotalLines: total,
  399. Soft: j > 0,
  400. })+realLine)
  401. }
  402. result[i] = strings.Join(line, "\n")
  403. }
  404. m.memo.Invalidate()
  405. return result
  406. }
  407. // SetYOffset sets the Y offset.
  408. func (m *Model) SetYOffset(n int) {
  409. m.YOffset = clamp(n, 0, m.maxYOffset())
  410. m.memo.Invalidate()
  411. }
  412. // SetXOffset sets the X offset.
  413. // No-op when soft wrap is enabled.
  414. func (m *Model) SetXOffset(n int) {
  415. if m.SoftWrap {
  416. return
  417. }
  418. m.xOffset = clamp(n, 0, m.maxXOffset())
  419. m.memo.Invalidate()
  420. }
  421. // EnsureVisible ensures that the given line and column are in the viewport.
  422. func (m *Model) EnsureVisible(line, colstart, colend int) {
  423. maxWidth := m.maxWidth()
  424. if colend <= maxWidth {
  425. m.SetXOffset(0)
  426. } else {
  427. m.SetXOffset(colstart - m.horizontalStep) // put one step to the left, feels more natural
  428. }
  429. if line < m.YOffset || line >= m.YOffset+m.maxHeight() {
  430. m.SetYOffset(line)
  431. }
  432. m.visibleLines()
  433. }
  434. // ViewDown moves the view down by the number of lines in the viewport.
  435. // Basically, "page down".
  436. func (m *Model) ViewDown() {
  437. if m.AtBottom() {
  438. return
  439. }
  440. m.LineDown(m.Height())
  441. m.memo.Invalidate()
  442. }
  443. // ViewUp moves the view up by one height of the viewport. Basically, "page up".
  444. func (m *Model) ViewUp() {
  445. if m.AtTop() {
  446. return
  447. }
  448. m.LineUp(m.Height())
  449. m.memo.Invalidate()
  450. }
  451. // HalfViewDown moves the view down by half the height of the viewport.
  452. func (m *Model) HalfViewDown() {
  453. if m.AtBottom() {
  454. return
  455. }
  456. m.LineDown(m.Height() / 2) //nolint:mnd
  457. m.memo.Invalidate()
  458. }
  459. // HalfViewUp moves the view up by half the height of the viewport.
  460. func (m *Model) HalfViewUp() {
  461. if m.AtTop() {
  462. return
  463. }
  464. m.LineUp(m.Height() / 2) //nolint:mnd
  465. m.memo.Invalidate()
  466. }
  467. // LineDown moves the view down by the given number of lines.
  468. func (m *Model) LineDown(n int) {
  469. if m.AtBottom() || n == 0 || len(m.lines) == 0 {
  470. return
  471. }
  472. // Make sure the number of lines by which we're going to scroll isn't
  473. // greater than the number of lines we actually have left before we reach
  474. // the bottom.
  475. m.SetYOffset(m.YOffset + n)
  476. m.hiIdx = m.findNearedtMatch()
  477. m.memo.Invalidate()
  478. }
  479. // LineUp moves the view down by the given number of lines. Returns the new
  480. // lines to show.
  481. func (m *Model) LineUp(n int) {
  482. if m.AtTop() || n == 0 || len(m.lines) == 0 {
  483. return
  484. }
  485. // Make sure the number of lines by which we're going to scroll isn't
  486. // greater than the number of lines we are from the top.
  487. m.SetYOffset(m.YOffset - n)
  488. m.hiIdx = m.findNearedtMatch()
  489. m.memo.Invalidate()
  490. }
  491. // TotalLineCount returns the total number of lines (both hidden and visible) within the viewport.
  492. func (m Model) TotalLineCount() int {
  493. return m.lineCount()
  494. }
  495. // VisibleLineCount returns the number of the visible lines within the viewport.
  496. func (m Model) VisibleLineCount() int {
  497. return len(m.visibleLines())
  498. }
  499. // GotoTop sets the viewport to the top position.
  500. func (m *Model) GotoTop() (lines []string) {
  501. if m.AtTop() {
  502. return nil
  503. }
  504. m.SetYOffset(0)
  505. m.hiIdx = m.findNearedtMatch()
  506. m.memo.Invalidate()
  507. return m.visibleLines()
  508. }
  509. // GotoBottom sets the viewport to the bottom position.
  510. func (m *Model) GotoBottom() (lines []string) {
  511. m.SetYOffset(m.maxYOffset())
  512. m.hiIdx = m.findNearedtMatch()
  513. m.memo.Invalidate()
  514. return m.visibleLines()
  515. }
  516. // SetHorizontalStep sets the amount of cells that the viewport moves in the
  517. // default viewport keymapping. If set to 0 or less, horizontal scrolling is
  518. // disabled.
  519. func (m *Model) SetHorizontalStep(n int) {
  520. if n < 0 {
  521. n = 0
  522. }
  523. m.horizontalStep = n
  524. m.memo.Invalidate()
  525. }
  526. // MoveLeft moves the viewport to the left by the given number of columns.
  527. func (m *Model) MoveLeft(cols int) {
  528. m.xOffset -= cols
  529. if m.xOffset < 0 {
  530. m.xOffset = 0
  531. m.memo.Invalidate()
  532. }
  533. }
  534. // MoveRight moves viewport to the right by the given number of columns.
  535. func (m *Model) MoveRight(cols int) {
  536. // prevents over scrolling to the right
  537. w := m.maxWidth()
  538. if m.xOffset > m.longestLineWidth-w {
  539. return
  540. }
  541. m.xOffset += cols
  542. }
  543. // Resets lines indent to zero.
  544. func (m *Model) ResetIndent() {
  545. m.xOffset = 0
  546. m.memo.Invalidate()
  547. }
  548. // SetHighlights sets ranges of characters to highlight.
  549. // For instance, `[]int{[]int{2, 10}, []int{20, 30}}` will highlight characters
  550. // 2 to 10 and 20 to 30.
  551. // Note that highlights are not expected to transpose each other, and are also
  552. // expected to be in order.
  553. // Use [Model.SetHighlights] to set the highlight ranges, and
  554. // [Model.HighlightNext] and [Model.HighlightPrevious] to navigate.
  555. // Use [Model.ClearHighlights] to remove all highlights.
  556. func (m *Model) SetHighlights(matches [][]int) {
  557. if len(matches) == 0 || len(m.lines) == 0 {
  558. return
  559. }
  560. m.highlights = parseMatches(m.GetContent(), matches)
  561. m.hiIdx = m.findNearedtMatch()
  562. m.showHighlight()
  563. m.memo.Invalidate()
  564. }
  565. // ClearHighlights clears previously set highlights.
  566. func (m *Model) ClearHighlights() {
  567. m.highlights = nil
  568. m.hiIdx = -1
  569. m.memo.Invalidate()
  570. }
  571. func (m *Model) showHighlight() {
  572. if m.hiIdx == -1 {
  573. return
  574. }
  575. line, colstart, colend := m.highlights[m.hiIdx].coords()
  576. m.EnsureVisible(line, colstart, colend)
  577. m.memo.Invalidate()
  578. }
  579. // HighlightNext highlights the next match.
  580. func (m *Model) HighlightNext() {
  581. if m.highlights == nil {
  582. return
  583. }
  584. m.hiIdx = (m.hiIdx + 1) % len(m.highlights)
  585. m.showHighlight()
  586. m.memo.Invalidate()
  587. }
  588. // HighlightPrevious highlights the previous match.
  589. func (m *Model) HighlightPrevious() {
  590. if m.highlights == nil {
  591. return
  592. }
  593. m.hiIdx = (m.hiIdx - 1 + len(m.highlights)) % len(m.highlights)
  594. m.showHighlight()
  595. m.memo.Invalidate()
  596. }
  597. func (m Model) findNearedtMatch() int {
  598. for i, match := range m.highlights {
  599. if match.lineStart >= m.YOffset {
  600. return i
  601. }
  602. }
  603. return -1
  604. }
  605. // Update handles standard message-based viewport updates.
  606. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
  607. m = m.updateAsModel(msg)
  608. return m, nil
  609. }
  610. // Author's note: this method has been broken out to make it easier to
  611. // potentially transition Update to satisfy tea.Model.
  612. func (m Model) updateAsModel(msg tea.Msg) Model {
  613. if !m.initialized {
  614. m.setInitialValues()
  615. }
  616. switch msg := msg.(type) {
  617. case tea.KeyPressMsg:
  618. switch {
  619. case key.Matches(msg, m.KeyMap.PageDown):
  620. m.ViewDown()
  621. case key.Matches(msg, m.KeyMap.PageUp):
  622. m.ViewUp()
  623. case key.Matches(msg, m.KeyMap.HalfPageDown):
  624. m.HalfViewDown()
  625. case key.Matches(msg, m.KeyMap.HalfPageUp):
  626. m.HalfViewUp()
  627. case key.Matches(msg, m.KeyMap.Down):
  628. m.LineDown(1)
  629. case key.Matches(msg, m.KeyMap.Up):
  630. m.LineUp(1)
  631. case key.Matches(msg, m.KeyMap.Left):
  632. m.MoveLeft(m.horizontalStep)
  633. case key.Matches(msg, m.KeyMap.Right):
  634. m.MoveRight(m.horizontalStep)
  635. }
  636. case tea.MouseWheelMsg:
  637. if !m.MouseWheelEnabled {
  638. break
  639. }
  640. switch msg.Button {
  641. case tea.MouseWheelDown:
  642. m.LineDown(m.MouseWheelDelta)
  643. case tea.MouseWheelUp:
  644. m.LineUp(m.MouseWheelDelta)
  645. }
  646. }
  647. return m
  648. }
  649. // View renders the viewport into a string.
  650. func (m *Model) render() {
  651. }
  652. func (m Model) View() string {
  653. return m.memo.View(func() string {
  654. w, h := m.Width(), m.Height()
  655. if sw := m.Style.GetWidth(); sw != 0 {
  656. w = min(w, sw)
  657. }
  658. if sh := m.Style.GetHeight(); sh != 0 {
  659. h = min(h, sh)
  660. }
  661. contentWidth := w - m.Style.GetHorizontalFrameSize()
  662. contentHeight := h - m.Style.GetVerticalFrameSize()
  663. visible := m.visibleLines()
  664. contents := lipgloss.NewStyle().
  665. Width(contentWidth). // pad to width.
  666. Height(contentHeight). // pad to height.
  667. MaxHeight(contentHeight). // truncate height if taller.
  668. MaxWidth(contentWidth). // truncate width if wider.
  669. Render(strings.Join(visible, "\n"))
  670. return m.Style.
  671. UnsetWidth().UnsetHeight(). // Style size already applied in contents.
  672. Render(contents)
  673. })
  674. }
  675. func clamp(v, low, high int) int {
  676. if high < low {
  677. low, high = high, low
  678. }
  679. return min(high, max(low, v))
  680. }
  681. func maxLineWidth(lines []string) int {
  682. result := 0
  683. for _, line := range lines {
  684. result = max(result, lipgloss.Width(line))
  685. }
  686. return result
  687. }