content: Add MessageText widget
Start to separate code for MessageRow, to avoid having every message type implementation in the same file.
This commit is contained in:
parent
622850cb21
commit
b653ca7933
4 changed files with 434 additions and 189 deletions
|
@ -75,6 +75,7 @@ src/session/content/item_row.rs
|
|||
src/session/content/invite.rs
|
||||
src/session/content/markdown_popover.rs
|
||||
src/session/content/message_row/mod.rs
|
||||
src/session/content/message_row/text.rs
|
||||
src/session/content/mod.rs
|
||||
src/session/content/room_details/mod.rs
|
||||
src/session/content/room_history.rs
|
||||
|
|
|
@ -58,6 +58,7 @@ sources = files(
|
|||
'session/content/invite.rs',
|
||||
'session/content/markdown_popover.rs',
|
||||
'session/content/message_row/mod.rs',
|
||||
'session/content/message_row/text.rs',
|
||||
'session/content/mod.rs',
|
||||
'session/content/room_history.rs',
|
||||
'session/content/room_details/mod.rs',
|
||||
|
|
|
@ -1,22 +1,19 @@
|
|||
mod text;
|
||||
|
||||
use crate::components::Avatar;
|
||||
use adw::{prelude::*, subclass::prelude::*};
|
||||
use gettextrs::gettext;
|
||||
use gtk::{
|
||||
gio, glib, glib::clone, glib::signal::SignalHandlerId, pango, subclass::prelude::*,
|
||||
CompositeTemplate,
|
||||
};
|
||||
use html2pango::{
|
||||
block::{markup_html, HtmlBlock},
|
||||
html_escape, markup_links,
|
||||
glib, glib::clone, glib::signal::SignalHandlerId, subclass::prelude::*, CompositeTemplate,
|
||||
};
|
||||
use log::warn;
|
||||
use matrix_sdk::ruma::events::{
|
||||
room::message::{FormattedBody, MessageFormat, MessageType, Relation},
|
||||
room::message::{MessageType, Relation},
|
||||
room::redaction::RoomRedactionEventContent,
|
||||
AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent,
|
||||
};
|
||||
use sourceview::prelude::*;
|
||||
|
||||
use self::text::MessageText;
|
||||
use crate::prelude::*;
|
||||
use crate::session::room::Event;
|
||||
|
||||
|
@ -243,10 +240,12 @@ impl MessageRow {
|
|||
}
|
||||
|
||||
fn update_content(&self, event: &Event) {
|
||||
let priv_ = imp::MessageRow::from_instance(self);
|
||||
let content = self.find_content(event);
|
||||
|
||||
// TODO: create widgets for all event types
|
||||
// TODO: display reaction events from event.relates_to()
|
||||
// TODO: we should reuse the already present child widgets when possible
|
||||
|
||||
match content {
|
||||
Some(AnyMessageEventContent::RoomMessage(message)) => {
|
||||
|
@ -258,58 +257,24 @@ impl MessageRow {
|
|||
match msgtype {
|
||||
MessageType::Audio(_message) => {}
|
||||
MessageType::Emote(message) => {
|
||||
// TODO we need to bind the display name to the sender
|
||||
if let Some(html_blocks) = message
|
||||
.formatted
|
||||
.filter(|formatted| is_valid_formatted_body(formatted))
|
||||
.and_then(|formatted| {
|
||||
let body = FormattedBody {
|
||||
body: format!(
|
||||
"<b>{}</b> {}",
|
||||
event.sender().display_name(),
|
||||
formatted.body
|
||||
),
|
||||
format: MessageFormat::Html,
|
||||
};
|
||||
|
||||
parse_formatted_body(Some(&body))
|
||||
})
|
||||
{
|
||||
self.show_html(html_blocks);
|
||||
} else {
|
||||
self.show_text(
|
||||
&format!(
|
||||
"<b>{}</b> {}",
|
||||
event.sender().display_name(),
|
||||
linkify(&message.body)
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
let child =
|
||||
MessageText::emote(message.formatted, message.body, event.sender());
|
||||
priv_.content.set_child(Some(&child));
|
||||
}
|
||||
MessageType::File(_message) => {}
|
||||
MessageType::Image(_message) => {}
|
||||
MessageType::Location(_message) => {}
|
||||
MessageType::Notice(message) => {
|
||||
// TODO: we should reuse the already present child widgets when possible
|
||||
if let Some(html_blocks) = parse_formatted_body(message.formatted.as_ref())
|
||||
{
|
||||
self.show_html(html_blocks);
|
||||
} else {
|
||||
self.show_text(&linkify(&message.body), true)
|
||||
};
|
||||
let child = MessageText::markup(message.formatted, message.body);
|
||||
priv_.content.set_child(Some(&child));
|
||||
}
|
||||
MessageType::ServerNotice(message) => {
|
||||
self.show_text(&message.body, false);
|
||||
let child = MessageText::text(message.body);
|
||||
priv_.content.set_child(Some(&child));
|
||||
}
|
||||
MessageType::Text(message) => {
|
||||
// TODO: we should reuse the already present child widgets when possible
|
||||
if let Some(html_blocks) = parse_formatted_body(message.formatted.as_ref())
|
||||
{
|
||||
self.show_html(html_blocks);
|
||||
} else {
|
||||
self.show_text(&linkify(&message.body), true)
|
||||
};
|
||||
let child = MessageText::markup(message.formatted, message.body);
|
||||
priv_.content.set_child(Some(&child));
|
||||
}
|
||||
MessageType::Video(_message) => {}
|
||||
MessageType::VerificationRequest(_message) => {}
|
||||
|
@ -320,149 +285,17 @@ impl MessageRow {
|
|||
}
|
||||
Some(AnyMessageEventContent::RoomEncrypted(content)) => {
|
||||
warn!("Couldn't decrypt event {:?}", content);
|
||||
self.show_text(&gettext("Fractal couldn't decrypt this message."), false)
|
||||
let child = MessageText::text(gettext("Fractal couldn't decrypt this message."));
|
||||
priv_.content.set_child(Some(&child));
|
||||
}
|
||||
Some(AnyMessageEventContent::RoomRedaction(_)) => {
|
||||
self.show_text(&gettext("This message was removed."), false)
|
||||
}
|
||||
_ => self.show_text(&gettext("Unsupported event"), false),
|
||||
}
|
||||
}
|
||||
|
||||
fn show_text(&self, text: &str, use_markup: bool) {
|
||||
let priv_ = imp::MessageRow::from_instance(self);
|
||||
|
||||
let child =
|
||||
if let Some(Ok(child)) = priv_.content.child().map(|w| w.downcast::<gtk::Label>()) {
|
||||
child
|
||||
} else {
|
||||
let child = gtk::Label::new(None);
|
||||
set_label_styles(&child);
|
||||
let child = MessageText::text(gettext("This message was removed."));
|
||||
priv_.content.set_child(Some(&child));
|
||||
child
|
||||
};
|
||||
|
||||
if use_markup {
|
||||
child.set_markup(text);
|
||||
} else {
|
||||
child.set_text(text);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_html(&self, blocks: Vec<HtmlBlock>) {
|
||||
let priv_ = imp::MessageRow::from_instance(self);
|
||||
|
||||
let child = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
priv_.content.set_child(Some(&child));
|
||||
|
||||
for block in blocks {
|
||||
let widget = create_widget_for_html_block(&block);
|
||||
child.append(&widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn linkify(text: &str) -> String {
|
||||
markup_links(&html_escape(text))
|
||||
}
|
||||
|
||||
fn is_valid_formatted_body(formatted: &FormattedBody) -> bool {
|
||||
formatted.format == MessageFormat::Html && !formatted.body.contains("<!-- raw HTML omitted -->")
|
||||
}
|
||||
|
||||
fn parse_formatted_body(formatted: Option<&FormattedBody>) -> Option<Vec<HtmlBlock>> {
|
||||
formatted
|
||||
.filter(|formatted| is_valid_formatted_body(formatted))
|
||||
.and_then(|formatted| markup_html(&formatted.body).ok())
|
||||
}
|
||||
|
||||
fn set_label_styles(w: >k::Label) {
|
||||
w.set_wrap(true);
|
||||
w.set_wrap_mode(pango::WrapMode::WordChar);
|
||||
w.set_justify(gtk::Justification::Left);
|
||||
w.set_xalign(0.0);
|
||||
w.set_valign(gtk::Align::Start);
|
||||
w.set_halign(gtk::Align::Fill);
|
||||
w.set_selectable(true);
|
||||
let menu_model: Option<gio::MenuModel> =
|
||||
gtk::Builder::from_resource("/org/gnome/FractalNext/content-item-row-menu.ui")
|
||||
.object("menu_model");
|
||||
w.set_extra_menu(menu_model.as_ref());
|
||||
}
|
||||
|
||||
fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
|
||||
match block {
|
||||
HtmlBlock::Heading(n, s) => {
|
||||
let w = gtk::Label::new(None);
|
||||
set_label_styles(&w);
|
||||
w.set_markup(s);
|
||||
w.add_css_class(&format!("h{}", n));
|
||||
w.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::UList(elements) => {
|
||||
let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
bx.set_margin_end(6);
|
||||
bx.set_margin_start(6);
|
||||
|
||||
for li in elements.iter() {
|
||||
let h_box = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
let bullet = gtk::Label::new(Some("•"));
|
||||
bullet.set_valign(gtk::Align::Start);
|
||||
let w = gtk::Label::new(None);
|
||||
set_label_styles(&w);
|
||||
h_box.append(&bullet);
|
||||
h_box.append(&w);
|
||||
w.set_markup(li);
|
||||
bx.append(&h_box);
|
||||
}
|
||||
|
||||
bx.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::OList(elements) => {
|
||||
let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
bx.set_margin_end(6);
|
||||
bx.set_margin_start(6);
|
||||
|
||||
for (i, ol) in elements.iter().enumerate() {
|
||||
let h_box = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
let bullet = gtk::Label::new(Some(&format!("{}.", i + 1)));
|
||||
bullet.set_valign(gtk::Align::Start);
|
||||
let w = gtk::Label::new(None);
|
||||
set_label_styles(&w);
|
||||
h_box.append(&bullet);
|
||||
h_box.append(&w);
|
||||
w.set_markup(ol);
|
||||
bx.append(&h_box);
|
||||
_ => {
|
||||
let child = MessageText::text(gettext("Unsupported event"));
|
||||
priv_.content.set_child(Some(&child));
|
||||
}
|
||||
|
||||
bx.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::Code(s) => {
|
||||
let scrolled = gtk::ScrolledWindow::new();
|
||||
scrolled.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never);
|
||||
let buffer = sourceview::Buffer::new(None);
|
||||
buffer.set_highlight_matching_brackets(false);
|
||||
buffer.set_text(s);
|
||||
let view = sourceview::View::with_buffer(&buffer);
|
||||
view.set_editable(false);
|
||||
view.add_css_class("codeview");
|
||||
scrolled.set_child(Some(&view));
|
||||
scrolled.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::Quote(blocks) => {
|
||||
let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
bx.add_css_class("quote");
|
||||
for block in blocks.iter() {
|
||||
let w = create_widget_for_html_block(block);
|
||||
bx.append(&w);
|
||||
}
|
||||
bx.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::Text(s) => {
|
||||
let w = gtk::Label::new(None);
|
||||
set_label_styles(&w);
|
||||
w.set_markup(s);
|
||||
w.upcast::<gtk::Widget>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
410
src/session/content/message_row/text.rs
Normal file
410
src/session/content/message_row/text.rs
Normal file
|
@ -0,0 +1,410 @@
|
|||
use adw::{prelude::BinExt, subclass::prelude::*};
|
||||
use gtk::{gio, glib, pango, prelude::*, subclass::prelude::*};
|
||||
use html2pango::{
|
||||
block::{markup_html, HtmlBlock},
|
||||
html_escape, markup_links,
|
||||
};
|
||||
use matrix_sdk::ruma::events::room::message::{FormattedBody, MessageFormat};
|
||||
use sourceview::prelude::*;
|
||||
|
||||
use crate::session::{room::Member, UserExt};
|
||||
|
||||
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, glib::GEnum)]
|
||||
#[repr(u32)]
|
||||
#[genum(type_name = "TextFormat")]
|
||||
pub enum TextFormat {
|
||||
Text = 0,
|
||||
Markup = 1,
|
||||
Html = 2,
|
||||
Emote = 3,
|
||||
HtmlEmote = 4,
|
||||
}
|
||||
|
||||
impl Default for TextFormat {
|
||||
fn default() -> Self {
|
||||
TextFormat::Text
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
use super::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::cell::{Cell, RefCell};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MessageText {
|
||||
/// The format of the text message.
|
||||
pub format: Cell<TextFormat>,
|
||||
/// The displayed content of the message.
|
||||
pub body: RefCell<Option<String>>,
|
||||
/// The sender of the message(only used for emotes).
|
||||
pub sender: RefCell<Option<Member>>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for MessageText {
|
||||
const NAME: &'static str = "ContentMessageText";
|
||||
type Type = super::MessageText;
|
||||
type ParentType = adw::Bin;
|
||||
}
|
||||
|
||||
impl ObjectImpl for MessageText {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![
|
||||
glib::ParamSpec::new_enum(
|
||||
"format",
|
||||
"Format",
|
||||
"The format of the text message",
|
||||
TextFormat::static_type(),
|
||||
TextFormat::default() as i32,
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
glib::ParamSpec::new_string(
|
||||
"body",
|
||||
"Body",
|
||||
"The displayed content of the message",
|
||||
None,
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::CONSTRUCT_ONLY,
|
||||
),
|
||||
glib::ParamSpec::new_object(
|
||||
"sender",
|
||||
"Sender",
|
||||
"The sender of the message",
|
||||
Member::static_type(),
|
||||
glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
|
||||
),
|
||||
]
|
||||
});
|
||||
|
||||
PROPERTIES.as_ref()
|
||||
}
|
||||
|
||||
fn set_property(
|
||||
&self,
|
||||
obj: &Self::Type,
|
||||
_id: usize,
|
||||
value: &glib::Value,
|
||||
pspec: &glib::ParamSpec,
|
||||
) {
|
||||
match pspec.name() {
|
||||
"format" => obj.set_format(value.get().unwrap()),
|
||||
"body" => obj.set_body(value.get().unwrap()),
|
||||
"sender" => obj.set_sender(value.get().unwrap()),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
|
||||
match pspec.name() {
|
||||
"format" => obj.format().to_value(),
|
||||
"body" => obj.body().to_value(),
|
||||
"sender" => obj.sender().to_value(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructed(&self, obj: &Self::Type) {
|
||||
self.parent_constructed(obj);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for MessageText {}
|
||||
|
||||
impl BinImpl for MessageText {}
|
||||
}
|
||||
|
||||
glib::wrapper! {
|
||||
/// A widget displaying the content of a text message.
|
||||
pub struct MessageText(ObjectSubclass<imp::MessageText>)
|
||||
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
|
||||
}
|
||||
|
||||
impl MessageText {
|
||||
// Creates a widget that displays plain text.
|
||||
pub fn text(body: String) -> Self {
|
||||
glib::Object::new(&[("body", &body)]).expect("Failed to create MessageText")
|
||||
}
|
||||
|
||||
// Creates a widget that displays text with markup. It will detect if it should display the body or the formatted body.
|
||||
pub fn markup(formatted: Option<FormattedBody>, body: String) -> Self {
|
||||
if let Some((html_blocks, body)) = formatted
|
||||
.filter(|formatted| is_valid_formatted_body(formatted))
|
||||
.and_then(|formatted| {
|
||||
parse_formatted_body(&formatted.body)
|
||||
.and_then(|blocks| Some((blocks, formatted.body)))
|
||||
})
|
||||
{
|
||||
let self_: Self = glib::Object::new(&[("format", &TextFormat::Html), ("body", &body)])
|
||||
.expect("Failed to create MessageText");
|
||||
|
||||
self_.build_html(html_blocks);
|
||||
self_
|
||||
} else {
|
||||
let self_: Self =
|
||||
glib::Object::new(&[("format", &TextFormat::Markup), ("body", &linkify(&body))])
|
||||
.expect("Failed to create MessageText");
|
||||
|
||||
self_.build();
|
||||
self_
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a widget that displays an emote. It will detect if it should display the body or the formatted body.
|
||||
pub fn emote(formatted: Option<FormattedBody>, body: String, sender: Member) -> Self {
|
||||
if let Some(body) = formatted
|
||||
.filter(|formatted| is_valid_formatted_body(formatted))
|
||||
.and_then(|formatted| {
|
||||
let body = format!("<b>{}</b> {}", sender.display_name(), formatted.body);
|
||||
|
||||
parse_formatted_body(&body).and_then(|_| Some(formatted.body))
|
||||
})
|
||||
{
|
||||
glib::Object::new(&[
|
||||
("format", &TextFormat::HtmlEmote),
|
||||
("body", &body),
|
||||
("sender", &Some(sender)),
|
||||
])
|
||||
.expect("Failed to create MessageText")
|
||||
} else {
|
||||
glib::Object::new(&[
|
||||
("format", &TextFormat::Emote),
|
||||
("body", &linkify(&body)),
|
||||
("sender", &Some(sender)),
|
||||
])
|
||||
.expect("Failed to create MessageText")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_format(&self, format: TextFormat) {
|
||||
let priv_ = imp::MessageText::from_instance(self);
|
||||
|
||||
if format == priv_.format.get() {
|
||||
return;
|
||||
}
|
||||
|
||||
priv_.format.set(format);
|
||||
}
|
||||
|
||||
pub fn format(&self) -> TextFormat {
|
||||
let priv_ = imp::MessageText::from_instance(self);
|
||||
priv_.format.get()
|
||||
}
|
||||
|
||||
pub fn set_body(&self, body: Option<String>) {
|
||||
let priv_ = imp::MessageText::from_instance(self);
|
||||
|
||||
if body.as_ref() == priv_.body.borrow().as_ref() {
|
||||
return;
|
||||
}
|
||||
|
||||
priv_.body.replace(body);
|
||||
}
|
||||
|
||||
pub fn body(&self) -> Option<String> {
|
||||
let priv_ = imp::MessageText::from_instance(self);
|
||||
priv_.body.borrow().to_owned()
|
||||
}
|
||||
|
||||
pub fn set_sender(&self, sender: Option<Member>) {
|
||||
let priv_ = imp::MessageText::from_instance(self);
|
||||
|
||||
if sender.as_ref() == priv_.sender.borrow().as_ref() {
|
||||
return;
|
||||
}
|
||||
|
||||
priv_.sender.replace(sender);
|
||||
|
||||
if self.format() == TextFormat::Emote || self.format() == TextFormat::HtmlEmote {
|
||||
self.build();
|
||||
}
|
||||
|
||||
self.notify("sender");
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> Option<Member> {
|
||||
let priv_ = imp::MessageText::from_instance(self);
|
||||
priv_.sender.borrow().to_owned()
|
||||
}
|
||||
|
||||
fn build(&self) {
|
||||
match self.format() {
|
||||
TextFormat::Text => {
|
||||
self.build_text(&self.body().unwrap(), false);
|
||||
}
|
||||
TextFormat::Markup => {
|
||||
self.build_text(&self.body().unwrap(), true);
|
||||
}
|
||||
TextFormat::Html => {
|
||||
let formatted = FormattedBody {
|
||||
body: self.body().unwrap(),
|
||||
format: MessageFormat::Html,
|
||||
};
|
||||
|
||||
let html = parse_formatted_body(&formatted.body).unwrap();
|
||||
self.build_html(html);
|
||||
}
|
||||
TextFormat::Emote => {
|
||||
// TODO: we need to bind the display name to the sender
|
||||
self.build_text(
|
||||
&format!(
|
||||
"<b>{}</b> {}",
|
||||
self.sender().unwrap().display_name(),
|
||||
&self.body().unwrap()
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
TextFormat::HtmlEmote => {
|
||||
// TODO: we need to bind the display name to the sender
|
||||
let formatted = FormattedBody {
|
||||
body: format!(
|
||||
"<b>{}</b> {}",
|
||||
self.sender().unwrap().display_name(),
|
||||
self.body().unwrap()
|
||||
),
|
||||
format: MessageFormat::Html,
|
||||
};
|
||||
|
||||
let html = parse_formatted_body(&formatted.body).unwrap();
|
||||
self.build_html(html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_text(&self, text: &str, use_markup: bool) {
|
||||
let child = if let Some(Ok(child)) = self.child().map(|w| w.downcast::<gtk::Label>()) {
|
||||
child
|
||||
} else {
|
||||
let child = gtk::Label::new(None);
|
||||
set_label_styles(&child);
|
||||
self.set_child(Some(&child));
|
||||
child
|
||||
};
|
||||
|
||||
if use_markup {
|
||||
child.set_markup(text);
|
||||
} else {
|
||||
child.set_text(text);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_html(&self, blocks: Vec<HtmlBlock>) {
|
||||
let child = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
self.set_child(Some(&child));
|
||||
|
||||
for block in blocks {
|
||||
let widget = create_widget_for_html_block(&block);
|
||||
child.append(&widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn linkify(text: &str) -> String {
|
||||
markup_links(&html_escape(text))
|
||||
}
|
||||
|
||||
fn is_valid_formatted_body(formatted: &FormattedBody) -> bool {
|
||||
formatted.format == MessageFormat::Html && !formatted.body.contains("<!-- raw HTML omitted -->")
|
||||
}
|
||||
|
||||
fn parse_formatted_body(formatted: &str) -> Option<Vec<HtmlBlock>> {
|
||||
markup_html(formatted).ok()
|
||||
}
|
||||
|
||||
fn set_label_styles(w: >k::Label) {
|
||||
w.set_wrap(true);
|
||||
w.set_wrap_mode(pango::WrapMode::WordChar);
|
||||
w.set_justify(gtk::Justification::Left);
|
||||
w.set_xalign(0.0);
|
||||
w.set_valign(gtk::Align::Start);
|
||||
w.set_halign(gtk::Align::Fill);
|
||||
w.set_selectable(true);
|
||||
let menu_model: Option<gio::MenuModel> =
|
||||
gtk::Builder::from_resource("/org/gnome/FractalNext/content-item-row-menu.ui")
|
||||
.object("menu_model");
|
||||
w.set_extra_menu(menu_model.as_ref());
|
||||
}
|
||||
|
||||
fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
|
||||
match block {
|
||||
HtmlBlock::Heading(n, s) => {
|
||||
let w = gtk::Label::new(None);
|
||||
set_label_styles(&w);
|
||||
w.set_markup(s);
|
||||
w.add_css_class(&format!("h{}", n));
|
||||
w.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::UList(elements) => {
|
||||
let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
bx.set_margin_end(6);
|
||||
bx.set_margin_start(6);
|
||||
|
||||
for li in elements.iter() {
|
||||
let h_box = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
let bullet = gtk::Label::new(Some("•"));
|
||||
bullet.set_valign(gtk::Align::Start);
|
||||
let w = gtk::Label::new(None);
|
||||
set_label_styles(&w);
|
||||
h_box.append(&bullet);
|
||||
h_box.append(&w);
|
||||
w.set_markup(li);
|
||||
bx.append(&h_box);
|
||||
}
|
||||
|
||||
bx.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::OList(elements) => {
|
||||
let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
bx.set_margin_end(6);
|
||||
bx.set_margin_start(6);
|
||||
|
||||
for (i, ol) in elements.iter().enumerate() {
|
||||
let h_box = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||
let bullet = gtk::Label::new(Some(&format!("{}.", i + 1)));
|
||||
bullet.set_valign(gtk::Align::Start);
|
||||
let w = gtk::Label::new(None);
|
||||
set_label_styles(&w);
|
||||
h_box.append(&bullet);
|
||||
h_box.append(&w);
|
||||
w.set_markup(ol);
|
||||
bx.append(&h_box);
|
||||
}
|
||||
|
||||
bx.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::Code(s) => {
|
||||
let scrolled = gtk::ScrolledWindow::new();
|
||||
scrolled.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never);
|
||||
let buffer = sourceview::Buffer::new(None);
|
||||
buffer.set_highlight_matching_brackets(false);
|
||||
buffer.set_text(s);
|
||||
let view = sourceview::View::with_buffer(&buffer);
|
||||
view.set_editable(false);
|
||||
view.add_css_class("codeview");
|
||||
scrolled.set_child(Some(&view));
|
||||
scrolled.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::Quote(blocks) => {
|
||||
let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
|
||||
bx.add_css_class("quote");
|
||||
for block in blocks.iter() {
|
||||
let w = create_widget_for_html_block(block);
|
||||
bx.append(&w);
|
||||
}
|
||||
bx.upcast::<gtk::Widget>()
|
||||
}
|
||||
HtmlBlock::Text(s) => {
|
||||
let w = gtk::Label::new(None);
|
||||
set_label_styles(&w);
|
||||
w.set_markup(s);
|
||||
w.upcast::<gtk::Widget>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MessageText {
|
||||
fn default() -> Self {
|
||||
Self::text(format!(""))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue