Skip to content

Commit a7e6987

Browse files
committed
feat(record): add menu bar
There are two main concepts added to the viewport code: - A **mask** is a subarea of the canvas which subsequent calls will implicitly not render outside of. This lets us overlay one component on top of another in a fixed area. We use this to allocate the top line of the screen to the menu bar and the rest of the screen to the files. We could also use this in the future for e.g. rendering files side-by-side. - A **timestamp** is a monotonically-increasing integer assigned to each component as it's drawn. Previously, we tried to figure out which component was being clicked by finding all components which drew to a certain point on the screen and selecting the smallest one. In some cases, this wasn't sufficient. For example, the sticky file header may be larger than a changed line drawn behind it, but we still want to select the sticky file header in this case. Now, we use the timestamp value to infer which of two components is drawn on top (with the later-drawn component being on top). The menu bar is a fairly straightforward state machine. There may come to be issues with managing interactions between components. For example, we now have an event to unfocus the menu bar, which means that we need to be aware of the menu bar when clicking on any other component on the screen. This sort of behavior will eventually lead to O(n^2) state transitions. The proper fix would probably be some kind of event system in combination with a focus system.
1 parent 6bf8aeb commit a7e6987

File tree

4 files changed

+773
-236
lines changed

4 files changed

+773
-236
lines changed

