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 event;
|
||||
pub mod layout;
|
||||
pub mod theme;
|
||||
|
Loading…
Reference in new issue