1 // -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*-
2 // (c) 2020 Henner Zeller <h.zeller@acm.org>
3 //
4 // This program is free software; you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation version 2.
7 //
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program.  If not, see <http://gnu.org/licenses/gpl-2.0.txt>
15 
16 #include "renderer.h"
17 
18 #include <string.h>
19 #include <unistd.h>
20 
21 namespace timg {
TrimTitle(const char * title,int requested_width)22 std::string Renderer::TrimTitle(const char *title, int requested_width) {
23     std::string result = title;
24     // Columns can be too narrow. We might need to trim what we print.
25     if ((int)result.length() > requested_width) {
26         result.replace(0, result.length() - requested_width + 3, "...");
27     } else if (options_.center_horizontally) {
28         const int start_spaces = (requested_width - result.length()) / 2;
29         result.insert(0, std::string(start_spaces, ' '));
30     }
31     result += "\n";
32     return result;
33 }
34 
35 namespace {
36 // Use the full canvas to display framebuffer; writes framebuffer directly.
37 class SingleColumnRenderer final : public Renderer {
38 public:
SingleColumnRenderer(timg::TerminalCanvas * canvas,const DisplayOptions & display_opts)39     SingleColumnRenderer(timg::TerminalCanvas *canvas,
40                          const DisplayOptions &display_opts)
41         : Renderer(canvas, display_opts) {}
42 
render_cb(const char * title)43     WriteFramebufferFun render_cb(const char *title) final {
44         // For single column mode, implementation is straightforward
45         RenderTitle(title);
46         return [this](int x, int dy, const Framebuffer &fb,
47                       SeqType seq_type, Duration end_of_frame) {
48             canvas_->Send(x, dy, fb, seq_type, end_of_frame);
49         };
50     }
51 
52 private:
RenderTitle(const char * title)53     void RenderTitle(const char *title) {
54         if (!title || !options_.show_filename) return;
55         const std::string tout = TrimTitle(
56             title, options_.width / options_.cell_x_px);
57         canvas_->AddPrefixNextSend(tout.data(), tout.size());
58     }
59 };
60 
61 // The multi column renderer positions every update in a new column.
62 // It keeps track which column it is in and if a new row needs to be started
63 // and uses cursor movements to get to the right place.
64 class MultiColumnRenderer final : public Renderer {
65 public:
MultiColumnRenderer(timg::TerminalCanvas * canvas,const DisplayOptions & display_opts,int cols,int rows)66     MultiColumnRenderer(timg::TerminalCanvas *canvas,
67                         const DisplayOptions &display_opts,
68                         int cols, int rows)
69         : Renderer(canvas, display_opts),
70           columns_(cols), column_width_(display_opts.width) {
71     }
72 
~MultiColumnRenderer()73     ~MultiColumnRenderer() final {
74         if (current_column_ != 0) {
75             const int down = highest_fb_column_height_ - last_fb_height_;
76             if (down > 0) {
77                 canvas_->MoveCursorDY(down / options_.cell_y_px);
78             }
79         }
80     }
81 
render_cb(const char * title)82     WriteFramebufferFun render_cb(const char *title) final {
83         ++current_column_;
84         if (current_column_ >= columns_) {
85             // If our current image is shorter than the previous one,
86             // we need to make up the difference to be ready for the next
87             const int down = highest_fb_column_height_ - last_fb_height_;
88             if (down > 0) canvas_->MoveCursorDY(down);
89             current_column_ = 0;
90             highest_fb_column_height_ = 0;
91         }
92 
93         PrepareTitle(title);
94         first_render_call_ = true;
95         return [this](int x, int dy, const Framebuffer &fb,
96                       SeqType seq_type, Duration end_of_frame) {
97             const int x_offset = current_column_ * column_width_;
98             int y_offset;
99             if (first_render_call_) {
100                 // Unless we're in the first column, we've to move up from last
101                 y_offset = current_column_ > 0 ? -last_fb_height_ : 0;
102             } else {
103                 y_offset = dy;
104             }
105             if (options_.show_filename && first_render_call_) {
106                 // If we have to show the headline, we've to do the move up
107                 // before Send() would do it.
108                 // We need to move one more up to be in the 'headline' ypos
109                 if (y_offset) {
110                     // Move pixels needed for cell, then one more
111                     // then one line more to reach the place of the title.
112                     const int y_move = (-y_offset + options_.cell_y_px - 1) /
113                         options_.cell_y_px;
114                     const int kSpaceForTitle = 1;
115                     canvas_->MoveCursorDY(-y_move - kSpaceForTitle);
116                 }
117                 canvas_->MoveCursorDX(x_offset / options_.cell_x_px);
118                 canvas_->AddPrefixNextSend(title_.c_str(), title_.length());
119                 y_offset = 0;  // No move by Send() needed anymore.
120             }
121 
122             canvas_->Send(x + x_offset, y_offset, fb, seq_type, end_of_frame);
123             last_fb_height_ = fb.height();
124             if (last_fb_height_ > highest_fb_column_height_)
125                 highest_fb_column_height_ = last_fb_height_;
126             first_render_call_ = false;
127         };
128     }
129 
130 private:
PrepareTitle(const char * title)131     void PrepareTitle(const char *title) {
132         if (!title || !options_.show_filename) return;
133         title_ = TrimTitle(title, column_width_ / options_.cell_x_px);
134     }
135 
136     const int columns_;
137     const int column_width_;
138     std::string title_;
139     bool first_render_call_ = true;
140     int current_column_ = -1;
141     int highest_fb_column_height_ = 0;  // maximum seen in this row
142     int last_fb_height_ = 0;
143 };
144 
145 }  // namespace
146 
Renderer(timg::TerminalCanvas * canvas,const DisplayOptions & display_opts)147 Renderer::Renderer(timg::TerminalCanvas *canvas,
148              const DisplayOptions &display_opts)
149     : canvas_(canvas), options_(display_opts) {}
150 
Create(timg::TerminalCanvas * output,const DisplayOptions & display_opts,int cols,int rows)151 std::unique_ptr<Renderer> Renderer::Create(timg::TerminalCanvas *output,
152                                            const DisplayOptions &display_opts,
153                                            int cols, int rows) {
154     if (cols > 1) {
155         return std::make_unique<MultiColumnRenderer>(output, display_opts,
156                                                      cols, rows);
157     }
158     return std::make_unique<SingleColumnRenderer>(output, display_opts);
159 }
160 
161 }
162