Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Rework imUI focus behaviour
Erik Kundt committed 1 year ago
commit e96ee308de904aedb85c62435def3db0dfb9d3f9
parent d822f7af6d319d1c8d8a80262fb11ef0efb1b6a5
3 files changed +164 -68
modified examples/selection.rs
@@ -87,6 +87,7 @@ impl Show<Message> for App {
                    Constraint::Min(1),
                    Constraint::Length(1),
                ]),
+
                Some(1),
                |ui| {
                    let columns = [
                        Column::new(Span::raw("Id").bold(), Constraint::Length(4)),
@@ -98,7 +99,6 @@ impl Show<Message> for App {

                    ui.columns(frame, columns.clone(), Some(Borders::None));

-
                    ui.set_focus(Some(1));
                    let table = ui.table(
                        frame,
                        &mut selected,
modified src/ui/im.rs
@@ -276,7 +276,9 @@ pub struct Ui<M> {
    /// The layout used to calculate the next area to draw.
    layout: Layout,
    /// Currently focused area.
-
    focus: Option<usize>,
+
    focus_area: Option<usize>,
+
    /// If this has focus.
+
    has_focus: bool,
    /// Current rendering counter that is increased whenever the next area to draw
    /// on is requested.
    count: usize,
@@ -284,15 +286,15 @@ pub struct Ui<M> {

impl<M> Ui<M> {
    pub fn input(&mut self, f: impl Fn(Key) -> bool) -> bool {
-
        self.has_focus() && self.ctx.inputs.iter().any(|key| f(*key))
+
        self.has_focus && self.is_area_focused() && self.ctx.inputs.iter().any(|key| f(*key))
    }

    pub fn input_global(&mut self, f: impl Fn(Key) -> bool) -> bool {
-
        self.ctx.inputs.iter().any(|key| f(*key))
+
        self.has_focus && self.ctx.inputs.iter().any(|key| f(*key))
    }

    pub fn input_with_key(&mut self, f: impl Fn(Key) -> bool) -> Option<Key> {
-
        if self.has_focus() {
+
        if self.has_focus && self.is_area_focused() {
            self.ctx.inputs.iter().find(|key| f(**key)).copied()
        } else {
            None
@@ -306,7 +308,8 @@ impl<M> Default for Ui<M> {
            theme: Theme::default(),
            area: Rect::default(),
            layout: Layout::default(),
-
            focus: None,
+
            focus_area: None,
+
            has_focus: true,
            count: 0,
            ctx: Context::default(),
        }
@@ -331,11 +334,21 @@ impl<M> Ui<M> {
        self
    }

+
    pub fn with_area_focus(mut self, focus: Option<usize>) -> Self {
+
        self.focus_area = focus;
+
        self
+
    }
+

    pub fn with_ctx(mut self, ctx: Context<M>) -> Self {
        self.ctx = ctx;
        self
    }

+
    pub fn with_focus(mut self) -> Self {
+
        self.has_focus = true;
+
        self
+
    }
+

    pub fn theme(&self) -> &Theme {
        &self.theme
    }
@@ -345,41 +358,44 @@ impl<M> Ui<M> {
    }

    pub fn next_area(&mut self) -> Option<(Rect, bool)> {
-
        let has_focus = self.focus.map(|focus| self.count == focus).unwrap_or(false);
+
        let area_focus = self
+
            .focus_area
+
            .map(|focus| self.count == focus)
+
            .unwrap_or(false);
        let rect = self.layout.split(self.area).get(self.count).cloned();

        self.count += 1;

-
        rect.map(|rect| (rect, has_focus))
+
        rect.map(|rect| (rect, area_focus))
    }

    pub fn current_area(&mut self) -> Option<(Rect, bool)> {
        let count = self.count.saturating_sub(1);

-
        let has_focus = self.focus.map(|focus| count == focus).unwrap_or(false);
+
        let area_focus = self.focus_area.map(|focus| count == focus).unwrap_or(false);
        let rect = self.layout.split(self.area).get(self.count).cloned();

-
        rect.map(|rect| (rect, has_focus))
+
        rect.map(|rect| (rect, area_focus))
    }

-
    pub fn has_focus(&self) -> bool {
+
    pub fn is_area_focused(&self) -> bool {
        let count = self.count.saturating_sub(1);
-
        self.focus.map(|focus| count == focus).unwrap_or(false)
+
        self.focus_area.map(|focus| count == focus).unwrap_or(false)
    }

-
    pub fn count(&self) -> usize {
-
        self.count
+
    pub fn has_focus(&self) -> bool {
+
        self.has_focus
    }

-
    pub fn set_focus(&mut self, focus: Option<usize>) {
-
        self.focus = focus;
+
    pub fn count(&self) -> usize {
+
        self.count
    }

    pub fn focus_next(&mut self) {
-
        if self.focus.is_none() {
-
            self.focus = Some(0);
+
        if self.focus_area.is_none() {
+
            self.focus_area = Some(0);
        } else {
-
            self.focus = Some(self.focus.unwrap().saturating_add(1));
+
            self.focus_area = Some(self.focus_area.unwrap().saturating_add(1));
        }
    }

@@ -408,21 +424,27 @@ where
    pub fn layout<R>(
        &mut self,
        layout: impl Into<Layout>,
+
        focus: Option<usize>,
        add_contents: impl FnOnce(&mut Self) -> R,
    ) -> InnerResponse<R> {
-
        self.layout_dyn(layout, Box::new(add_contents))
+
        self.layout_dyn(layout, focus, Box::new(add_contents))
    }

    pub fn layout_dyn<R>(
        &mut self,
        layout: impl Into<Layout>,
+
        focus: Option<usize>,
        add_contents: Box<AddContentFn<M, R>>,
    ) -> InnerResponse<R> {
-
        let (area, _) = self.next_area().unwrap_or_default();
-
        let mut child_ui = self.child_ui(area, layout);
-
        let inner = add_contents(&mut child_ui);
+
        let (area, area_focus) = self.next_area().unwrap_or_default();

-
        InnerResponse::new(inner, Response::default())
+
        let mut child_ui = Ui {
+
            has_focus: area_focus,
+
            focus_area: focus,
+
            ..self.child_ui(area, layout)
+
        };
+

+
        InnerResponse::new(add_contents(&mut child_ui), Response::default())
    }
}

@@ -436,17 +458,35 @@ where
        focus: &mut Option<usize>,
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
    ) -> InnerResponse<R> {
-
        let (area, _) = self.next_area().unwrap_or_default();
+
        let (area, area_focus) = self.next_area().unwrap_or_default();

        let layout: Layout = layout.into();
        let len = layout.len();

-
        let mut child_ui = self.child_ui(area, layout);
-
        child_ui.set_focus(Some(0));
+
        // TODO(erikli): Check if setting the focus area is needed at all.
+
        let mut child_ui = Ui {
+
            has_focus: area_focus,
+
            focus_area: *focus,
+
            ..self.child_ui(area, layout)
+
        };

        widget::Group::new(len, focus).show(&mut child_ui, add_contents)
    }

+
    pub fn composite<R>(
+
        &mut self,
+
        layout: impl Into<Layout>,
+
        focus: usize,
+
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
+
    ) -> InnerResponse<R> {
+
        let (area, area_focus) = self.next_area().unwrap_or_default();
+

+
        let mut child_ui = self.child_ui(area, layout);
+
        child_ui.has_focus = area_focus;
+

+
        widget::Composite::new(focus).show(&mut child_ui, add_contents)
+
    }
+

    pub fn label<'a>(&mut self, frame: &mut Frame, content: impl Into<Text<'a>>) -> Response {
        widget::Label::new(content).ui(self, frame)
    }
modified src/ui/im/widget.rs
@@ -48,9 +48,11 @@ impl Window {
        M: Clone,
    {
        let mut ui = Ui::default()
+
            .with_focus()
            .with_area(ctx.frame_size())
            .with_ctx(ctx.clone())
-
            .with_layout(Layout::horizontal([Constraint::Min(1)]).into());
+
            .with_layout(Layout::horizontal([Constraint::Min(1)]).into())
+
            .with_area_focus(Some(0));

        let inner = add_contents(&mut ui);

@@ -128,23 +130,18 @@ impl<'a> Group<'a> {
            len: self.len,
        };

-
        if let Some(key) = ui.input_with_key(|_| true) {
-
            match key {
-
                Key::Char('\t') => {
-
                    state.focus_next();
-
                    response.changed = true;
-
                }
-
                Key::BackTab => {
-
                    state.focus_prev();
-
                    response.changed = true;
-
                }
-
                _ => {}
-
            }
+
        if ui.input_global(|key| key == Key::Char('\t')) {
+
            state.focus_next();
+
            response.changed = true;
+
        }
+
        if ui.input_global(|key| key == Key::BackTab) {
+
            state.focus_prev();
+
            response.changed = true;
        }
        *self.focus = state.focus;

        let mut ui = Ui {
-
            focus: state.focus,
+
            focus_area: state.focus,
            ..ui.clone()
        };

@@ -154,6 +151,69 @@ impl<'a> Group<'a> {
    }
}

+
#[derive(Clone, Debug)]
+
pub struct CompositeState {
+
    len: usize,
+
    focus: usize,
+
}
+

+
impl CompositeState {
+
    pub fn new(len: usize, focus: usize) -> Self {
+
        Self { len, focus }
+
    }
+

+
    pub fn focus(&self) -> usize {
+
        self.focus
+
    }
+

+
    pub fn len(&self) -> usize {
+
        self.len
+
    }
+

+
    pub fn is_empty(&self) -> bool {
+
        self.len == 0
+
    }
+
}
+

+
pub struct Composite {
+
    focus: usize,
+
}
+

+
impl Composite {
+
    pub fn new(focus: usize) -> Self {
+
        Self { focus }
+
    }
+

+
    pub fn show<M, R>(
+
        self,
+
        ui: &mut Ui<M>,
+
        add_contents: impl FnOnce(&mut Ui<M>) -> R,
+
    ) -> InnerResponse<R>
+
    where
+
        M: Clone,
+
    {
+
        self.show_dyn(ui, Box::new(add_contents))
+
    }
+

+
    pub fn show_dyn<M, R>(
+
        self,
+
        ui: &mut Ui<M>,
+
        add_contents: Box<AddContentFn<M, R>>,
+
    ) -> InnerResponse<R>
+
    where
+
        M: Clone,
+
    {
+
        let mut ui = Ui {
+
            focus_area: Some(self.focus),
+
            ..ui.clone()
+
        };
+

+
        let inner = add_contents(&mut ui);
+

+
        InnerResponse::new(inner, Response::default())
+
    }
+
}
+

pub struct Label<'a> {
    content: Text<'a>,
}
@@ -298,7 +358,7 @@ where
    {
        let mut response = Response::default();

-
        let (area, has_focus) = ui.next_area().unwrap_or_default();
+
        let (area, area_focus) = ui.next_area().unwrap_or_default();

        let show_scrollbar = self.show_scrollbar && self.items.len() >= area.height.into();
        let has_items = !self.items.is_empty();
@@ -311,7 +371,7 @@ where
            },
        };

-
        let border_style = if has_focus {
+
        let border_style = if area_focus && ui.has_focus {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
@@ -326,25 +386,30 @@ where
            match key {
                Key::Up | Key::Char('k') => {
                    state.prev();
+
                    response.changed = true;
                }
                Key::Down | Key::Char('j') => {
                    state.next(len);
+
                    response.changed = true;
                }
                Key::PageUp => {
                    state.prev_page(page_size);
+
                    response.changed = true;
                }
                Key::PageDown => {
                    state.next_page(len, page_size);
+
                    response.changed = true;
                }
                Key::Home => {
                    state.begin();
+
                    response.changed = true;
                }
                Key::End => {
                    state.end(len);
+
                    response.changed = true;
                }
                _ => {}
            }
-
            response.changed = true;
        }

        let widths: Vec<Constraint> = self
@@ -388,9 +453,9 @@ where
                .rows(rows)
                .widths(widths)
                .column_spacing(1)
-
                .highlight_style(style::highlight(has_focus));
+
                .highlight_style(style::highlight(area_focus));

-
            let table = if !has_focus && self.dim {
+
            let table = if !area_focus && self.dim {
                table.dim()
            } else {
                table
@@ -405,7 +470,7 @@ where
                    .track_symbol(None)
                    .end_symbol(None)
                    .thumb_symbol("┃")
-
                    .style(if has_focus {
+
                    .style(if area_focus {
                        Style::default()
                    } else {
                        Style::default().dim()
@@ -473,21 +538,12 @@ where
    {
        let mut response = Response::default();

-
        let (_, has_focus) = ui.current_area().unwrap_or_default();
-

-
        ui.layout(
+
        ui.composite(
            Layout::vertical([Constraint::Length(3), Constraint::Min(1)]),
+
            1,
            |ui| {
-
                // TODO(erikli): Find better solution for border focus workaround or improve
-
                // interface for manually advancing / setting the focus index.
-
                if has_focus {
-
                    ui.set_focus(Some(0));
-
                }
                ui.columns(frame, self.header.clone().to_vec(), Some(Borders::Top));

-
                if has_focus {
-
                    ui.set_focus(Some(1));
-
                }
                let table = ui.table(
                    frame,
                    self.selected,
@@ -519,9 +575,9 @@ impl<'a> Widget for Columns<'a> {
    where
        M: Clone,
    {
-
        let (area, has_focus) = ui.next_area().unwrap_or_default();
+
        let (area, _) = ui.next_area().unwrap_or_default();

-
        let border_style = if has_focus {
+
        let border_style = if ui.has_focus {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
@@ -578,9 +634,9 @@ impl<'a> Widget for Bar<'a> {
    where
        M: Clone,
    {
-
        let (area, has_focus) = ui.next_area().unwrap_or_default();
+
        let (area, area_focus) = ui.next_area().unwrap_or_default();

-
        let border_style = if has_focus {
+
        let border_style = if area_focus {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
@@ -694,10 +750,10 @@ impl<'a> Widget for TextView<'a> {
    {
        let mut response = Response::default();

-
        let (area, has_focus) = ui.next_area().unwrap_or_default();
+
        let (area, area_focus) = ui.next_area().unwrap_or_default();

        let show_scrollbar = true;
-
        let border_style = if has_focus {
+
        let border_style = if area_focus && ui.has_focus() {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
@@ -729,7 +785,7 @@ impl<'a> Widget for TextView<'a> {
            .track_symbol(None)
            .end_symbol(None)
            .thumb_symbol("┃")
-
            .style(if has_focus {
+
            .style(if area_focus {
                Style::default()
            } else {
                Style::default().dim()
@@ -916,9 +972,9 @@ impl<'a> TextEdit<'a> {
    {
        let mut response = Response::default();

-
        let (area, has_focus) = ui.next_area().unwrap_or_default();
+
        let (area, area_focus) = ui.next_area().unwrap_or_default();

-
        let border_style = if has_focus {
+
        let border_style = if area_focus && ui.has_focus() {
            ui.theme.focus_border_style
        } else {
            ui.theme.border_style
@@ -937,7 +993,7 @@ impl<'a> TextEdit<'a> {
        let overline = String::from("▔").repeat(area.width as usize);
        let cursor_pos = *self.cursor as u16;

-
        let (label, input, overline) = if !has_focus && self.dim {
+
        let (label, input, overline) = if !area_focus && self.dim {
            (
                Span::from(label_content.clone()).magenta().dim().reversed(),
                Span::from(state.text.clone()).reset().dim(),