use crate::icons::{Icon, IconName};
use iced::widget::{button, column, container, row, text_editor, Space, TextEditor};
use iced::{Background, Border, Color, Element, Length, Padding, Theme};
use std::rc::Rc;
pub type RichTextContent = text_editor::Content;
#[derive(Debug, Clone)]
pub enum RichTextAction {
Edit(text_editor::Action),
ToggleBold,
ToggleItalic,
ToggleUnderline,
ToggleStrikethrough,
ToggleCode,
InsertHeading(u8),
InsertBullet,
InsertNumber,
InsertLink,
InsertRule,
Undo,
Redo,
}
#[derive(Debug, Clone, Default)]
pub struct FormattingState {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub code: bool,
}
pub struct RichTextEditor<'a, Message> {
content: &'a RichTextContent,
on_action: Option<Rc<dyn Fn(RichTextAction) -> Message + 'a>>,
placeholder: Option<&'a str>,
width: Length,
height: Length,
show_toolbar: bool,
formatting: FormattingState,
}
impl<'a, Message: Clone + 'a> RichTextEditor<'a, Message> {
pub fn new(content: &'a RichTextContent) -> Self {
Self {
content,
on_action: None,
placeholder: None,
width: Length::Fill,
height: Length::Fixed(300.0),
show_toolbar: true,
formatting: FormattingState::default(),
}
}
#[must_use]
pub fn on_action<F>(mut self, on_action: F) -> Self
where
F: Fn(RichTextAction) -> Message + 'a,
{
self.on_action = Some(Rc::new(on_action));
self
}
#[must_use]
pub fn placeholder(mut self, placeholder: &'a str) -> Self {
self.placeholder = Some(placeholder);
self
}
#[must_use]
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
#[must_use]
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
#[must_use]
pub fn toolbar(mut self, show: bool) -> Self {
self.show_toolbar = show;
self
}
#[must_use]
pub fn formatting(mut self, state: FormattingState) -> Self {
self.formatting = state;
self
}
}
impl<'a, Message: Clone + 'a> From<RichTextEditor<'a, Message>> for Element<'a, Message, Theme> {
fn from(editor: RichTextEditor<'a, Message>) -> Self {
let on_action = editor.on_action;
let mut text_ed = TextEditor::new(editor.content);
if let Some(ref on_action) = on_action {
let on_action = Rc::clone(on_action);
text_ed = text_ed.on_action(move |action| on_action(RichTextAction::Edit(action)));
}
if let Some(placeholder) = editor.placeholder {
text_ed = text_ed.placeholder(placeholder);
}
text_ed = text_ed
.padding(Padding::new(12.0))
.style(|theme: &Theme, status| {
let palette = theme.extended_palette();
let border_color = match status {
text_editor::Status::Active => palette.background.strong.color,
text_editor::Status::Hovered => palette.primary.weak.color,
text_editor::Status::Focused => palette.primary.base.color,
text_editor::Status::Disabled => palette.background.strong.color,
};
text_editor::Style {
background: Background::Color(palette.background.base.color),
border: Border {
color: border_color,
width: 1.0,
radius: 6.0.into(),
},
icon: palette.background.weak.text,
placeholder: palette.background.weak.text,
value: palette.background.base.text,
selection: palette.primary.weak.color,
}
});
let toolbar: Option<Element<'a, Message, Theme>> = if editor.show_toolbar {
if let Some(ref on_action) = on_action {
Some(create_toolbar(on_action, &editor.formatting))
} else {
None
}
} else {
None
};
let content: Element<'a, Message, Theme> = if let Some(toolbar) = toolbar {
column![toolbar, text_ed]
.spacing(0)
.into()
} else {
text_ed.into()
};
container(content)
.width(editor.width)
.height(editor.height)
.into()
}
}
fn create_toolbar<'a, Message: Clone + 'a>(
on_action: &Rc<dyn Fn(RichTextAction) -> Message + 'a>,
formatting: &FormattingState,
) -> Element<'a, Message, Theme> {
let on_action = Rc::clone(on_action);
let btn = move |icon_name: IconName, action: RichTextAction, active: bool| {
let icon: Element<'a, Message, Theme> = Icon::new(icon_name).size(16.0).into();
let on_action = Rc::clone(&on_action);
button(icon)
.padding([6, 8])
.on_press(on_action(action))
.style(move |theme: &Theme, status| {
let palette = theme.extended_palette();
let bg = if active {
palette.primary.weak.color
} else {
match status {
button::Status::Hovered => palette.background.weak.color,
_ => Color::TRANSPARENT,
}
};
button::Style {
background: Some(Background::Color(bg)),
text_color: palette.background.base.text,
border: Border {
radius: 4.0.into(),
..Default::default()
},
..Default::default()
}
})
};
let separator = || {
container(Space::new(1, 20))
.style(|theme: &Theme| {
let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
..Default::default()
}
})
};
let toolbar_content = row![
btn(IconName::Edit, RichTextAction::ToggleBold, formatting.bold),
btn(IconName::Edit, RichTextAction::ToggleItalic, formatting.italic),
separator(),
btn(IconName::List, RichTextAction::InsertBullet, false),
separator(),
btn(IconName::Link, RichTextAction::InsertLink, false),
btn(IconName::Minus, RichTextAction::InsertRule, false),
Space::with_width(Length::Fill),
btn(IconName::ArrowLeft, RichTextAction::Undo, false),
btn(IconName::ArrowRight, RichTextAction::Redo, false),
]
.spacing(2)
.padding([8, 12])
.align_y(iced::Alignment::Center);
container(toolbar_content)
.width(Length::Fill)
.style(|theme: &Theme| {
let palette = theme.extended_palette();
container::Style {
background: Some(Background::Color(palette.background.weak.color)),
border: Border {
color: palette.background.strong.color,
width: 1.0,
radius: 6.0.into(),
},
..Default::default()
}
})
.into()
}
pub mod formatting {
pub fn bold(text: &str) -> String {
format!("**{}**", text)
}
pub fn italic(text: &str) -> String {
format!("*{}*", text)
}
pub fn strikethrough(text: &str) -> String {
format!("~~{}~~", text)
}
pub fn code(text: &str) -> String {
format!("`{}`", text)
}
pub fn heading(level: u8, text: &str) -> String {
let hashes = "#".repeat(level.min(6) as usize);
format!("{} {}", hashes, text)
}
pub fn bullet(text: &str) -> String {
format!("- {}", text)
}
pub fn numbered(num: usize, text: &str) -> String {
format!("{}. {}", num, text)
}
pub fn link(text: &str, url: &str) -> String {
format!("[{}]({})", text, url)
}
pub fn horizontal_rule() -> &'static str {
"---"
}
}