1use ratatui::layout::{Alignment, Rect};
5use ratatui::style::{Color, Modifier, Style};
6use ratatui::widgets::{Block, Borders, Clear, Paragraph};
7use ratatui::Frame;
8
9use crate::app::{App, WDrag};
10use crate::geometry::{contains, hit, map_rect, rects_for};
11
12impl App {
13 pub fn draw(&self, f: &mut Frame) {
14 let full = f.area();
15 self.last_full.set(full);
16 let canvas = Rect {
19 x: full.x,
20 y: full.y + 1,
21 width: full.width,
22 height: full.height.saturating_sub(4),
23 };
24 self.last_canvas.set(canvas);
25
26 f.render_widget(Block::default().style(Style::default().bg(Color::Black)), full);
27
28 if self.window_mode {
29 self.draw_windows(f, canvas);
30 } else {
31 self.draw_panes(f, canvas);
32 }
33
34 self.draw_chrome(f, full);
35 }
36
37 fn draw_panes(&self, f: &mut Frame, canvas: Rect) {
38 let rects = rects_for(&self.panes, canvas, self.win_w, self.win_h);
39 let hover = self.drag_src.and_then(|_| hit(&rects, self.cursor.0, self.cursor.1));
40 let sel_idx = self.selected_index();
41
42 for (i, r) in &rects {
43 let p = &self.panes[*i];
44 let is_src = Some(*i) == self.drag_src;
45 let is_hover = Some(*i) == hover && Some(*i) != self.drag_src;
46 let is_sel = Some(*i) == sel_idx;
47 let is_kill =
48 self.pending_kill.as_ref().is_some_and(|(id, is_win)| !*is_win && id == &p.id);
49
50 let border_style = if is_kill {
51 Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
52 } else if is_src {
53 Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
54 } else if is_hover {
55 Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
56 } else if is_sel {
57 Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)
58 } else if p.active {
59 Style::default().fg(Color::Cyan)
60 } else {
61 Style::default().fg(Color::DarkGray)
62 };
63
64 let mut block = Block::default()
65 .borders(Borders::ALL)
66 .border_style(border_style)
67 .title(format!(" {}:{} ", p.index, p.cmd));
68 if is_hover {
69 block = block.style(Style::default().bg(Color::Rgb(20, 60, 20)));
70 }
71 f.render_widget(block, *r);
72
73 if r.height >= 3 && r.width >= 6 {
74 let label = if is_kill {
75 "kill?"
76 } else if is_src {
77 "↕ moving"
78 } else if is_hover {
79 "drop here"
80 } else if is_sel {
81 "● selected"
82 } else {
83 p.id.as_str()
84 };
85 let inner = Rect {
86 x: r.x + 1,
87 y: r.y + r.height / 2,
88 width: r.width.saturating_sub(2),
89 height: 1,
90 };
91 f.render_widget(Paragraph::new(label).alignment(Alignment::Center), inner);
92 }
93 }
94 }
95
96 fn draw_windows(&self, f: &mut Frame, canvas: Rect) {
97 let mut tiles = self.tiles.borrow_mut();
98 let mut mpanes = self.mpanes.borrow_mut();
99 tiles.clear();
100 mpanes.clear();
101
102 let n = self.windows.len() as u16;
103 if n == 0 {
104 return;
105 }
106 let cols = ((n as f32).sqrt().ceil() as u16).max(1);
107 let rows = n.div_ceil(cols);
108 let cell_w = (canvas.width / cols).max(3);
109 let cell_h = (canvas.height / rows.max(1)).max(3);
110
111 let mut tr: Vec<Rect> = Vec::with_capacity(self.windows.len());
113 for i in 0..self.windows.len() {
114 let ci = i as u16 % cols;
115 let ri = i as u16 / cols;
116 tr.push(Rect {
117 x: canvas.x + ci * cell_w + 1,
118 y: canvas.y + ri * cell_h,
119 width: cell_w.saturating_sub(2).max(2),
120 height: cell_h.saturating_sub(1).max(2),
121 });
122 }
123
124 let drag_win = match &self.wdrag {
125 Some(WDrag::Win(id)) => Some(id.clone()),
126 _ => None,
127 };
128 let drag_pane = match &self.wdrag {
129 Some(WDrag::Pane { pane, .. }) => Some(pane.clone()),
130 _ => None,
131 };
132 let hover_win = if self.wdrag.is_some() {
133 tr.iter()
134 .position(|r| contains(*r, self.cursor.0, self.cursor.1))
135 .map(|i| self.windows[i].id.clone())
136 } else {
137 None
138 };
139
140 for (i, tile) in tr.iter().enumerate() {
141 let w = &self.windows[i];
142 let is_drag = drag_win.as_deref() == Some(w.id.as_str());
143 let is_hover = hover_win.as_deref() == Some(w.id.as_str()) && !is_drag;
144 let is_sel = self.sel_win.as_deref() == Some(w.id.as_str());
145 let border = if is_drag {
146 Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
147 } else if is_hover {
148 Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
149 } else if is_sel {
150 Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)
151 } else if w.active {
152 Style::default().fg(Color::Cyan)
153 } else {
154 Style::default().fg(Color::DarkGray)
155 };
156 let mut block = Block::default()
157 .borders(Borders::ALL)
158 .border_style(border)
159 .title(format!(" {}:{} ", w.index, w.name));
160 if is_hover {
161 block = block.style(Style::default().bg(Color::Rgb(20, 60, 20)));
162 }
163 f.render_widget(block, *tile);
164 tiles.push((w.id.clone(), *tile));
165
166 let inner = Rect {
167 x: tile.x + 1,
168 y: tile.y + 1,
169 width: tile.width.saturating_sub(2),
170 height: tile.height.saturating_sub(2),
171 };
172 if inner.width < 2 || inner.height < 1 {
173 continue;
174 }
175 for p in &w.panes {
176 let mr = map_rect(p, inner, w.w, w.h);
177 let pstyle = if drag_pane.as_deref() == Some(p.id.as_str()) {
178 Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
179 } else if p.active {
180 Style::default().fg(Color::Cyan)
181 } else {
182 Style::default().fg(Color::DarkGray)
183 };
184 f.render_widget(Block::default().borders(Borders::ALL).border_style(pstyle), mr);
185 mpanes.push((w.id.clone(), p.id.clone(), mr));
186 }
187 }
188 }
189
190 fn draw_chrome(&self, f: &mut Frame, full: Rect) {
191 f.render_widget(
193 Paragraph::new(format!(" {}", self.status))
194 .style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
195 Rect { x: full.x, y: full.bottom().saturating_sub(2), width: full.width, height: 1 },
196 );
197
198 let keys = if self.window_mode {
200 " arrows = navigate+switch · Enter = enter window · drag/⇧arrows = reorder · drag pane → window = move · right-click/R = rename · n · x p panes · ?"
201 } else {
202 " drag swap · arrows select · ⇧arrows move · |/- split · x kill · R rename layout 1/2/3/4/5 · ␣ cycle w windows · ? help · q quit"
203 };
204 f.render_widget(
205 Paragraph::new(keys).style(Style::default().fg(Color::Gray)),
206 Rect { x: full.x, y: full.bottom().saturating_sub(1), width: full.width, height: 1 },
207 );
208
209 let bar = Rect { x: full.x, y: full.y, width: full.width, height: 1 };
212 f.render_widget(Block::default().style(Style::default().bg(Color::Rgb(30, 30, 30))), bar);
213 if let Some(label) = self.active_window_label() {
214 f.render_widget(
215 Paragraph::new(label)
216 .alignment(Alignment::Center)
217 .style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
218 bar,
219 );
220 }
221
222 f.render_widget(
224 Paragraph::new("[ quit ]").style(
225 Style::default().fg(Color::White).bg(Color::Red).add_modifier(Modifier::BOLD),
226 ),
227 self.quit_button_rect(),
228 );
229 let mode_label = if self.window_mode { "[ panes ] " } else { "[ windows ]" };
230 f.render_widget(
231 Paragraph::new(mode_label).style(
232 Style::default().fg(Color::White).bg(Color::Blue).add_modifier(Modifier::BOLD),
233 ),
234 self.mode_button_rect(),
235 );
236 f.render_widget(
237 Paragraph::new("[ ? help ]").style(
238 Style::default().fg(Color::Black).bg(Color::Yellow).add_modifier(Modifier::BOLD),
239 ),
240 self.help_button_rect(),
241 );
242
243 self.draw_menu(f);
244 self.draw_help(f);
245 self.draw_confirm(f);
246 self.draw_prompt(f);
247 }
248
249 fn draw_menu(&self, f: &mut Frame) {
250 let Some(menu) = &self.menu else { return };
251 let area = self.menu_rect(menu);
252 f.render_widget(Clear, area);
253 f.render_widget(
254 Block::default()
255 .borders(Borders::ALL)
256 .border_style(Style::default().fg(Color::White))
257 .title(format!(" {} ", menu.title)),
258 area,
259 );
260 for (i, (label, _)) in menu.items.iter().enumerate() {
261 if (i as u16) + 1 >= area.height.saturating_sub(1) {
262 break;
263 }
264 let row = Rect {
265 x: area.x + 1,
266 y: area.y + 1 + i as u16,
267 width: area.width.saturating_sub(2),
268 height: 1,
269 };
270 let style = if i == menu.highlight {
271 Style::default().fg(Color::Black).bg(Color::White)
272 } else {
273 Style::default().fg(Color::Gray)
274 };
275 f.render_widget(Paragraph::new(format!(" {label}")).style(style), row);
276 }
277 }
278
279 fn draw_help(&self, f: &mut Frame) {
280 if !self.show_help {
281 return;
282 }
283 let lines = [
284 "tmux-tui — tmux layout manager w panes/windows · Tab toggle",
285 "",
286 "Pane mode",
287 " drag a box / click swap / select a pane",
288 " arrows / Shift+arrows select neighbour / move pane",
289 " | - split left-right / top-bottom",
290 " x kill selected pane",
291 " 1 2 3 4 5 / space layout presets / cycle",
292 " R rename the current window",
293 " right-click New pane ▸ / Kill / Layout / Rename",
294 "",
295 "Window mode (w, or [ windows ])",
296 " arrows navigate + switch window live",
297 " Enter enter the focused window (closes)",
298 " drag window / Shift+arrows reorder windows",
299 " drag a pane into another move pane across windows",
300 " n / x / R new / close / rename window",
301 " right-click Rename / New / Close window",
302 "",
303 "[ quit ] closes · drops are schematic, not live panes (tmux#3503)",
304 "press any key or click to close",
305 ];
306 let full = self.last_full.get();
307 let w = (lines.iter().map(|l| l.chars().count()).max().unwrap_or(40) as u16 + 4)
308 .min(full.width.max(1));
309 let h = (lines.len() as u16 + 2).min(full.height.max(1));
310 let area = Rect {
311 x: full.x + full.width.saturating_sub(w) / 2,
312 y: full.y + full.height.saturating_sub(h) / 2,
313 width: w,
314 height: h,
315 };
316 f.render_widget(Clear, area);
317 f.render_widget(
318 Block::default()
319 .borders(Borders::ALL)
320 .border_style(Style::default().fg(Color::Cyan))
321 .title(" help "),
322 area,
323 );
324 for (i, line) in lines.iter().enumerate() {
325 if (i as u16) + 1 >= area.height.saturating_sub(1) {
326 break;
327 }
328 let row = Rect {
329 x: area.x + 2,
330 y: area.y + 1 + i as u16,
331 width: area.width.saturating_sub(3),
332 height: 1,
333 };
334 f.render_widget(Paragraph::new(*line).style(Style::default().fg(Color::Gray)), row);
335 }
336 }
337
338 fn draw_confirm(&self, f: &mut Frame) {
339 let Some((id, is_win)) = &self.pending_kill else {
340 return;
341 };
342 let (area, yes, no) = self.confirm_rects();
343 let what = if *is_win { "window" } else { "pane" };
344 f.render_widget(Clear, area);
345 f.render_widget(
346 Block::default()
347 .borders(Borders::ALL)
348 .border_style(Style::default().fg(Color::Red))
349 .title(" confirm "),
350 area,
351 );
352 f.render_widget(
353 Paragraph::new(format!("Close {what} {id} ?"))
354 .style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD)),
355 Rect { x: area.x + 2, y: area.y + 1, width: area.width.saturating_sub(3), height: 1 },
356 );
357 f.render_widget(
358 Paragraph::new("[ Yes ]").style(
359 Style::default().fg(Color::White).bg(Color::Red).add_modifier(Modifier::BOLD),
360 ),
361 yes,
362 );
363 f.render_widget(
364 Paragraph::new("[ No ]").style(
365 Style::default().fg(Color::Black).bg(Color::White).add_modifier(Modifier::BOLD),
366 ),
367 no,
368 );
369 }
370
371 fn draw_prompt(&self, f: &mut Frame) {
372 let Some(p) = &self.prompt else {
373 return;
374 };
375 let (area, field, ok, cancel) = self.prompt_rects();
376 f.render_widget(Clear, area);
377 f.render_widget(
378 Block::default()
379 .borders(Borders::ALL)
380 .border_style(Style::default().fg(Color::Cyan))
381 .title(" rename window "),
382 area,
383 );
384 f.render_widget(
385 Paragraph::new(format!("window {}", p.win)).style(Style::default().fg(Color::DarkGray)),
386 Rect { x: area.x + 2, y: area.y + 1, width: area.width.saturating_sub(3), height: 1 },
387 );
388 f.render_widget(
389 Paragraph::new(format!("{}▏", p.buf))
390 .style(Style::default().fg(Color::White).bg(Color::Rgb(40, 40, 40))),
391 field,
392 );
393 f.render_widget(
394 Paragraph::new("[ OK ]").style(
395 Style::default().fg(Color::Black).bg(Color::Green).add_modifier(Modifier::BOLD),
396 ),
397 ok,
398 );
399 f.render_widget(
400 Paragraph::new("[Cancel]").style(
401 Style::default().fg(Color::Black).bg(Color::White).add_modifier(Modifier::BOLD),
402 ),
403 cancel,
404 );
405 }
406}