More safe subtractions all around.

This commit is contained in:
Alexandre Bury 2017-08-14 16:32:01 -07:00
parent 14161f51e4
commit 05bac7a192
12 changed files with 173 additions and 132 deletions

View File

@ -167,6 +167,7 @@ impl<'a> Printer<'a> {
let start = start.into(); let start = start.into();
let size = size.into(); let size = size.into();
if size.x < 2 || size.y < 2 { if size.x < 2 || size.y < 2 {
return; return;
} }
@ -247,8 +248,8 @@ impl<'a> Printer<'a> {
pub fn print_hdelim<T: Into<Vec2>>(&self, start: T, len: usize) { pub fn print_hdelim<T: Into<Vec2>>(&self, start: T, len: usize) {
let start = start.into(); let start = start.into();
self.print(start, ""); self.print(start, "");
self.print_hline(start + (1, 0), len - 2, ""); self.print_hline(start + (1, 0), len.saturating_sub(2), "");
self.print(start + (len - 1, 0), ""); self.print(start + (len.saturating_sub(1), 0), "");
} }
/// Returns a printer on a subset of this one's area. /// Returns a printer on a subset of this one's area.
@ -257,10 +258,15 @@ impl<'a> Printer<'a> {
-> Printer<'a> { -> Printer<'a> {
let size = size.into(); let size = size.into();
let offset = offset.into().or_min(self.size); let offset = offset.into().or_min(self.size);
let available = if !offset.fits_in(self.size) {
Vec2::zero()
} else {
Vec2::min(self.size - offset, size)
};
Printer { Printer {
offset: self.offset + offset, offset: self.offset + offset,
// We can't be larger than what remains // We can't be larger than what remains
size: Vec2::min(self.size - offset, size), size: available,
focused: self.focused && focused, focused: self.focused && focused,
theme: self.theme, theme: self.theme,
backend: self.backend, backend: self.backend,

View File

@ -30,15 +30,57 @@ impl PartialOrd for XY<usize> {
} }
} }
impl XY<usize> {
/// Saturating subtraction. Computes `self - other`, saturating at 0.
///
/// Never panics.
pub fn saturating_sub<O: Into<Self>>(&self, other: O) -> Self {
let other = other.into();
Self::new(self.x.saturating_sub(other.x),
self.y.saturating_sub(other.y))
}
/// Checked subtraction. Computes `self - other` if possible.
///
/// Returns `None` if `self.x < other.x || self.y < other.y`.
///
/// Never panics.
pub fn checked_sub<O: Into<Self>>(&self, other: O) -> Option<Self> {
let other = other.into();
if self.fits(other) {
Some(*self - other)
} else {
None
}
}
/// Returns a `XY<isize>` from `self`.
pub fn signed(self) -> XY<isize> {
self.into()
}
}
impl<T: Ord> XY<T> { impl<T: Ord> XY<T> {
/// Returns `true` if `self` could fit inside `other`. /// Returns `true` if `self` could fit inside `other`.
/// ///
/// Shortcut for `self.x <= other.x && self.y <= other.y`. /// Shortcut for `self.x <= other.x && self.y <= other.y`.
///
/// If this returns `true`, then `other - self` will not underflow.
pub fn fits_in<O: Into<Self>>(&self, other: O) -> bool { pub fn fits_in<O: Into<Self>>(&self, other: O) -> bool {
let other = other.into(); let other = other.into();
self.x <= other.x && self.y <= other.y self.x <= other.x && self.y <= other.y
} }
/// Returns `true` if `other` could fit inside `self`.
///
/// Shortcut for `self.x >= other.x && self.y >= other.y`.
///
/// If this returns `true`, then `self - other` will not underflow.
pub fn fits<O: Into<Self>>(&self, other: O) -> bool {
let other = other.into();
self.x >= other.x && self.y >= other.y
}
/// Returns a new Vec2 that is a maximum per coordinate. /// Returns a new Vec2 that is a maximum per coordinate.
pub fn max<A: Into<XY<T>>, B: Into<XY<T>>>(a: A, b: B) -> Self { pub fn max<A: Into<XY<T>>, B: Into<XY<T>>>(a: A, b: B) -> Self {
let a = a.into(); let a = a.into();

View File

@ -177,14 +177,12 @@ impl ScrollBase {
let max_y = min(self.view_height, let max_y = min(self.view_height,
self.content_height - self.start_line); self.content_height - self.start_line);
let w = if self.scrollable() { let w = if self.scrollable() {
if printer.size.x < 2 {
return;
}
// We have to remove the bar width and the padding. // We have to remove the bar width and the padding.
printer.size.x - 1 - self.right_padding printer.size.x.saturating_sub(1 + self.right_padding)
} else { } else {
printer.size.x printer.size.x
}; };
for y in 0..max_y { for y in 0..max_y {
// Y is the actual coordinate of the line. // Y is the actual coordinate of the line.
// The item ID is then Y + self.start_line // The item ID is then Y + self.start_line
@ -218,7 +216,7 @@ impl ScrollBase {
}; };
// TODO: use 1 instead of 2 // TODO: use 1 instead of 2
let scrollbar_x = printer.size.x - 1 - self.scrollbar_offset; let scrollbar_x = printer.size.x.saturating_sub(1 + self.scrollbar_offset);
printer.print_vline((scrollbar_x, 0), printer.size.y, "|"); printer.print_vline((scrollbar_x, 0), printer.size.y, "|");
printer.with_color(color, |printer| { printer.with_color(color, |printer| {
printer.print_vline((scrollbar_x, start), height, ""); printer.print_vline((scrollbar_x, start), height, "");

View File

@ -179,28 +179,26 @@ impl View for Dialog {
// Current horizontal position of the next button we'll draw. // Current horizontal position of the next button we'll draw.
// Sum of the sizes + len-1 for margins // Sum of the sizes + len-1 for margins
let width = if self.buttons.is_empty() { let width = self.buttons
0 .iter()
} else { .map(|button| button.size.x)
self.buttons .fold(0, |a, b| a + b) +
.iter() self.buttons.len().saturating_sub(1);
.map(|button| button.size.x)
.fold(0, |a, b| a + b) + self.buttons.len() - 1
};
let overhead = self.padding + self.borders; let overhead = self.padding + self.borders;
if printer.size.x < overhead.horizontal() { if printer.size.x < overhead.horizontal() {
return; return;
} }
let mut offset = overhead.left + let mut offset = overhead.left +
self.align self.align.h.get_offset(width,
.h printer.size.x -
.get_offset(width, printer.size.x - overhead.horizontal()); overhead.horizontal());
let overhead_bottom = self.padding.bottom + self.borders.bottom + 1; let overhead_bottom = self.padding.bottom + self.borders.bottom + 1;
if overhead_bottom > printer.size.y {
return; let y = match printer.size.y.checked_sub(overhead_bottom) {
} Some(y) => y,
let y = printer.size.y - overhead_bottom; None => return,
};
for (i, button) in self.buttons.iter().enumerate() { for (i, button) in self.buttons.iter().enumerate() {
let size = button.size; let size = button.size;
@ -217,16 +215,16 @@ impl View for Dialog {
// What do we have left? // What do we have left?
let taken = Vec2::new(0, buttons_height) + self.borders.combined() + let taken = Vec2::new(0, buttons_height) + self.borders.combined() +
self.padding.combined(); self.padding.combined();
if !taken.fits_in(printer.size) {
return;
}
let inner_size = printer.size - taken;
self.content let inner_size = match printer.size.checked_sub(taken) {
.draw(&printer.sub_printer(self.borders.top_left() + Some(s) => s,
self.padding.top_left(), None => return,
inner_size, };
self.focus == Focus::Content));
self.content.draw(&printer.sub_printer(self.borders.top_left() +
self.padding.top_left(),
inner_size,
self.focus == Focus::Content));
printer.print_box(Vec2::new(0, 0), printer.size, false); printer.print_box(Vec2::new(0, 0), printer.size, false);
@ -253,9 +251,10 @@ impl View for Dialog {
// Buttons are not flexible, so their size doesn't depend on ours. // Buttons are not flexible, so their size doesn't depend on ours.
let mut buttons_size = Vec2::new(0, 0); let mut buttons_size = Vec2::new(0, 0);
if !self.buttons.is_empty() {
buttons_size.x += self.buttons.len() - 1; // Start with the inter-button space.
} buttons_size.x += self.buttons.len().saturating_sub(1);
for button in &mut self.buttons { for button in &mut self.buttons {
let s = button.view.required_size(req); let s = button.view.required_size(req);
buttons_size.x += s.x; buttons_size.x += s.x;
@ -264,11 +263,12 @@ impl View for Dialog {
// We also remove one row for the buttons. // We also remove one row for the buttons.
let taken = nomans_land + Vec2::new(0, buttons_size.y); let taken = nomans_land + Vec2::new(0, buttons_size.y);
if !taken.fits_in(req) {
let content_req = match req.checked_sub(taken) {
Some(r) => r,
// Bad!! // Bad!!
return taken; None => return taken,
} };
let content_req = req - taken;
let content_size = self.content.required_size(content_req); let content_size = self.content.required_size(content_req);
@ -291,11 +291,7 @@ impl View for Dialog {
// Padding and borders are taken, sorry. // Padding and borders are taken, sorry.
// TODO: handle border-less themes? // TODO: handle border-less themes?
let taken = self.borders.combined() + self.padding.combined(); let taken = self.borders.combined() + self.padding.combined();
size = if taken.fits_in(size) { size = size.saturating_sub(taken);
size - taken
} else {
Vec2::zero()
};
// Buttons are kings, we give them everything they want. // Buttons are kings, we give them everything they want.
let mut buttons_height = 0; let mut buttons_height = 0;
@ -309,7 +305,7 @@ impl View for Dialog {
if buttons_height > size.y { if buttons_height > size.y {
buttons_height = size.y; buttons_height = size.y;
} }
self.content.layout(size - Vec2::new(0, buttons_height)); self.content.layout(size.saturating_sub((0, buttons_height)));
} }
fn on_event(&mut self, event: Event) -> EventResult { fn on_event(&mut self, event: Event) -> EventResult {
@ -339,8 +335,7 @@ impl View for Dialog {
match event { match event {
// Up goes back to the content // Up goes back to the content
Event::Key(Key::Up) => { Event::Key(Key::Up) => {
if self.content if self.content.take_focus(Direction::down()) {
.take_focus(Direction::down()) {
self.focus = Focus::Content; self.focus = Focus::Content;
EventResult::Consumed(None) EventResult::Consumed(None)
} else { } else {
@ -348,8 +343,7 @@ impl View for Dialog {
} }
} }
Event::Shift(Key::Tab) => { Event::Shift(Key::Tab) => {
if self.content if self.content.take_focus(Direction::back()) {
.take_focus(Direction::back()) {
self.focus = Focus::Content; self.focus = Focus::Content;
EventResult::Consumed(None) EventResult::Consumed(None)
} else { } else {
@ -358,7 +352,7 @@ impl View for Dialog {
} }
Event::Key(Key::Tab) => { Event::Key(Key::Tab) => {
if self.content if self.content
.take_focus(Direction::front()) { .take_focus(Direction::front()) {
self.focus = Focus::Content; self.focus = Focus::Content;
EventResult::Consumed(None) EventResult::Consumed(None)
} else { } else {
@ -367,8 +361,7 @@ impl View for Dialog {
} }
// Left and Right move to other buttons // Left and Right move to other buttons
Event::Key(Key::Right) if i + 1 < Event::Key(Key::Right) if i + 1 <
self.buttons self.buttons.len() => {
.len() => {
self.focus = Focus::Button(i + 1); self.focus = Focus::Button(i + 1);
EventResult::Consumed(None) EventResult::Consumed(None)
} }
@ -400,7 +393,7 @@ impl View for Dialog {
} }
fn call_on_any<'a>(&mut self, selector: &Selector, fn call_on_any<'a>(&mut self, selector: &Selector,
callback: Box<FnMut(&mut Any) + 'a>) { callback: Box<FnMut(&mut Any) + 'a>) {
self.content.call_on_any(selector, callback); self.content.call_on_any(selector, callback);
} }

View File

@ -370,13 +370,14 @@ impl EditView {
.map(|g| g.width()) .map(|g| g.width())
.next() .next()
.unwrap_or(1); .unwrap_or(1);
if c_len > self.last_length {
// Weird - no available space?
return;
}
// Now, we have to fit self.content[..self.cursor] // Now, we have to fit self.content[..self.cursor]
// into self.last_length - c_len. // into self.last_length - c_len.
let available = self.last_length - c_len; let available = match self.last_length.checked_sub(c_len) {
Some(s) => s,
// Weird - no available space?
None => return,
};
// Look at the content before the cursor (we will print its tail). // Look at the content before the cursor (we will print its tail).
// From the end, count the length until we reach `available`. // From the end, count the length until we reach `available`.
// Then sum the byte lengths. // Then sum the byte lengths.
@ -384,6 +385,8 @@ impl EditView {
self.cursor], self.cursor],
available) available)
.length; .length;
assert!(suffix_length <= self.cursor);
self.offset = self.cursor - suffix_length; self.offset = self.cursor - suffix_length;
// Make sure the cursor is in view // Make sure the cursor is in view
assert!(self.cursor >= self.offset); assert!(self.cursor >= self.offset);
@ -392,8 +395,11 @@ impl EditView {
// If we have too much space // If we have too much space
if self.content[self.offset..].width() < self.last_length { if self.content[self.offset..].width() < self.last_length {
assert!(self.last_length >= 1);
let suffix_length = let suffix_length =
simple_suffix(&self.content, self.last_length - 1).length; simple_suffix(&self.content, self.last_length - 1).length;
assert!(self.content.len() >= 1);
self.offset = self.content.len() - suffix_length; self.offset = self.content.len() - suffix_length;
} }
} }
@ -425,6 +431,7 @@ impl View for EditView {
printer.with_effect(effect, |printer| { printer.with_effect(effect, |printer| {
if width < self.last_length { if width < self.last_length {
// No problem, everything fits. // No problem, everything fits.
assert!(printer.size.x >= width);
if self.secret { if self.secret {
printer.print_hline((0, 0), width, "*"); printer.print_hline((0, 0), width, "*");
} else { } else {

View File

@ -270,7 +270,7 @@ impl View for LinearLayout {
} }
// This here is how much we're generously offered // This here is how much we're generously offered
// (We just checked that req >= desperate, so the substraction is safe // (We just checked that req >= desperate, so the subtraction is safe
let mut available = self.orientation.get(&(req - desperate)); let mut available = self.orientation.get(&(req - desperate));
// println_stderr!("Available: {:?}", available); // println_stderr!("Available: {:?}", available);
@ -279,7 +279,7 @@ impl View for LinearLayout {
let mut overweight: Vec<(usize, usize)> = sizes.iter() let mut overweight: Vec<(usize, usize)> = sizes.iter()
.map(|v| self.orientation.get(v)) .map(|v| self.orientation.get(v))
.zip(min_sizes.iter().map(|v| self.orientation.get(v))) .zip(min_sizes.iter().map(|v| self.orientation.get(v)))
.map(|(a, b)| if a > b { a - b } else { 0 }) .map(|(a, b)| a.saturating_sub(b))
.enumerate() .enumerate()
.collect(); .collect();
// println_stderr!("Overweight: {:?}", overweight); // println_stderr!("Overweight: {:?}", overweight);

View File

@ -199,7 +199,8 @@ impl ListView {
} }
} }
fn try_focus((i, child): (usize, &mut ListChild), source: direction::Direction) fn try_focus((i, child): (usize, &mut ListChild),
source: direction::Direction)
-> Option<usize> { -> Option<usize> {
match *child { match *child {
ListChild::Delimiter => None, ListChild::Delimiter => None,
@ -274,12 +275,8 @@ impl View for ListView {
let spacing = 1; let spacing = 1;
let scrollbar_width = if self.children.len() > size.y { 2 } else { 0 }; let scrollbar_width = if self.children.len() > size.y { 2 } else { 0 };
let available = if label_width + spacing + scrollbar_width > size.x { let available = size.x.saturating_sub(label_width + spacing +
// We have no space for the kids! :( scrollbar_width);
0
} else {
size.x - label_width - spacing - scrollbar_width
};
// println_stderr!("Available: {}", available); // println_stderr!("Available: {}", available);
@ -351,10 +348,8 @@ impl View for ListView {
} }
fn call_on_any<'a>(&mut self, selector: &Selector, fn call_on_any<'a>(&mut self, selector: &Selector,
mut callback: Box<FnMut(&mut Any) + 'a>) { mut callback: Box<FnMut(&mut Any) + 'a>) {
for view in self.children for view in self.children.iter_mut().filter_map(ListChild::view) {
.iter_mut()
.filter_map(ListChild::view) {
view.call_on_any(selector, Box::new(|any| callback(any))); view.call_on_any(selector, Box::new(|any| callback(any)));
} }
} }
@ -364,7 +359,9 @@ impl View for ListView {
.iter_mut() .iter_mut()
.enumerate() .enumerate()
.filter_map(|(i, v)| v.view().map(|v| (i, v))) .filter_map(|(i, v)| v.view().map(|v| (i, v)))
.filter_map(|(i, v)| v.focus_view(selector).ok().map(|_| i)) .filter_map(|(i, v)| {
v.focus_view(selector).ok().map(|_| i)
})
.next() { .next() {
self.focus = i; self.focus = i;
Ok(()) Ok(())

View File

@ -170,7 +170,7 @@ impl MenuPopup {
impl View for MenuPopup { impl View for MenuPopup {
fn draw(&self, printer: &Printer) { fn draw(&self, printer: &Printer) {
if printer.size.x < 2 || printer.size.y < 2 { if !printer.size.fits((2, 2)) {
return; return;
} }
@ -200,7 +200,8 @@ impl View for MenuPopup {
} }
printer.print_hline((1, 0), printer.size.x - 2, " "); printer.print_hline((1, 0), printer.size.x - 2, " ");
printer.print((2, 0), label); printer.print((2, 0), label);
printer.print((printer.size.x - 4, 0), ">>"); printer.print((printer.size.x.saturating_sub(4), 0),
">>");
} }
MenuItem::Leaf(ref label, _) => { MenuItem::Leaf(ref label, _) => {
if printer.size.x < 2 { if printer.size.x < 2 {
@ -251,7 +252,12 @@ impl View for MenuPopup {
Event::Key(Key::PageDown) => self.scroll_down(5, false), Event::Key(Key::PageDown) => self.scroll_down(5, false),
Event::Key(Key::Home) => self.focus = 0, Event::Key(Key::Home) => self.focus = 0,
Event::Key(Key::End) => self.focus = self.menu.children.len() - 1, Event::Key(Key::End) => {
self.focus = self.menu
.children
.len()
.saturating_sub(1)
}
Event::Key(Key::Right) if self.menu.children[self.focus] Event::Key(Key::Right) if self.menu.children[self.focus]
.is_subtree() => { .is_subtree() => {
@ -297,6 +303,7 @@ impl View for MenuPopup {
} }
fn layout(&mut self, size: Vec2) { fn layout(&mut self, size: Vec2) {
self.scrollbase.set_heights(size.y - 2, self.menu.children.len()); self.scrollbase.set_heights(size.y.saturating_sub(2),
self.menu.children.len());
} }
} }

View File

@ -20,11 +20,7 @@ impl<V: View> ViewWrapper for Panel<V> {
fn wrap_required_size(&mut self, req: Vec2) -> Vec2 { fn wrap_required_size(&mut self, req: Vec2) -> Vec2 {
// TODO: make borders conditional? // TODO: make borders conditional?
let req = if Vec2::new(2, 2).fits_in(req) { let req = req.saturating_sub((2, 2));
req - (2, 2)
} else {
Vec2::zero()
};
self.view.required_size(req) + (2, 2) self.view.required_size(req) + (2, 2)
} }
@ -32,15 +28,11 @@ impl<V: View> ViewWrapper for Panel<V> {
fn wrap_draw(&self, printer: &Printer) { fn wrap_draw(&self, printer: &Printer) {
printer.print_box((0, 0), printer.size, true); printer.print_box((0, 0), printer.size, true);
self.view.draw(&printer.sub_printer((1, 1), self.view.draw(&printer.sub_printer((1, 1),
printer.size - (2, 2), printer.size.saturating_sub((2, 2)),
true)); true));
} }
fn wrap_layout(&mut self, size: Vec2) { fn wrap_layout(&mut self, size: Vec2) {
self.view.layout(if Vec2::new(2, 2).fits_in(size) { self.view.layout(size.saturating_sub((2, 2)));
size - (2, 2)
} else {
size
});
} }
} }

View File

@ -1,7 +1,6 @@
use Cursive; use Cursive;
use Printer; use Printer;
use With; use With;
use XY;
use align::{Align, HAlign, VAlign}; use align::{Align, HAlign, VAlign};
use direction::Direction; use direction::Direction;
use event::{Callback, Event, EventResult, Key}; use event::{Callback, Event, EventResult, Key};
@ -67,7 +66,7 @@ pub struct SelectView<T = String> {
last_size: Vec2, last_size: Vec2,
} }
impl <T: 'static> Default for SelectView<T> { impl<T: 'static> Default for SelectView<T> {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
@ -252,7 +251,8 @@ impl<T: 'static> SelectView<T> {
printer.print_hline((0, 0), x, " "); printer.print_hline((0, 0), x, " ");
printer.print((x, 0), &self.items[i].label); printer.print((x, 0), &self.items[i].label);
if l < printer.size.x { if l < printer.size.x {
printer.print_hline((x + l, 0), printer.size.x - l - x, " "); assert!((l + x) <= printer.size.x);
printer.print_hline((x + l, 0), printer.size.x - (l + x), " ");
} }
} }
@ -290,12 +290,11 @@ impl<T: 'static> SelectView<T> {
fn focus_up(&mut self, n: usize) { fn focus_up(&mut self, n: usize) {
let focus = self.focus(); let focus = self.focus();
let n = min(focus, n); self.focus.set(focus.saturating_sub(n));
self.focus.set(focus - n);
} }
fn focus_down(&mut self, n: usize) { fn focus_down(&mut self, n: usize) {
let focus = min(self.focus() + n, self.items.len() - 1); let focus = min(self.focus() + n, self.items.len().saturating_sub(1));
self.focus.set(focus); self.focus.set(focus);
} }
} }
@ -353,23 +352,22 @@ impl<T: 'static> View for SelectView<T> {
} else { } else {
ColorStyle::Highlight ColorStyle::Highlight
}; };
let x = printer.size.x; let x = match printer.size.x.checked_sub(1) {
if x == 0 { Some(x) => x,
return; None => return,
} };
printer.with_color(style, |printer| { printer.with_color(style, |printer| {
// Prepare the entire background // Prepare the entire background
printer.print_hline((1, 0), x - 1, " "); printer.print_hline((1, 0), x, " ");
// Draw the borders // Draw the borders
printer.print((0, 0), "<"); printer.print((0, 0), "<");
printer.print((x - 1, 0), ">"); printer.print((x, 0), ">");
let label = &self.items[self.focus()].label; let label = &self.items[self.focus()].label;
// And center the text? // And center the text?
let offset = HAlign::Center.get_offset(label.len(), x); let offset = HAlign::Center.get_offset(label.len(), x + 1);
printer.print((offset, 0), label); printer.print((offset, 0), label);
}); });
@ -445,20 +443,17 @@ impl<T: 'static> View for SelectView<T> {
// We'll want to show the popup so that the text matches. // We'll want to show the popup so that the text matches.
// It'll be soo cool. // It'll be soo cool.
let item_length = self.items[focus].label.len(); let item_length = self.items[focus].label.len();
let text_offset = if self.last_size.x >= item_length { let text_offset =
(self.last_size.x - item_length) / 2 (self.last_size.x.saturating_sub(item_length)) / 2;
} else {
// We were too small to show the entire item last time.
0
};
// The total offset for the window is: // The total offset for the window is:
// * the last absolute offset at which we drew this view // * the last absolute offset at which we drew this view
// * shifted to the top of the focus (so the line matches)
// * shifted to the right of the text offset // * shifted to the right of the text offset
// * shifted to the top of the focus (so the line matches)
// * shifted top-left of the border+padding of the popup // * shifted top-left of the border+padding of the popup
let offset = self.last_offset.get() - (0, focus) + let offset = self.last_offset.get();
(text_offset, 0) - let offset = offset + (text_offset, 0);
(2, 1); let offset = offset.saturating_sub((0, focus));
let offset = offset.saturating_sub((2, 1));
// And now, we can return the callback. // And now, we can return the callback.
EventResult::with_cb(move |s| { EventResult::with_cb(move |s| {
// The callback will want to work with a fresh Rc // The callback will want to work with a fresh Rc
@ -468,12 +463,11 @@ impl<T: 'static> View for SelectView<T> {
// A nice effect is that window resizes will keep both // A nice effect is that window resizes will keep both
// layers together. // layers together.
let current_offset = s.screen().offset(); let current_offset = s.screen().offset();
let offset = XY::<isize>::from(offset) - let offset = offset.signed() - current_offset;
current_offset;
// And finally, put the view in view! // And finally, put the view in view!
s.screen_mut() s.screen_mut().add_layer_at(Position::parent(offset),
.add_layer_at(Position::parent(offset), MenuPopup::new(tree)
MenuPopup::new(tree).focus(focus)); .focus(focus));
}) })
} }
_ => EventResult::Ignored, _ => EventResult::Ignored,
@ -488,7 +482,7 @@ impl<T: 'static> View for SelectView<T> {
Event::Key(Key::PageUp) => self.focus_up(10), Event::Key(Key::PageUp) => self.focus_up(10),
Event::Key(Key::PageDown) => self.focus_down(10), Event::Key(Key::PageDown) => self.focus_down(10),
Event::Key(Key::Home) => self.focus.set(0), Event::Key(Key::Home) => self.focus.set(0),
Event::Key(Key::End) => self.focus.set(self.items.len() - 1), Event::Key(Key::End) => self.focus.set(self.items.len().saturating_sub(1)),
Event::Key(Key::Enter) if self.on_submit.is_some() => { Event::Key(Key::Enter) if self.on_submit.is_some() => {
let cb = self.on_submit.clone().unwrap(); let cb = self.on_submit.clone().unwrap();
let v = self.selection(); let v = self.selection();
@ -504,9 +498,10 @@ impl<T: 'static> View for SelectView<T> {
// the list when we reach the end. // the list when we reach the end.
// This is achieved by chaining twice the iterator // This is achieved by chaining twice the iterator
let iter = self.items.iter().chain(self.items.iter()); let iter = self.items.iter().chain(self.items.iter());
if let Some((i, _)) = iter.enumerate() if let Some((i, _)) =
.skip(self.focus() + 1) iter.enumerate()
.find(|&(_, item)| item.label.starts_with(c)) { .skip(self.focus() + 1)
.find(|&(_, item)| item.label.starts_with(c)) {
// Apply modulo in case we have a hit // Apply modulo in case we have a hit
// from the chained iterator // from the chained iterator
self.focus.set(i % self.items.len()); self.focus.set(i % self.items.len());

View File

@ -49,13 +49,13 @@ impl<T: View> ViewWrapper for ShadowView<T> {
fn wrap_required_size(&mut self, req: Vec2) -> Vec2 { fn wrap_required_size(&mut self, req: Vec2) -> Vec2 {
// Make sure req >= offset // Make sure req >= offset
let offset = self.padding().or_min(req); let offset = self.padding();
self.view.required_size(req - offset) + offset self.view.required_size(req.saturating_sub(offset)) + offset
} }
fn wrap_layout(&mut self, size: Vec2) { fn wrap_layout(&mut self, size: Vec2) {
let offset = self.padding().or_min(size); let offset = self.padding();
self.view.layout(size - offset); self.view.layout(size.saturating_sub(offset));
} }
fn wrap_draw(&self, printer: &Printer) { fn wrap_draw(&self, printer: &Printer) {
@ -74,6 +74,10 @@ impl<T: View> ViewWrapper for ShadowView<T> {
let h = printer.size.y; let h = printer.size.y;
let w = printer.size.x; let w = printer.size.x;
if h == 0 || w == 0 {
return;
}
printer.with_color(ColorStyle::Shadow, |printer| { printer.with_color(ColorStyle::Shadow, |printer| {
printer.print_hline((1, h - 1), w - 1, " "); printer.print_hline((1, h - 1), w - 1, " ");
printer.print_vline((w - 1, 1), h - 1, " "); printer.print_vline((w - 1, 1), h - 1, " ");
@ -81,8 +85,9 @@ impl<T: View> ViewWrapper for ShadowView<T> {
} }
// Draw the view background // Draw the view background
let printer = let printer = printer.sub_printer(Vec2::zero(),
printer.sub_printer(Vec2::zero(), printer.size - (1, 1), true); printer.size.saturating_sub((1, 1)),
true);
self.view.draw(&printer); self.view.draw(&printer);
} }
} }

View File

@ -192,13 +192,12 @@ impl TextView {
// We take 1 column for the bar itself + 1 spacing column // We take 1 column for the bar itself + 1 spacing column
scrollbar_width = 2; scrollbar_width = 2;
if size.x < scrollbar_width {
// Again, this is a lost cause.
return;
}
// If we're too high, include a scrollbar_width // If we're too high, include a scrollbar_width
let available = size.x - scrollbar_width; let available = match size.x.checked_sub(scrollbar_width) {
Some(s) => s,
None => return,
};
self.rows = LinesIterator::new(&self.content, available).collect(); self.rows = LinesIterator::new(&self.content, available).collect();
if self.rows.is_empty() && !self.content.is_empty() { if self.rows.is_empty() && !self.content.is_empty() {