cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / sheet / clipfmt.go
clipfmt.go 10.3K
1package sheet23import (4 "maps"5 "slices"6)78// Formats travel with copy, cut and move, as in Sheets. A copy takes the9// formatting its range shows, whatever it comes from (the cell, its row,10// its column, the sheet), and a paste gives each destination cell what11// its source showed, storing only what the destination's own lines don't12// already show. Copying whole columns or rows and pasting them as whole13// lines (at the top of a column, the start of a row, or A1 for the whole14// sheet) carries the line formats themselves, and rows their heights,15// and cutting them moves the line formats and heights, leaving the16// source lines plain. Cutting a block leaves its cells plain, over17// whatever lines cross it. Merged cells wholly inside a copy are merged18// where it lands (merge.go).1920// clipFormats is the formatting of a copied range, taken at copy time.21type clipFormats struct {22 cols, rows bool // whole columns, whole rows were copied (both: the sheet)23 cells map[Addr]lineFmt // what each stored cell showed, by offset24 colFmt, rowFmt map[int]lineFmt // formatted columns and rows crossing the range, by offset25 sheet lineFmt26 heights map[int]int // whole rows' heights, by offset27}2829// copyFormats takes the formatting of r.30func (s *Sheet) copyFormats(r Rect) clipFormats {31 f := clipFormats{cols: r.AllRows(), rows: r.AllCols(), cells: map[Addr]lineFmt{}, sheet: s.lines.sheet}32 for _, a := range s.cellsIn(r) {33 f.cells[Addr{Col: a.Col - r.From.Col, Row: a.Row - r.From.Row}] = s.effective(a)34 }35 f.colFmt = linesIn(s.lines.cols, r.From.Col, r.To.Col)36 f.rowFmt = linesIn(s.lines.rows, r.From.Row, r.To.Row)37 if f.rows {38 f.heights = s.heightsIn(r.From.Row, r.To.Row)39 }40 return f41}4243// linesIn is the lines of m from lo to hi, by offset from lo.44func linesIn(m map[int]lineFmt, lo, hi int) map[int]lineFmt {45 out := map[int]lineFmt{}46 for n, l := range m {47 if n >= lo && n <= hi {48 out[n-lo] = l49 }50 }51 return out52}5354// mode is how the formats land: on cells, or on whole lines.55func (f *clipFormats) mode() lineMode {56 switch {57 case f.cols && f.rows:58 return lineAll59 case f.cols:60 return lineCols61 case f.rows:62 return lineRows63 }64 return lineBlock65}6667// lined reports whether any line format crosses the copied range.68func (f *clipFormats) lined() bool {69 return len(f.colFmt) > 0 || len(f.rowFmt) > 0 || !f.sheet.IsZero()70}7172// want is what the cell at offset off of the copy showed.73func (f *clipFormats) want(off Addr) lineFmt {74 if l, ok := f.cells[off]; ok {75 return l76 }77 return fallback(f.rowFmt[off.Row], f.colFmt[off.Col], f.sheet)78}7980// carry is the line format a pasted column or row gets for l, the81// source's, filling what it leaves to the source sheet's format where82// the destination sheet's differs.83func (f *clipFormats) carry(l, dstSheet lineFmt) lineFmt {84 if l.Format.IsZero() && f.sheet.Format != dstSheet.Format {85 l.Format = f.sheet.Format86 }87 if l.Style.IsZero() && f.sheet.Style != dstSheet.Style {88 l.Style = f.sheet.Style89 }90 return l91}9293// lineMode is whether a paste or move lands on cells or on whole lines.94type lineMode int9596const (97 lineBlock lineMode = iota98 lineCols99 lineRows100 lineAll101)102103// pasteLayout is where a paste lands: dst, tiled by the clip's size,104// across by down times.105type pasteLayout struct {106 mode lineMode107 dst Rect108 tw, th int // a tile's size; a whole line's length along it109 across, down int110}111112// off is the offset within the copy that the cell at a receives.113func (p pasteLayout) off(a Addr) Addr {114 return Addr{Col: (a.Col - p.dst.From.Col) % p.tw, Row: (a.Row - p.dst.From.Row) % p.th}115}116117// layout is where pasting c into dst lands. Like Sheets, a destination118// that is an exact multiple of the clip's size is tiled; otherwise the119// clip is pasted once at dst's top-left corner. Whole lines pasted at120// the start of a line land on whole lines.121func (c *Clip) layout(dst Rect) (pasteLayout, error) {122 cols, rows := c.Size()123 p := pasteLayout{mode: c.formats.mode(), tw: cols, th: rows, across: 1, down: 1}124 switch {125 case p.mode == lineAll && dst.From == Addr{}:126 p.tw, p.th = MaxCols, MaxRows127 case p.mode == lineCols && dst.From.Row == 0:128 p.th = MaxRows129 case p.mode == lineRows && dst.From.Col == 0:130 p.tw = MaxCols131 default:132 p.mode = lineBlock133 }134 w, h := dst.To.Col-dst.From.Col+1, dst.To.Row-dst.From.Row+1135 switch p.mode {136 case lineBlock:137 if w%cols == 0 && h%rows == 0 {138 p.across, p.down = w/cols, h/rows139 }140 case lineCols:141 if w%cols == 0 {142 p.across = w / cols143 }144 case lineRows:145 if h%rows == 0 {146 p.down = h / rows147 }148 }149 p.dst = Rect{From: dst.From, To: Addr{Col: dst.From.Col + p.across*p.tw - 1, Row: dst.From.Row + p.down*p.th - 1}}150 if !p.dst.To.Valid() {151 return pasteLayout{}, ErrPasteEdge152 }153 return p, nil154}155156// MoveRange is the range cutting c moves when pasted at to: the whole157// lines copied when to starts a line, else the copied cells.158func (c *Clip) MoveRange(to Addr) Rect {159 if p, err := c.layout(Rect{From: to, To: to}); err == nil && p.mode != lineBlock {160 return c.Range161 }162 return c.Src163}164165// pasteFormats has the cells p covers show what the copy f showed: whole166// lines take its line formats, and cells whose lines don't show what167// their source did get formatting of their own.168func (s *Sheet) pasteFormats(f *clipFormats, p pasteLayout) {169 s.pasteLines(f, p)170 for _, a := range s.formatTargets(f, p) {171 s.placeFormat(a, f.want(p.off(a)))172 }173}174175// pasteLines gives the whole lines p covers the copy's line formats.176func (s *Sheet) pasteLines(f *clipFormats, p pasteLayout) {177 switch p.mode {178 case lineAll:179 s.setLine(false, wholeSheet, f.sheet)180 for _, n := range unionKeys(s.lines.cols, f.colFmt) {181 s.setLine(false, n, f.colFmt[n])182 }183 for _, n := range unionKeys(s.lines.rows, f.rowFmt) {184 s.setLine(true, n, f.rowFmt[n])185 }186 for _, n := range slices.Collect(maps.Keys(s.heights)) {187 s.setHeight(n, 0)188 }189 for n, h := range f.heights {190 s.setHeight(n, h)191 }192 case lineCols:193 for i := range p.across * p.tw {194 s.setLine(false, p.dst.From.Col+i, f.carry(f.colFmt[i%p.tw], s.lines.sheet))195 }196 case lineRows:197 for i := range p.down * p.th {198 s.setLine(true, p.dst.From.Row+i, f.carry(f.rowFmt[i%p.th], s.lines.sheet))199 }200 if len(f.heights) > 0 || len(s.heights) > 0 {201 for i := range p.down * p.th {202 s.setHeight(p.dst.From.Row+i, f.heights[i%p.th])203 }204 }205 }206}207208// formatTargets are the cells of p.dst that may need formatting of their209// own: those stored, and where a formatted line crosses the pasted lines210// or, for a block, every cell when lines are formatted on either side,211// up to materializeLimit cells made.212func (s *Sheet) formatTargets(f *clipFormats, p pasteLayout) []Addr {213 targets := s.cellsIn(p.dst)214 for _, r := range s.formatAreas(f, p) {215 for row := r.From.Row; row <= r.To.Row; row++ {216 for c := r.From.Col; c <= r.To.Col; c++ {217 if len(targets) > materializeLimit {218 return targets219 }220 if a := (Addr{Col: c, Row: row}); !s.cells.has(a) {221 targets = append(targets, a)222 }223 }224 }225 }226 return targets227}228229// formatAreas are the parts of p.dst whose blank cells may show other230// than their source: where formatted rows cross pasted columns, formatted231// columns cross pasted rows, or a whole block when lines are formatted232// on either side and it is small enough.233func (s *Sheet) formatAreas(f *clipFormats, p pasteLayout) []Rect {234 d := p.dst235 var out []Rect236 switch p.mode {237 case lineCols:238 for _, n := range unionKeys(s.lines.rows, f.rowFmt) {239 out = append(out, Rect{From: Addr{Col: d.From.Col, Row: n}, To: Addr{Col: d.To.Col, Row: n}})240 }241 case lineRows:242 for _, n := range unionKeys(s.lines.cols, f.colFmt) {243 out = append(out, Rect{From: Addr{Col: n, Row: d.From.Row}, To: Addr{Col: n, Row: d.To.Row}})244 }245 case lineBlock:246 area := (d.To.Col - d.From.Col + 1) * (d.To.Row - d.From.Row + 1)247 if area <= materializeLimit && (f.lined() || !s.lines.none()) {248 out = append(out, d)249 }250 }251 return out252}253254// moveFormats gives dst on s, where the cells of src on sheet from land,255// the formatting f they showed at src; whole lines take their line256// formats along, leaving the source lines plain.257func (s *Sheet) moveFormats(from *Sheet, f *clipFormats, src, dst Rect) {258 p := pasteLayout{mode: f.mode(), dst: dst, across: 1, down: 1,259 tw: src.To.Col - src.From.Col + 1, th: src.To.Row - src.From.Row + 1}260 switch p.mode {261 case lineAll:262 from.setLine(false, wholeSheet, lineFmt{})263 for _, n := range slices.Sorted(maps.Keys(from.lines.cols)) {264 from.setLine(false, n, lineFmt{})265 }266 for _, n := range slices.Sorted(maps.Keys(from.lines.rows)) {267 from.setLine(true, n, lineFmt{})268 }269 for n := range from.heights {270 from.setHeight(n, 0)271 }272 case lineCols:273 for n := range f.colFmt {274 from.setLine(false, src.From.Col+n, lineFmt{})275 }276 case lineRows:277 for n := range f.rowFmt {278 from.setLine(true, src.From.Row+n, lineFmt{})279 }280 for n := range f.heights {281 from.setHeight(src.From.Row+n, 0)282 }283 case lineBlock:284 from.plainSource(src, dst, from == s)285 }286 s.pasteFormats(f, p)287}288289// plainSource leaves the cells a block was cut from showing plain, as290// Sheets clears them: where a formatted row, column or the sheet crosses291// src, its blank cells get plain formatting of their own, up to292// materializeLimit cells looked at, except those in dst when the block293// lands on the same sheet.294func (s *Sheet) plainSource(src, dst Rect, same bool) {295 var areas []Rect296 if !s.lines.sheet.IsZero() {297 areas = []Rect{src}298 } else {299 for n := range linesIn(s.lines.rows, src.From.Row, src.To.Row) {300 row := src.From.Row + n301 areas = append(areas, Rect{From: Addr{Col: src.From.Col, Row: row}, To: Addr{Col: src.To.Col, Row: row}})302 }303 for n := range linesIn(s.lines.cols, src.From.Col, src.To.Col) {304 col := src.From.Col + n305 areas = append(areas, Rect{From: Addr{Col: col, Row: src.From.Row}, To: Addr{Col: col, Row: src.To.Row}})306 }307 }308 seen := 0309 for _, r := range areas {310 for row := r.From.Row; row <= r.To.Row; row++ {311 for c := r.From.Col; c <= r.To.Col; c++ {312 if seen++; seen > materializeLimit {313 return314 }315 if a := (Addr{Col: c, Row: row}); !(same && dst.Contains(a)) && !s.cells.has(a) {316 s.placeFormat(a, lineFmt{})317 }318 }319 }320 }321}322323// unionKeys are the keys of a and b, in order.324func unionKeys(a, b map[int]lineFmt) []int {325 keys := slices.Collect(maps.Keys(a))326 for n := range b {327 if _, ok := a[n]; !ok {328 keys = append(keys, n)329 }330 }331 slices.Sort(keys)332 return keys333}