parent
4d60c10330
commit
afd7cb3b01
@ -1,3 +0,0 @@
|
|||||||
pub mod constants;
|
|
||||||
pub mod theme;
|
|
||||||
|
|
@ -0,0 +1,193 @@
|
|||||||
|
use crate::ui::{
|
||||||
|
component::{Component, Event, EventCtx},
|
||||||
|
display::{self, Color, Font},
|
||||||
|
geometry::{Offset, Point, Rect},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
event::{ButtonEvent, T1Button},
|
||||||
|
theme,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum ButtonMsg {
|
||||||
|
Clicked,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum ButtonPos {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonPos {
|
||||||
|
fn hit(&self, b: &T1Button) -> bool {
|
||||||
|
matches!(
|
||||||
|
(self, b),
|
||||||
|
(Self::Left, T1Button::Left) | (Self::Right, T1Button::Right)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Button<T> {
|
||||||
|
area: Rect,
|
||||||
|
pos: ButtonPos,
|
||||||
|
baseline: Point,
|
||||||
|
content: ButtonContent<T>,
|
||||||
|
styles: ButtonStyleSheet,
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]>> Button<T> {
|
||||||
|
pub fn new(
|
||||||
|
area: Rect,
|
||||||
|
pos: ButtonPos,
|
||||||
|
content: ButtonContent<T>,
|
||||||
|
styles: ButtonStyleSheet,
|
||||||
|
) -> Self {
|
||||||
|
let (area, baseline) = Self::placement(area, pos, &content, &styles);
|
||||||
|
Self {
|
||||||
|
area,
|
||||||
|
pos,
|
||||||
|
baseline,
|
||||||
|
content,
|
||||||
|
styles,
|
||||||
|
state: State::Released,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_text(area: Rect, pos: ButtonPos, text: T, styles: ButtonStyleSheet) -> Self {
|
||||||
|
Self::new(area, pos, ButtonContent::Text(text), styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_icon(
|
||||||
|
area: Rect,
|
||||||
|
pos: ButtonPos,
|
||||||
|
image: &'static [u8],
|
||||||
|
styles: ButtonStyleSheet,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(area, pos, ButtonContent::Icon(image), styles)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(&self) -> &ButtonContent<T> {
|
||||||
|
&self.content
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(&self) -> &ButtonStyle {
|
||||||
|
match self.state {
|
||||||
|
State::Released => self.styles.normal,
|
||||||
|
State::Pressed => self.styles.active,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, ctx: &mut EventCtx, state: State) {
|
||||||
|
if self.state != state {
|
||||||
|
self.state = state;
|
||||||
|
ctx.request_paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placement(
|
||||||
|
area: Rect,
|
||||||
|
pos: ButtonPos,
|
||||||
|
content: &ButtonContent<T>,
|
||||||
|
styles: &ButtonStyleSheet,
|
||||||
|
) -> (Rect, Point) {
|
||||||
|
let border_width = if styles.normal.border_horiz { 2 } else { 0 };
|
||||||
|
let content_width = match content {
|
||||||
|
ButtonContent::Text(text) => display::text_width(text.as_ref(), styles.normal.font) - 1,
|
||||||
|
ButtonContent::Icon(_icon) => todo!(),
|
||||||
|
};
|
||||||
|
let button_width = content_width + 2 * border_width;
|
||||||
|
let area = match pos {
|
||||||
|
ButtonPos::Left => area.vsplit(button_width).0,
|
||||||
|
ButtonPos::Right => area.vsplit(-button_width).1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_of_baseline = area.bottom_left() + Offset::new(border_width, -2);
|
||||||
|
|
||||||
|
return (area, start_of_baseline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]>> Component for Button<T> {
|
||||||
|
type Msg = ButtonMsg;
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
match event {
|
||||||
|
Event::Button(ButtonEvent::ButtonPressed(which)) if self.pos.hit(&which) => {
|
||||||
|
self.set(ctx, State::Pressed);
|
||||||
|
}
|
||||||
|
Event::Button(ButtonEvent::ButtonReleased(which)) if self.pos.hit(&which) => {
|
||||||
|
if matches!(self.state, State::Pressed) {
|
||||||
|
self.set(ctx, State::Released);
|
||||||
|
return Some(ButtonMsg::Clicked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
let style = self.style();
|
||||||
|
|
||||||
|
match &self.content {
|
||||||
|
ButtonContent::Text(text) => {
|
||||||
|
let background_color = style.text_color.neg();
|
||||||
|
if style.border_horiz {
|
||||||
|
display::rounded_rect1(self.area, background_color, theme::BG);
|
||||||
|
} else {
|
||||||
|
display::rect(self.area, background_color)
|
||||||
|
}
|
||||||
|
|
||||||
|
display::text(
|
||||||
|
self.baseline,
|
||||||
|
text.as_ref(),
|
||||||
|
style.font,
|
||||||
|
style.text_color,
|
||||||
|
background_color,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ButtonContent::Icon(_image) => {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl<T> crate::trace::Trace for Button<T>
|
||||||
|
where
|
||||||
|
T: AsRef<[u8]> + crate::trace::Trace,
|
||||||
|
{
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.open("Button");
|
||||||
|
match &self.content {
|
||||||
|
ButtonContent::Text(text) => t.field("text", text),
|
||||||
|
ButtonContent::Icon(_) => t.symbol("icon"),
|
||||||
|
}
|
||||||
|
t.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
enum State {
|
||||||
|
Released,
|
||||||
|
Pressed,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ButtonContent<T> {
|
||||||
|
Text(T),
|
||||||
|
Icon(&'static [u8]),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ButtonStyleSheet {
|
||||||
|
pub normal: &'static ButtonStyle,
|
||||||
|
pub active: &'static ButtonStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ButtonStyle {
|
||||||
|
pub font: Font,
|
||||||
|
pub text_color: Color,
|
||||||
|
pub border_horiz: bool,
|
||||||
|
}
|
@ -0,0 +1,117 @@
|
|||||||
|
use super::{
|
||||||
|
button::{Button, ButtonMsg::Clicked, ButtonPos},
|
||||||
|
theme,
|
||||||
|
};
|
||||||
|
use crate::ui::{
|
||||||
|
component::{Child, Component, Event, EventCtx},
|
||||||
|
display,
|
||||||
|
geometry::{Offset, Rect},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub enum DialogMsg<T> {
|
||||||
|
Content(T),
|
||||||
|
LeftClicked,
|
||||||
|
RightClicked,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Dialog<T, U> {
|
||||||
|
header: Option<(U, Rect)>,
|
||||||
|
content: Child<T>,
|
||||||
|
left_btn: Option<Child<Button<U>>>,
|
||||||
|
right_btn: Option<Child<Button<U>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Component, U: AsRef<[u8]>> Dialog<T, U> {
|
||||||
|
pub fn new(
|
||||||
|
area: Rect,
|
||||||
|
content: impl FnOnce(Rect) -> T,
|
||||||
|
left: Option<impl FnOnce(Rect, ButtonPos) -> Button<U>>,
|
||||||
|
right: Option<impl FnOnce(Rect, ButtonPos) -> Button<U>>,
|
||||||
|
header: Option<U>,
|
||||||
|
) -> Self {
|
||||||
|
let (header_area, content_area, button_area) = Self::areas(area, &header);
|
||||||
|
let content = Child::new(content(content_area));
|
||||||
|
let left_btn = left.map(|f| Child::new(f(button_area, ButtonPos::Left)));
|
||||||
|
let right_btn = right.map(|f| Child::new(f(button_area, ButtonPos::Right)));
|
||||||
|
Self {
|
||||||
|
header: header.zip(header_area),
|
||||||
|
content,
|
||||||
|
left_btn,
|
||||||
|
right_btn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint_header(&self) {
|
||||||
|
if let Some((title, area)) = &self.header {
|
||||||
|
display::text(
|
||||||
|
area.bottom_left() + Offset::new(0, -2),
|
||||||
|
title.as_ref(),
|
||||||
|
theme::FONT_BOLD,
|
||||||
|
theme::FG,
|
||||||
|
theme::BG,
|
||||||
|
);
|
||||||
|
display::dotted_line(area.bottom_left(), area.width(), theme::FG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn areas(area: Rect, header: &Option<U>) -> (Option<Rect>, Rect, Rect) {
|
||||||
|
const HEADER_SPACE: i32 = 4;
|
||||||
|
let button_height = theme::FONT_BOLD.line_height() + 2;
|
||||||
|
let header_height = theme::FONT_BOLD.line_height();
|
||||||
|
|
||||||
|
let (content_area, button_area) = area.hsplit(-button_height);
|
||||||
|
if header.is_none() {
|
||||||
|
(None, content_area, button_area)
|
||||||
|
} else {
|
||||||
|
let (header_area, content_area) = content_area.hsplit(header_height);
|
||||||
|
let (_space, content_area) = content_area.hsplit(HEADER_SPACE);
|
||||||
|
(Some(header_area), content_area, button_area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Component, U: AsRef<[u8]>> Component for Dialog<T, U> {
|
||||||
|
type Msg = DialogMsg<T::Msg>;
|
||||||
|
|
||||||
|
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
|
||||||
|
if let Some(msg) = self.content.event(ctx, event) {
|
||||||
|
Some(DialogMsg::Content(msg))
|
||||||
|
} else if let Some(Clicked) = self.left_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||||
|
Some(DialogMsg::LeftClicked)
|
||||||
|
} else if let Some(Clicked) = self.right_btn.as_mut().and_then(|b| b.event(ctx, event)) {
|
||||||
|
Some(DialogMsg::RightClicked)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paint(&mut self) {
|
||||||
|
self.paint_header();
|
||||||
|
self.content.paint();
|
||||||
|
if let Some(b) = self.left_btn.as_mut() {
|
||||||
|
b.paint();
|
||||||
|
}
|
||||||
|
if let Some(b) = self.right_btn.as_mut() {
|
||||||
|
b.paint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ui_debug")]
|
||||||
|
impl<T, U> crate::trace::Trace for Dialog<T, U>
|
||||||
|
where
|
||||||
|
T: crate::trace::Trace,
|
||||||
|
U: crate::trace::Trace + AsRef<[u8]>,
|
||||||
|
{
|
||||||
|
fn trace(&self, t: &mut dyn crate::trace::Tracer) {
|
||||||
|
t.open("Dialog");
|
||||||
|
t.field("content", &self.content);
|
||||||
|
if let Some(label) = &self.left_btn {
|
||||||
|
t.field("left", label);
|
||||||
|
}
|
||||||
|
if let Some(label) = &self.right_btn {
|
||||||
|
t.field("right", label);
|
||||||
|
}
|
||||||
|
t.close();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
mod button;
|
||||||
|
mod dialog;
|
||||||
|
|
||||||
|
use super::{event, theme};
|
||||||
|
|
||||||
|
pub use button::{Button, ButtonContent, ButtonMsg, ButtonPos, ButtonStyle, ButtonStyleSheet};
|
||||||
|
pub use dialog::{Dialog, DialogMsg};
|
@ -0,0 +1,146 @@
|
|||||||
|
use core::convert::{TryFrom, TryInto};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::Error,
|
||||||
|
micropython::{buffer::Buffer, map::Map, obj::Obj, qstr::Qstr},
|
||||||
|
ui::{
|
||||||
|
component::{Child, Text},
|
||||||
|
display,
|
||||||
|
layout::obj::LayoutObj,
|
||||||
|
},
|
||||||
|
util,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
component::{Button, Dialog, DialogMsg},
|
||||||
|
theme,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl<T> TryFrom<DialogMsg<T>> for Obj
|
||||||
|
where
|
||||||
|
Obj: TryFrom<T>,
|
||||||
|
Error: From<<T as TryInto<Obj>>::Error>,
|
||||||
|
{
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(val: DialogMsg<T>) -> Result<Self, Self::Error> {
|
||||||
|
match val {
|
||||||
|
DialogMsg::Content(c) => Ok(c.try_into()?),
|
||||||
|
DialogMsg::LeftClicked => 1.try_into(),
|
||||||
|
DialogMsg::RightClicked => 2.try_into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
extern "C" fn ui_layout_new_confirm_action(
|
||||||
|
n_args: usize,
|
||||||
|
args: *const Obj,
|
||||||
|
kwargs: *const Map,
|
||||||
|
) -> Obj {
|
||||||
|
let block = |_args: &[Obj], kwargs: &Map| {
|
||||||
|
let title: Buffer = kwargs.get(Qstr::MP_QSTR_title)?.try_into()?;
|
||||||
|
let action: Option<Buffer> = kwargs.get(Qstr::MP_QSTR_action)?.try_into_option()?;
|
||||||
|
let description: Option<Buffer> =
|
||||||
|
kwargs.get(Qstr::MP_QSTR_description)?.try_into_option()?;
|
||||||
|
let verb: Option<Buffer> = kwargs.get(Qstr::MP_QSTR_verb)?.try_into_option()?;
|
||||||
|
let verb_cancel: Option<Buffer> =
|
||||||
|
kwargs.get(Qstr::MP_QSTR_verb_cancel)?.try_into_option()?;
|
||||||
|
let reverse: bool = kwargs.get(Qstr::MP_QSTR_reverse)?.try_into()?;
|
||||||
|
|
||||||
|
let format = match (&action, &description, reverse) {
|
||||||
|
(Some(_), Some(_), false) => "{bold}{action}\n\r{normal}{description}",
|
||||||
|
(Some(_), Some(_), true) => "{normal}{description}\n\r{bold}{action}",
|
||||||
|
(Some(_), None, _) => "{bold}{action}",
|
||||||
|
(None, Some(_), _) => "{normal}{description}",
|
||||||
|
_ => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
let left = verb_cancel
|
||||||
|
.map(|label| |area, pos| Button::with_text(area, pos, label, theme::button_cancel()));
|
||||||
|
let right = verb
|
||||||
|
.map(|label| |area, pos| Button::with_text(area, pos, label, theme::button_default()));
|
||||||
|
|
||||||
|
let obj = LayoutObj::new(Child::new(Dialog::new(
|
||||||
|
display::screen(),
|
||||||
|
|area| {
|
||||||
|
Text::new::<theme::T1DefaultText>(area, format)
|
||||||
|
.with(b"action", action.unwrap_or("".into()))
|
||||||
|
.with(b"description", description.unwrap_or("".into()))
|
||||||
|
},
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
Some(title),
|
||||||
|
)))?;
|
||||||
|
Ok(obj.into())
|
||||||
|
};
|
||||||
|
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::trace::{Trace, Tracer};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Tracer for Vec<u8> {
|
||||||
|
fn bytes(&mut self, b: &[u8]) {
|
||||||
|
self.extend(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string(&mut self, s: &str) {
|
||||||
|
self.extend(s.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol(&mut self, name: &str) {
|
||||||
|
self.extend(name.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&mut self, name: &str) {
|
||||||
|
self.extend(b"<");
|
||||||
|
self.extend(name.as_bytes());
|
||||||
|
self.extend(b" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field(&mut self, name: &str, value: &dyn Trace) {
|
||||||
|
self.extend(name.as_bytes());
|
||||||
|
self.extend(b":");
|
||||||
|
value.trace(self);
|
||||||
|
self.extend(b" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(&mut self) {
|
||||||
|
self.extend(b">")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trace(val: &impl Trace) -> String {
|
||||||
|
let mut t = Vec::new();
|
||||||
|
val.trace(&mut t);
|
||||||
|
String::from_utf8(t).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trace_example_layout() {
|
||||||
|
let layout = Child::new(Dialog::new(
|
||||||
|
display::screen(),
|
||||||
|
|area| {
|
||||||
|
Text::new(
|
||||||
|
area,
|
||||||
|
"Testing text layout, with some text, and some more text. And {param}",
|
||||||
|
)
|
||||||
|
.with(b"param", b"parameters!")
|
||||||
|
},
|
||||||
|
Some(|area, pos| Button::with_text(area, pos, "Left", theme::button_cancel())),
|
||||||
|
Some(|area, pos| Button::with_text(area, pos, "Right", theme::button_default())),
|
||||||
|
None,
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
trace(&layout),
|
||||||
|
r#"<Dialog content:<Text content:Testing text layout,
|
||||||
|
with some text, and
|
||||||
|
some more text. And p-
|
||||||
|
arameters! > left:<Button text:Left > right:<Button text:Right > >"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
pub mod component;
|
||||||
pub mod constant;
|
pub mod constant;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod layout;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
Loading…
Reference in new issue