scm-record/README.md

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,6 @@ This is a standalone binary that uses the `scm-record` library as a front-end, a
2323
- [Mercurial](https://www.mercurial-scm.org/): via [the `extdiff` extension](https://wiki.mercurial-scm.org/ExtdiffExtension). Only supports viewing diffs, not editing them.
2424
- [Jujutsu](https://github.com/martinvonz/jj): via [the `ui.diff-editor` option](https://github.com/martinvonz/jj/blob/main/docs/config.md#editing-diffs).
2525

26-
# Keybindings
27-
28-
Ideally, these would be documented in the UI itself.
29-
30-
- `ctrl-c`, `q`: discard changes and quit. If there are unsaved changes, you will be prompted to confirm discarding them.
31-
- `c`: confirm changes and quit.
32-
- `f`: expand/collapse the current item.
33-
- `F`: expand/collapse all items.
34-
- `up`/`k`: select the next item.
35-
- `down`/`j`: select the previous item.
36-
- `left`/`h`: select the outer item.
37-
- `right`/`l`: select the inner item.
38-
- `space`: toggle the current item.
39-
- `enter`: toggle the current item and move to the next item of the same kind.
40-
- `a`: invert the toggled state of each item.
41-
- `A`: toggle or untoggle all items uniformly.
42-
- `ctrl-y`: scroll the viewport up by one line.
43-
- `ctrl-e`: scroll the viewport down by one line.
44-
- `page-up`/`ctrl-b`: scroll the viewport up by one screen.
45-
- `page-down`/`ctrl-f`: scroll the viewport up by one screen.
46-
- `ctrl-u`: move the selection half a screen up from the currently-selected item.
47-
- `ctrl-d`: move the selection half a screen down from the currently-selected item.
48-
4926
# Integration with other projects
5027

5128
Here's some projects that don't use `scm-record`, but could benefit from integration with it (with your contribution):
@@ -62,5 +39,4 @@ Here's some projects that don't use `scm-record`, but could benefit from integra
6239
Here are some features in the UI which are not yet implemented:
6340

6441
- Jump to next/previous element of same kind.
65-
- Menu bar to explain available actions and keybindings.
6642
- Edit one side of the diff in an editor.

scm-record/src/render.rs

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use std::borrow::Cow;
12
use std::cmp::{max, min};
3+
use std::collections::HashMap;
24
use std::fmt::Debug;
35
use std::hash::Hash;
4-
use std::{borrow::Cow, collections::HashMap};
6+
use std::mem;
57

68
use cassowary::{Solver, Variable};
79
use num_traits::cast;
@@ -21,12 +23,6 @@ pub(crate) struct RectSize {
2123
pub height: usize,
2224
}
2325

24-
impl RectSize {
25-
pub fn area(self) -> usize {
26-
self.width * self.height
27-
}
28-
}
29-
3026
impl From<tui::layout::Rect> for RectSize {
3127
fn from(rect: tui::layout::Rect) -> Self {
3228
Rect::from(rect).into()
@@ -73,14 +69,6 @@ impl From<tui::layout::Rect> for Rect {
7369
}
7470

7571
impl Rect {
76-
/// The size of the `Rect`. (To get the area as a scalar, call `.size().area()`.)
77-
pub fn size(self) -> RectSize {
78-
RectSize {
79-
width: self.width,
80-
height: self.height,
81-
}
82-
}
83-
8472
/// The (x, y) coordinate of the top-left corner of this `Rect`.
8573
fn top_left(self) -> (isize, isize) {
8674
(self.x, self.y)
@@ -246,7 +234,7 @@ struct DrawTrace<ComponentId> {
246234
rect: Rect,
247235

248236
/// The bounding boxes of where each child component drew.
249-
components: HashMap<ComponentId, Rect>,
237+
components: HashMap<ComponentId, DrawnRect>,
250238
}
251239

252240
impl<ComponentId: Clone + Debug + Eq + Hash> DrawTrace<ComponentId> {
@@ -283,6 +271,14 @@ impl<ComponentId> Default for DrawTrace<ComponentId> {
283271
}
284272
}
285273

274+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
275+
pub(crate) struct DrawnRect {
276+
pub rect: Rect,
277+
pub timestamp: usize,
278+
}
279+
280+
pub(crate) type DrawnRects<C> = HashMap<C, DrawnRect>;
281+
286282
/// Accessor to draw on the virtual canvas. The caller can draw anywhere on the
287283
/// canvas, but the actual renering will be restricted to this viewport. All
288284
/// draw calls are also tracked so that we know where each component was drawn
@@ -291,15 +287,37 @@ impl<ComponentId> Default for DrawTrace<ComponentId> {
291287
pub(crate) struct Viewport<'a, ComponentId> {
292288
buf: &'a mut Buffer,
293289
rect: Rect,
290+
mask: Option<Rect>,
291+
timestamp: usize,
294292
trace: Vec<DrawTrace<ComponentId>>,
295293
debug_messages: Vec<String>,
296294
}
297295

298296
impl<'a, ComponentId: Clone + Debug + Eq + Hash> Viewport<'a, ComponentId> {
297+
pub fn new(buf: &'a mut Buffer, rect: Rect) -> Self {
298+
Self {
299+
buf,
300+
rect,
301+
mask: Default::default(),
302+
timestamp: Default::default(),
303+
trace: vec![Default::default()],
304+
debug_messages: Default::default(),
305+
}
306+
}
307+
308+
/// The portion of the virtual canvas that will be rendered to the terminal.
309+
/// Thus, this `Rect` should have the same dimensions as the terminal.
299310
pub fn rect(&self) -> Rect {
300311
self.rect
301312
}
302313

314+
/// The mask used for rendering. If this is `Some`, then calls to
315+
/// `draw_span` will only render inside the mask area. This can be used to
316+
/// overlay one component on top of another in a fixed area.
317+
pub fn mask(&self) -> Option<Rect> {
318+
self.mask
319+
}
320+
303321
/// The size of the viewport.
304322
pub fn size(&self) -> RectSize {
305323
RectSize {
@@ -315,7 +333,7 @@ impl<'a, ComponentId: Clone + Debug + Eq + Hash> Viewport<'a, ComponentId> {
315333
x: isize,
316334
y: isize,
317335
component: &C,
318-
) -> HashMap<C::Id, Rect> {
336+
) -> DrawnRects<C::Id> {
319337
let widget = TopLevelWidget { component, x, y };
320338
let term_area = frame.size();
321339
let mut drawn_rects = Default::default();
@@ -341,6 +359,15 @@ impl<'a, ComponentId: Clone + Debug + Eq + Hash> Viewport<'a, ComponentId> {
341359
self.debug_messages.push(message.into())
342360
}
343361

362+
/// Set a mask to be used for rendering inside `f`.
363+
pub fn with_mask<T>(&mut self, mask: Rect, f: impl FnOnce(&mut Self) -> T) -> T {
364+
let mut mask = Some(mask);
365+
mem::swap(&mut self.mask, &mut mask);
366+
let result = f(self);
367+
mem::swap(&mut self.mask, &mut mask);
368+
result
369+
}
370+
344371
/// Draw the provided child component to the screen at the given `(x, y)`
345372
/// location.
346373
pub fn draw_component<C: Component<Id = ComponentId>>(
@@ -349,16 +376,29 @@ impl<'a, ComponentId: Clone + Debug + Eq + Hash> Viewport<'a, ComponentId> {
349376
y: isize,
350377
component: &C,
351378
) -> Rect {
352-
self.trace.push(Default::default());
353-
component.draw(self, x, y);
354-
let mut trace = self.trace.pop().unwrap();
355-
356-
let trace_rect = trace
357-
.components
358-
.values()
359-
.fold(trace.rect, |acc, elem| acc.union_bounding(*elem));
379+
let timestamp = {
380+
let timestamp = self.timestamp;
381+
self.timestamp += 1;
382+
timestamp
383+
};
384+
let mut trace = {
385+
self.trace.push(Default::default());
386+
component.draw(self, x, y);
387+
self.trace.pop().unwrap()
388+
};
389+
390+
let trace_rect = trace.components.values().fold(trace.rect, |acc, elem| {
391+
let DrawnRect { rect, timestamp: _ } = elem;
392+
acc.union_bounding(*rect)
393+
});
360394
trace.rect = trace_rect;
361-
trace.components.insert(component.id(), trace_rect);
395+
trace.components.insert(
396+
component.id(),
397+
DrawnRect {
398+
rect: trace_rect,
399+
timestamp,
400+
},
401+
);
362402

363403
self.current_trace_mut().merge(trace);
364404
trace_rect
@@ -376,6 +416,10 @@ impl<'a, ComponentId: Clone + Debug + Eq + Hash> Viewport<'a, ComponentId> {
376416
self.current_trace_mut().merge_rect(span_rect);
377417

378418
let draw_rect = self.rect.intersect(span_rect);
419+
let draw_rect = match self.mask {
420+
Some(mask) => draw_rect.intersect(mask),
421+
None => draw_rect,
422+
};
379423
if !draw_rect.is_empty() {
380424
let span_start_idx = (draw_rect.x - span_rect.x).unwrap_usize();
381425
let span_start_byte_idx = content
@@ -462,22 +506,19 @@ struct TopLevelWidget<'a, C> {
462506
}
463507

464508
impl<C: Component> StatefulWidget for TopLevelWidget<'_, C> {
465-
type State = HashMap<C::Id, Rect>;
509+
type State = DrawnRects<C::Id>;
466510

467511
fn render(self, area: tui::layout::Rect, buf: &mut Buffer, state: &mut Self::State) {
468512
let Self { component, x, y } = self;
469-
let trace = vec![Default::default()];
470-
let mut viewport = Viewport::<C::Id> {
513+
let mut viewport: Viewport<C::Id> = Viewport::new(
471514
buf,
472-
rect: Rect {
515+
Rect {
473516
x,
474517
y,
475518
width: area.width.into(),
476519
height: area.height.into(),
477520
},
478-
trace,
479-
debug_messages: Default::default(),
480-
};
521+
);
481522
viewport.draw_component(0, 0, component);
482523
*state = viewport.trace.pop().unwrap().components;
483524
debug_assert!(viewport.trace.is_empty());

0 commit comments

Comments
 (0)