use egui::{
CornerRadius, FontId, FontSelection, Response, Stroke, TextEdit, Ui, Vec2, Widget, WidgetInfo,
WidgetText, WidgetType,
};
use crate::{flash, theme::Theme};
#[must_use = "Add with `ui.add(...)`."]
pub struct TextArea<'a> {
text: &'a mut String,
label: Option<WidgetText>,
hint: Option<&'a str>,
dirty: bool,
rows: usize,
desired_width: Option<f32>,
monospace: bool,
id_salt: Option<egui::Id>,
}
impl<'a> std::fmt::Debug for TextArea<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextArea")
.field("dirty", &self.dirty)
.field("rows", &self.rows)
.field("monospace", &self.monospace)
.field("desired_width", &self.desired_width)
.finish()
}
}
impl<'a> TextArea<'a> {
pub fn new(text: &'a mut String) -> Self {
Self {
text,
label: None,
hint: None,
dirty: false,
rows: 5,
desired_width: None,
monospace: false,
id_salt: None,
}
}
pub fn label(mut self, text: impl Into<WidgetText>) -> Self {
self.label = Some(text.into());
self
}
pub fn hint(mut self, text: &'a str) -> Self {
self.hint = Some(text);
self
}
pub fn dirty(mut self, dirty: bool) -> Self {
self.dirty = dirty;
self
}
pub fn rows(mut self, rows: usize) -> Self {
self.rows = rows.max(1);
self
}
pub fn desired_width(mut self, width: f32) -> Self {
self.desired_width = Some(width);
self
}
pub fn monospace(mut self, monospace: bool) -> Self {
self.monospace = monospace;
self
}
pub fn id_salt(mut self, id: impl std::hash::Hash) -> Self {
self.id_salt = Some(egui::Id::new(id));
self
}
}
impl<'a> Widget for TextArea<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let theme = Theme::current(ui.ctx());
let p = &theme.palette;
let t = &theme.typography;
ui.vertical(|ui| {
if let Some(label) = &self.label {
ui.add_space(2.0);
let rich = egui::RichText::new(label.text())
.color(p.text_muted)
.size(t.label);
ui.add(egui::Label::new(rich).wrap_mode(egui::TextWrapMode::Extend));
ui.add_space(2.0);
}
let id_salt = self.id_salt.unwrap_or_else(|| ui.next_auto_id());
let widget_id = ui.make_persistent_id(egui::Id::new(id_salt));
let flash = flash::active_flash(ui.ctx(), widget_id);
let bg_fill = flash::background_fill(&theme, p.input_bg, flash);
let desired_width = self.desired_width.unwrap_or_else(|| ui.available_width());
let margin = Vec2::new(theme.control_padding_x * 0.5, theme.control_padding_y);
let font_id = if self.monospace {
FontId::monospace(t.monospace)
} else {
FontId::proportional(t.body)
};
let response = crate::theme::with_themed_visuals(ui, |ui| {
let v = ui.visuals_mut();
crate::theme::themed_input_visuals(v, &theme, bg_fill);
v.extreme_bg_color = bg_fill;
v.selection.bg_fill = crate::theme::with_alpha(p.sky, 90);
v.selection.stroke = Stroke::new(1.0, p.sky);
let mut edit = TextEdit::multiline(self.text)
.id_salt(id_salt)
.font(FontSelection::FontId(font_id))
.text_color(p.text)
.margin(margin)
.desired_width(desired_width)
.desired_rows(self.rows);
if let Some(hint) = self.hint {
edit = edit.hint_text(egui::RichText::new(hint).color(p.text_faint));
}
ui.add(edit)
});
if self.dirty && ui.is_rect_visible(response.rect) {
let stroke_w = 1.0;
let bar_w = 3.0;
let r = ((theme.control_radius - stroke_w).max(0.0)) as u8;
let bar = egui::Rect::from_min_max(
egui::pos2(
response.rect.min.x + stroke_w,
response.rect.min.y + stroke_w,
),
egui::pos2(
response.rect.min.x + stroke_w + bar_w,
response.rect.max.y - stroke_w,
),
);
let corner = CornerRadius {
nw: r,
sw: r,
ne: 0,
se: 0,
};
ui.painter().rect_filled(bar, corner, p.sky);
}
if let Some(label) = &self.label {
let label = label.text().to_string();
response.widget_info(|| WidgetInfo::labeled(WidgetType::TextEdit, true, &label));
}
response
})
.inner
}
}