1use crate::{
2 grid::{
3 ansi::ANSIBuf,
4 config::{
5 AlignmentHorizontal, AlignmentVertical, ColoredConfig, Entity, Offset, SpannedConfig,
6 },
7 dimension::{Dimension, Estimate},
8 records::{ExactRecords, Records},
9 util::string::get_text_width,
10 },
11 settings::{
12 object::{
13 Column, FirstColumn, FirstRow, LastColumn, LastColumnOffset, LastRow, LastRowOffset,
14 Object, Row,
15 },
16 Alignment, Color, TableOption,
17 },
18};
19
20#[derive(Debug)]
45pub struct LineText<Line> {
46 text: String,
48 offset: Offset,
49 color: Option<ANSIBuf>,
50 alignment: Option<Alignment>,
51 line: Line,
52}
53
54impl<Line> LineText<Line> {
55 pub fn new<S>(text: S, line: Line) -> Self
79 where
80 S: Into<String>,
81 {
82 LineText {
83 line,
84 text: text.into(),
85 offset: Offset::Start(0),
86 color: None,
87 alignment: None,
88 }
89 }
90
91 pub fn align(mut self, alignment: Alignment) -> Self {
110 self.alignment = Some(alignment);
111 self
112 }
113
114 pub fn offset(mut self, offset: impl Into<Offset>) -> Self {
132 self.offset = offset.into();
133 self
134 }
135
136 pub fn color(mut self, color: Color) -> Self {
155 self.color = Some(color.into());
156 self
157 }
158}
159
160impl<R, D> TableOption<R, ColoredConfig, D> for LineText<Row>
161where
162 R: Records + ExactRecords,
163 for<'a> &'a R: Records,
164 for<'a> D: Estimate<&'a R, ColoredConfig>,
165 D: Dimension,
166{
167 fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
168 let line = self.line.into();
169 change_horizontal_chars(records, dims, cfg, create_line(self, line))
170 }
171}
172
173impl<R, D> TableOption<R, ColoredConfig, D> for LineText<FirstRow>
174where
175 R: Records + ExactRecords,
176 for<'a> &'a R: Records,
177 for<'a> D: Estimate<&'a R, ColoredConfig>,
178 D: Dimension,
179{
180 fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
181 change_horizontal_chars(records, dims, cfg, create_line(self, 0))
182 }
183}
184
185impl<R, D> TableOption<R, ColoredConfig, D> for LineText<LastRow>
186where
187 R: Records + ExactRecords,
188 for<'a> &'a R: Records,
189 for<'a> D: Estimate<&'a R, ColoredConfig>,
190 D: Dimension,
191{
192 fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
193 let line = self.line.cells(records).next();
194 if let Some(Entity::Row(line)) = line {
195 change_horizontal_chars(records, dims, cfg, create_line(self, line))
196 }
197 }
198}
199
200impl<R, D> TableOption<R, ColoredConfig, D> for LineText<LastRowOffset>
201where
202 R: Records + ExactRecords,
203 for<'a> &'a R: Records,
204 for<'a> D: Estimate<&'a R, ColoredConfig>,
205 D: Dimension,
206{
207 fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
208 let line = self.line.cells(records).next();
209 if let Some(Entity::Row(line)) = line {
210 change_horizontal_chars(records, dims, cfg, create_line(self, line))
211 }
212 }
213}
214
215impl<R, D> TableOption<R, ColoredConfig, D> for LineText<Column>
216where
217 R: Records + ExactRecords,
218 for<'a> &'a R: Records,
219 for<'a> D: Estimate<&'a R, ColoredConfig>,
220 D: Dimension,
221{
222 fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
223 let line = self.line.into();
224 change_vertical_chars(records, dims, cfg, create_line(self, line))
225 }
226}
227
228impl<R, D> TableOption<R, ColoredConfig, D> for LineText<FirstColumn>
229where
230 R: Records + ExactRecords,
231 for<'a> &'a R: Records,
232 for<'a> D: Estimate<&'a R, ColoredConfig>,
233 D: Dimension,
234{
235 fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
236 change_vertical_chars(records, dims, cfg, create_line(self, 0))
237 }
238}
239
240impl<R, D> TableOption<R, ColoredConfig, D> for LineText<LastColumn>
241where
242 R: Records + ExactRecords,
243 for<'a> &'a R: Records,
244 for<'a> D: Estimate<&'a R, ColoredConfig>,
245 D: Dimension,
246{
247 fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
248 let line = self.line.cells(records).next();
249 if let Some(Entity::Column(line)) = line {
250 change_vertical_chars(records, dims, cfg, create_line(self, line))
251 }
252 }
253}
254
255impl<R, D> TableOption<R, ColoredConfig, D> for LineText<LastColumnOffset>
256where
257 R: Records + ExactRecords,
258 for<'a> &'a R: Records,
259 for<'a> D: Estimate<&'a R, ColoredConfig>,
260 D: Dimension,
261{
262 fn change(self, records: &mut R, cfg: &mut ColoredConfig, dims: &mut D) {
263 let line = self.line.cells(records).next();
264 if let Some(Entity::Column(line)) = line {
265 change_vertical_chars(records, dims, cfg, create_line(self, line))
266 }
267 }
268}
269
270fn set_horizontal_chars<D>(
271 cfg: &mut SpannedConfig,
272 dims: &D,
273 line: LineText<usize>,
274 shape: (usize, usize),
275) where
276 D: Dimension,
277{
278 let alignment = line.alignment.and_then(|a| a.as_horizontal());
279 let offset = line.offset;
280 let text = &line.text;
281 let color = &line.color;
282 let line = line.line;
283
284 let (_, count_columns) = shape;
285 let total_width = total_width(cfg, dims, count_columns);
286
287 let offset = match alignment {
288 Some(alignment) => {
289 let off = get_horizontal_alignment_offset(text, alignment, total_width);
290 offset_sum(off, offset)
291 }
292 None => offset,
293 };
294
295 let pos = get_start_pos(offset, total_width);
296
297 let pos = match pos {
298 Some(pos) => pos,
299 None => return,
300 };
301
302 let mut chars = text.chars();
303 let mut i = cfg.has_vertical(0, count_columns) as usize;
304 if i == 1 && pos == 0 {
305 let c = match chars.next() {
306 Some(c) => c,
307 None => return,
308 };
309
310 let mut b = cfg.get_border((line, 0).into(), shape);
311 b.left_top_corner = b.left_top_corner.map(|_| c);
312 cfg.set_border((line, 0).into(), b);
313
314 if let Some(color) = color.as_ref() {
315 let mut b = cfg.get_border_color((line, 0).into(), shape).cloned();
316 b.left_top_corner = Some(color.clone());
317 cfg.set_border_color((line, 0).into(), b);
318 }
319 }
320
321 for col in 0..count_columns {
322 let w = dims.get_width(col);
323 if i + w > pos {
324 for off in 0..w {
325 if i + off < pos {
326 continue;
327 }
328
329 let c = match chars.next() {
330 Some(c) => c,
331 None => return,
332 };
333
334 cfg.set_horizontal_char((line, col).into(), Offset::Start(off), c);
335 if let Some(color) = color.as_ref() {
336 cfg.set_horizontal_char_color(
337 (line, col).into(),
338 Offset::Start(off),
339 color.clone(),
340 );
341 }
342 }
343 }
344
345 i += w;
346
347 if cfg.has_vertical(col + 1, count_columns) {
348 i += 1;
349
350 if i > pos {
351 let c = match chars.next() {
352 Some(c) => c,
353 None => return,
354 };
355
356 let mut b = cfg.get_border((line, col).into(), shape);
357 b.right_top_corner = b.right_top_corner.map(|_| c);
358 cfg.set_border((line, col).into(), b);
359
360 if let Some(color) = color.as_ref() {
361 let mut b = cfg.get_border_color((line, col).into(), shape).cloned();
362 b.right_top_corner = Some(color.clone());
363 cfg.set_border_color((line, col).into(), b);
364 }
365 }
366 }
367 }
368}
369
370fn set_vertical_chars<D>(
371 cfg: &mut SpannedConfig,
372 dims: &D,
373 line: LineText<usize>,
374 shape: (usize, usize),
375) where
376 D: Dimension,
377{
378 let alignment = line.alignment.and_then(|a| a.as_vertical());
379 let offset = line.offset;
380 let text = &line.text;
381 let color = &line.color;
382 let line = line.line;
383
384 let (count_rows, _) = shape;
385 let total_width = total_height(cfg, dims, count_rows);
386
387 let offset = match alignment {
388 Some(alignment) => {
389 let off = get_vertical_alignment_offset(text, alignment, total_width);
390 offset_sum(off, offset)
391 }
392 None => offset,
393 };
394
395 let pos = get_start_pos(offset, total_width);
396
397 let pos = match pos {
398 Some(pos) => pos,
399 None => return,
400 };
401
402 let mut chars = text.chars();
403 let mut i = cfg.has_horizontal(0, count_rows) as usize;
404 if i == 1 && pos == 0 {
405 let c = match chars.next() {
406 Some(c) => c,
407 None => return,
408 };
409
410 let mut b = cfg.get_border((0, line).into(), shape);
411 b.left_top_corner = b.left_top_corner.map(|_| c);
412 cfg.set_border((0, line).into(), b);
413
414 if let Some(color) = color.as_ref() {
415 let mut b = cfg.get_border_color((0, line).into(), shape).cloned();
416 b.left_top_corner = Some(color.clone());
417 cfg.set_border_color((0, line).into(), b);
418 }
419 }
420
421 for row in 0..count_rows {
422 let row_height = dims.get_height(row);
423 if i + row_height > pos {
424 for off in 0..row_height {
425 if i + off < pos {
426 continue;
427 }
428
429 let c = match chars.next() {
430 Some(c) => c,
431 None => return,
432 };
433
434 cfg.set_vertical_char((row, line).into(), Offset::Start(off), c); if let Some(color) = color.as_ref() {
437 cfg.set_vertical_char_color(
438 (row, line).into(),
439 Offset::Start(off),
440 color.clone(),
441 );
442 }
443 }
444 }
445
446 i += row_height;
447
448 if cfg.has_horizontal(row + 1, count_rows) {
449 i += 1;
450
451 if i > pos {
452 let c = match chars.next() {
453 Some(c) => c,
454 None => return,
455 };
456
457 let mut b = cfg.get_border((row, line).into(), shape);
458 b.left_bottom_corner = b.left_bottom_corner.map(|_| c);
459 cfg.set_border((row, line).into(), b);
460
461 if let Some(color) = color.as_ref() {
462 let mut b = cfg.get_border_color((row, line).into(), shape).cloned();
463 b.left_bottom_corner = Some(color.clone());
464 cfg.set_border_color((row, line).into(), b);
465 }
466 }
467 }
468 }
469}
470
471fn get_start_pos(offset: Offset, total: usize) -> Option<usize> {
472 match offset {
473 Offset::Start(i) => {
474 if i > total {
475 None
476 } else {
477 Some(i)
478 }
479 }
480 Offset::End(i) => {
481 if i > total {
482 None
483 } else {
484 Some(total - i)
485 }
486 }
487 }
488}
489
490fn get_horizontal_alignment_offset(
491 text: &str,
492 alignment: AlignmentHorizontal,
493 total: usize,
494) -> Offset {
495 match alignment {
496 AlignmentHorizontal::Center => {
497 let width = get_text_width(text);
498 let mut off = 0;
499 if total > width {
500 let center = total / 2;
501 let text_center = width / 2;
502 off = center.saturating_sub(text_center);
503 }
504
505 Offset::Start(off)
506 }
507 AlignmentHorizontal::Left => Offset::Start(0),
508 AlignmentHorizontal::Right => {
509 let width = get_text_width(text);
510 Offset::End(width)
511 }
512 }
513}
514
515fn get_vertical_alignment_offset(text: &str, alignment: AlignmentVertical, total: usize) -> Offset {
516 match alignment {
517 AlignmentVertical::Center => {
518 let width = get_text_width(text);
519 let mut off = 0;
520 if total > width {
521 let center = total / 2;
522 let text_center = width / 2;
523 off = center.saturating_sub(text_center);
524 }
525
526 Offset::Start(off)
527 }
528 AlignmentVertical::Top => Offset::Start(0),
529 AlignmentVertical::Bottom => Offset::End(0),
530 }
531}
532
533fn offset_sum(orig: Offset, and: Offset) -> Offset {
534 match (orig, and) {
535 (Offset::Start(a), Offset::Start(b)) => Offset::Start(a + b),
536 (Offset::Start(a), Offset::End(b)) => Offset::Start(a.saturating_sub(b)),
537 (Offset::End(a), Offset::Start(b)) => Offset::End(a + b),
538 (Offset::End(a), Offset::End(b)) => Offset::End(a.saturating_sub(b)),
539 }
540}
541
542fn total_width<D>(cfg: &SpannedConfig, dims: &D, count_columns: usize) -> usize
544where
545 D: Dimension,
546{
547 let mut total = cfg.has_vertical(count_columns, count_columns) as usize;
548 for col in 0..count_columns {
549 total += dims.get_width(col);
550 total += cfg.has_vertical(col, count_columns) as usize;
551 }
552
553 total
554}
555
556fn total_height<D>(cfg: &SpannedConfig, dims: &D, count_rows: usize) -> usize
557where
558 D: Dimension,
559{
560 let mut total = cfg.has_horizontal(count_rows, count_rows) as usize;
561 for row in 0..count_rows {
562 total += dims.get_height(row);
563 total += cfg.has_horizontal(row, count_rows) as usize;
564 }
565
566 total
567}
568
569fn change_horizontal_chars<R, D>(
570 records: &mut R,
571 dims: &mut D,
572 cfg: &mut ColoredConfig,
573 line: LineText<usize>,
574) where
575 R: Records + ExactRecords,
576 for<'a> D: Estimate<&'a R, ColoredConfig>,
577 D: Dimension,
578{
579 dims.estimate(records, cfg);
580 let shape = (records.count_rows(), records.count_columns());
581 set_horizontal_chars(cfg, dims, line, shape);
582}
583
584fn change_vertical_chars<R, D>(
585 records: &mut R,
586 dims: &mut D,
587 cfg: &mut ColoredConfig,
588 line: LineText<usize>,
589) where
590 R: Records + ExactRecords,
591 for<'a> D: Estimate<&'a R, ColoredConfig>,
592 D: Dimension,
593{
594 dims.estimate(records, cfg);
595 let shape = (records.count_rows(), records.count_columns());
596 set_vertical_chars(cfg, dims, line, shape);
597}
598
599fn create_line<T>(orig: LineText<T>, line: usize) -> LineText<usize> {
600 LineText {
601 text: orig.text,
602 offset: orig.offset,
603 color: orig.color,
604 alignment: orig.alignment,
605 line,
606 }
607}