Merge dac4c649f7 into a5996b4e90
This commit is contained in:
commit
e015480c01
9 changed files with 886 additions and 0 deletions
20
Cargo.toml
20
Cargo.toml
|
|
@ -21,6 +21,14 @@ clipboard = ["iced_sctk?/clipboard"]
|
|||
dbus-config = ["cosmic-config/dbus", "dep:zbus", "cosmic-settings-daemon"]
|
||||
# Debug features
|
||||
debug = ["iced/debug"]
|
||||
# Enables markdown widget
|
||||
markdown = [
|
||||
"dep:cosmic-text",
|
||||
"dep:markdown",
|
||||
"dep:syntect",
|
||||
"dep:two-face",
|
||||
"dep:cosmic-syntax-theme"
|
||||
]
|
||||
# Enables pipewire support in ashpd, if ashpd is enabled
|
||||
pipewire = ["ashpd?/pipewire"]
|
||||
# Enables process spawning helper
|
||||
|
|
@ -83,16 +91,19 @@ derive_setters = "0.1.5"
|
|||
fraction = "0.14.0"
|
||||
image = { version = "0.25.1", optional = true }
|
||||
lazy_static = "1.4.0"
|
||||
markdown = { version = "0.3.0", optional = true }
|
||||
mime = { version = "0.3.17", optional = true }
|
||||
nix = { version = "0.27", features = ["process"], optional = true }
|
||||
palette = "0.7.3"
|
||||
rfd = { version = "0.14.0", optional = true }
|
||||
serde = { version = "1.0.180", features = ["derive"] }
|
||||
slotmap = "1.0.6"
|
||||
syntect = { version = "5.2.0", optional = true }
|
||||
textdistance = { version = "1.0.2", optional = true }
|
||||
thiserror = "1.0.44"
|
||||
tokio = { version = "1.24.2", optional = true }
|
||||
tracing = "0.1"
|
||||
two-face = { version = "0.4.0", optional = true }
|
||||
unicode-segmentation = "1.6"
|
||||
url = "2.4.0"
|
||||
zbus = { version = "4.2.1", default-features = false, optional = true }
|
||||
|
|
@ -102,6 +113,11 @@ freedesktop-icons = "0.2.5"
|
|||
freedesktop-desktop-entry = { version = "0.5.1", optional = true }
|
||||
shlex = { version = "1.3.0", optional = true }
|
||||
|
||||
[dependencies.cosmic-text]
|
||||
git = "https://github.com/pop-os/cosmic-text"
|
||||
default-features = false
|
||||
optional = true
|
||||
|
||||
[dependencies.cosmic-theme]
|
||||
path = "cosmic-theme"
|
||||
|
||||
|
|
@ -157,6 +173,10 @@ optional = true
|
|||
version = "0.8"
|
||||
optional = true
|
||||
|
||||
[dependencies.cosmic-syntax-theme]
|
||||
git = "https://github.com/pop-os/cosmic-syntax-theme.git"
|
||||
optional = true
|
||||
|
||||
[dependencies.taffy]
|
||||
git = "https://github.com/DioxusLabs/taffy"
|
||||
rev = "7781c70"
|
||||
|
|
|
|||
11
examples/markdown-widget/Cargo.toml
Normal file
11
examples/markdown-widget/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "markdown-widget"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dependencies.libcosmic]
|
||||
path = "../../"
|
||||
default-features = false
|
||||
features = ["winit", "wgpu", "markdown"]
|
||||
105
examples/markdown-widget/markdown.md
Normal file
105
examples/markdown-widget/markdown.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
# Introduction to Markdown
|
||||
|
||||
Markdown is a lightweight markup language with plain text formatting syntax. Its design allows it to be converted to many output formats, but the original tool by the same name only supports HTML. Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor.
|
||||
|
||||
## Benefits of Markdown
|
||||
|
||||
Markdown offers several advantages:
|
||||
|
||||
1. **Simplicity**: Markdown's syntax is simple and easy to learn.
|
||||
2. **Readability**: Even without formatting, Markdown documents are readable in plain text.
|
||||
3. **Portability**: Being plain text, Markdown files can be opened and edited in any text editor.
|
||||
4. **Flexibility**: It can be converted to various formats including HTML, PDF, and more.
|
||||
|
||||
## Basic Syntax
|
||||
|
||||
Here are some basic Markdown syntax elements:
|
||||
|
||||
- **Headings**: Use `#` for headings. More `#` characters indicate smaller headings.
|
||||
- **Bold**: Use `**` or `__` for bold text. Example: `**bold text**`
|
||||
- **Italic**: Use `*` or `_` for italic text. Example: `*italic text*`
|
||||
- **Lists**: Use `-` or `*` for unordered lists and numbers for ordered lists.
|
||||
|
||||
### Links
|
||||
|
||||
My favorite search engine is [Duck Duck Go](https://duckduckgo.com "The best search engine for privacy") privacy search engine.
|
||||
|
||||
### Images
|
||||
|
||||

|
||||
|
||||
## Example
|
||||
|
||||
# Heading One
|
||||
## Heading Two
|
||||
### Heading Three
|
||||
#### Heading Four
|
||||
##### Heading Five
|
||||
###### Heading Six
|
||||
|
||||
This is a paragraph with **bold text** and *italic text*.
|
||||
|
||||
> This is a block quote
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
|
||||
1. First
|
||||
2. Second
|
||||
3. Third
|
||||
|
||||
# Example Markdown Document
|
||||
|
||||
This is a sample markdown document demonstrating how to include an embedded code snippet.
|
||||
|
||||
|
||||
## Code Snippet
|
||||
|
||||
Below is an example of a simple Python function that adds two numbers:
|
||||
|
||||
```python
|
||||
def add(a, b):
|
||||
"""
|
||||
This function takes two numbers and returns their sum.
|
||||
|
||||
Parameters:
|
||||
a (int or float): The first number.
|
||||
b (int or float): The second number.
|
||||
|
||||
Returns:
|
||||
int or float: The sum of the two numbers.
|
||||
"""
|
||||
return a + b
|
||||
|
||||
# Example usage:
|
||||
result = add(3, 5)
|
||||
print(f"The sum of 3 and 5 is {result}")
|
||||
```
|
||||
|
||||
## and this is rust
|
||||
|
||||
```rust
|
||||
fn parse_span(span: Span) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
match span {
|
||||
markdown::Span::Break => result.push('\n'),
|
||||
markdown::Span::Text(text) => result.push_str(&text),
|
||||
markdown::Span::Code(code) => result.push_str(&code),
|
||||
markdown::Span::Link(_, _, _) => {}
|
||||
markdown::Span::Image(_, _, _) => {}
|
||||
markdown::Span::Emphasis(emphasis) => {
|
||||
for span in emphasis {
|
||||
result.push_str(&parse_span(span));
|
||||
}
|
||||
}
|
||||
markdown::Span::Strong(strong) => {
|
||||
for span in strong {
|
||||
result.push_str(&parse_span(span));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}```
|
||||
|
||||
10
examples/markdown-widget/src/main.rs
Normal file
10
examples/markdown-widget/src/main.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
mod window;
|
||||
|
||||
use cosmic::app::Settings;
|
||||
use window::Window;
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
let settings = Settings::default();
|
||||
|
||||
cosmic::app::run::<Window>(settings, ())
|
||||
}
|
||||
45
examples/markdown-widget/src/window.rs
Normal file
45
examples/markdown-widget/src/window.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use cosmic::{app::Core, widget, Application, Command};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {}
|
||||
|
||||
pub struct Window {
|
||||
core: Core,
|
||||
markdown_text: String,
|
||||
}
|
||||
|
||||
impl Application for Window {
|
||||
type Executor = cosmic::executor::Default;
|
||||
|
||||
type Flags = ();
|
||||
|
||||
type Message = Message;
|
||||
|
||||
const APP_ID: &'static str = "io.github.elevenhsoft.Markdown";
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn init(core: Core, _flags: Self::Flags) -> (Self, cosmic::app::Command<Self::Message>) {
|
||||
let markdown_text = include_str!("../markdown.md").to_string();
|
||||
|
||||
(
|
||||
Self {
|
||||
core,
|
||||
markdown_text,
|
||||
},
|
||||
Command::none(),
|
||||
)
|
||||
}
|
||||
|
||||
fn view(&self) -> cosmic::prelude::Element<Self::Message> {
|
||||
let md = widget::text::markdown(&self.markdown_text);
|
||||
|
||||
widget::row().push(widget::scrollable(md)).into()
|
||||
}
|
||||
}
|
||||
353
src/widget/markdown/mod.rs
Normal file
353
src/widget/markdown/mod.rs
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
pub mod parser;
|
||||
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::{Element, Renderer};
|
||||
use cosmic_text::{
|
||||
Attrs, Buffer, Color, Edit, Editor, Family, FontSystem, Metrics, Shaping, SwashCache, Weight,
|
||||
};
|
||||
use iced::{Length, Rectangle, Size};
|
||||
use iced_core::{
|
||||
image, layout,
|
||||
widget::{self, tree},
|
||||
Widget,
|
||||
};
|
||||
use markdown::tokenize;
|
||||
|
||||
static FONT_SYSTEM: OnceLock<Mutex<FontSystem>> = OnceLock::new();
|
||||
|
||||
pub struct Markdown {
|
||||
syntax_editor: Mutex<Editor<'static>>,
|
||||
swash_cache: Mutex<SwashCache>,
|
||||
margin: f32,
|
||||
}
|
||||
|
||||
impl Markdown {
|
||||
pub fn new(content: &str) -> Self {
|
||||
FONT_SYSTEM.get_or_init(|| Mutex::new(FontSystem::new()));
|
||||
|
||||
let metrics = metrics(14.0);
|
||||
let buffer = Buffer::new_empty(metrics);
|
||||
|
||||
let mut editor = Editor::new(buffer);
|
||||
let mut parser = parser::Parser::new();
|
||||
let blocks = tokenize(content);
|
||||
|
||||
parser.run(Box::leak(Box::new(blocks)));
|
||||
|
||||
editor.with_buffer_mut(|buffer| {
|
||||
set_buffer_text(
|
||||
&mut FONT_SYSTEM.get().unwrap().lock().unwrap(),
|
||||
&mut parser.get_spans(),
|
||||
buffer,
|
||||
)
|
||||
});
|
||||
|
||||
Self {
|
||||
syntax_editor: Mutex::new(editor),
|
||||
swash_cache: Mutex::new(SwashCache::new()),
|
||||
margin: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn margin(&mut self, margin: f32) {
|
||||
self.margin = margin;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
handle_opt: Mutex<Option<image::Handle>>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Creates a new [`State`].
|
||||
pub fn new() -> State {
|
||||
State {
|
||||
handle_opt: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message> Widget<Message, crate::Theme, Renderer> for Markdown {
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: Length::Shrink,
|
||||
height: Length::Shrink,
|
||||
}
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::new())
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
_tree: &mut widget::Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let mut font_system = FONT_SYSTEM.get().unwrap().lock().unwrap();
|
||||
let max_width = limits.max().width - self.margin;
|
||||
|
||||
let mut editor = self.syntax_editor.lock().unwrap();
|
||||
editor.borrow_with(&mut font_system).shape_as_needed(true);
|
||||
|
||||
editor.with_buffer_mut(|buffer| {
|
||||
let mut layout_lines = 0;
|
||||
let mut width = 0.0;
|
||||
let mut height = 0.0;
|
||||
|
||||
buffer.set_size(
|
||||
&mut font_system,
|
||||
Some(max_width),
|
||||
Some(buffer.metrics().line_height),
|
||||
);
|
||||
|
||||
buffer.set_wrap(&mut font_system, cosmic_text::Wrap::Word);
|
||||
|
||||
for line in buffer.lines.iter() {
|
||||
match line.layout_opt() {
|
||||
Some(layout) => {
|
||||
layout_lines += 1;
|
||||
|
||||
for l in layout.iter() {
|
||||
if layout_lines > 1 {
|
||||
width = max_width;
|
||||
|
||||
break;
|
||||
}
|
||||
width = l.w;
|
||||
}
|
||||
|
||||
for l in layout.iter() {
|
||||
if let Some(line_height) = l.line_height_opt {
|
||||
height += line_height;
|
||||
} else {
|
||||
height += buffer.metrics().line_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
|
||||
buffer.set_size(&mut font_system, Some(max_width), Some(height));
|
||||
|
||||
let size = Size::new(width, height);
|
||||
|
||||
layout::Node::new(size)
|
||||
})
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &crate::Theme,
|
||||
style: &iced_core::renderer::Style,
|
||||
layout: iced_core::Layout<'_>,
|
||||
_cursor: iced_core::mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
) {
|
||||
let state = tree.state.downcast_ref::<State>();
|
||||
|
||||
let mut swash_cache = self.swash_cache.lock().unwrap();
|
||||
let mut font_system = FONT_SYSTEM.get().unwrap().lock().unwrap();
|
||||
let mut editor = self.syntax_editor.lock().unwrap();
|
||||
|
||||
let scale_factor = style.scale_factor as f32;
|
||||
|
||||
let view_w = layout.bounds().width as i32;
|
||||
let view_h = layout.bounds().height as i32;
|
||||
|
||||
let calculate_image_scaled = |view: i32| -> (i32, f32) {
|
||||
// Get smallest set of physical pixels that fit inside the logical pixels
|
||||
let image = ((view as f32) * scale_factor).floor() as i32;
|
||||
// Convert that back into logical pixels
|
||||
let scaled = (image as f32) / scale_factor;
|
||||
(image, scaled)
|
||||
};
|
||||
let calculate_ideal = |view_start: i32| -> (i32, f32) {
|
||||
// Search for a perfect match within 16 pixels
|
||||
for i in 0..16 {
|
||||
let view = view_start - i;
|
||||
let (image, scaled) = calculate_image_scaled(view);
|
||||
if view == scaled as i32 {
|
||||
return (image, scaled);
|
||||
}
|
||||
}
|
||||
let (image, scaled) = calculate_image_scaled(view_start);
|
||||
(image, scaled)
|
||||
};
|
||||
|
||||
let (image_w, _scaled_w) = calculate_ideal(view_w);
|
||||
let (image_h, _scaled_h) = calculate_ideal(view_h);
|
||||
|
||||
editor.shape_as_needed(&mut font_system, true);
|
||||
|
||||
let mut pixels_u8 = vec![0; image_w as usize * image_h as usize * 4];
|
||||
|
||||
let pixels = unsafe {
|
||||
std::slice::from_raw_parts_mut(pixels_u8.as_mut_ptr() as *mut u32, pixels_u8.len() / 4)
|
||||
};
|
||||
|
||||
let mut handle_opt = state.handle_opt.lock().unwrap();
|
||||
|
||||
if editor.redraw() || handle_opt.is_none() {
|
||||
editor.with_buffer(|buffer| {
|
||||
buffer.draw(
|
||||
&mut font_system,
|
||||
&mut swash_cache,
|
||||
cosmic_text::Color(0xFFFFFF),
|
||||
|x, y, w, h, color| {
|
||||
draw_rect(
|
||||
pixels,
|
||||
Canvas {
|
||||
w: image_w,
|
||||
h: image_h,
|
||||
},
|
||||
Canvas {
|
||||
w: w as i32,
|
||||
h: h as i32,
|
||||
},
|
||||
Offset { x, y },
|
||||
color,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
*handle_opt = Some(image::Handle::from_pixels(
|
||||
image_w as u32,
|
||||
image_h as u32,
|
||||
pixels_u8,
|
||||
));
|
||||
|
||||
if let Some(ref handle) = *handle_opt {
|
||||
image::Renderer::draw(
|
||||
renderer,
|
||||
handle.clone(),
|
||||
image::FilterMethod::Nearest,
|
||||
Rectangle {
|
||||
x: layout.position().x,
|
||||
y: layout.position().y,
|
||||
width: image_w as f32,
|
||||
height: image_h as f32,
|
||||
},
|
||||
[0.0; 4],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> From<Markdown> for Element<'a, Message> {
|
||||
fn from(value: Markdown) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
struct Canvas {
|
||||
w: i32,
|
||||
h: i32,
|
||||
}
|
||||
|
||||
struct Offset {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
|
||||
fn draw_rect(
|
||||
buffer: &mut [u32],
|
||||
canvas: Canvas,
|
||||
offset: Canvas,
|
||||
screen: Offset,
|
||||
cosmic_color: cosmic_text::Color,
|
||||
) {
|
||||
// Grab alpha channel and green channel
|
||||
let mut color = cosmic_color.0 & 0xFF00FF00;
|
||||
// Shift red channel
|
||||
color |= (cosmic_color.0 & 0x00FF0000) >> 16;
|
||||
// Shift blue channel
|
||||
color |= (cosmic_color.0 & 0x000000FF) << 16;
|
||||
|
||||
let alpha = (color >> 24) & 0xFF;
|
||||
match alpha {
|
||||
0 => {
|
||||
// Do not draw if alpha is zero.
|
||||
}
|
||||
255 => {
|
||||
// Handle overwrite
|
||||
for x in screen.x..screen.x + offset.w {
|
||||
if x < 0 || x >= canvas.w {
|
||||
// Skip if y out of bounds
|
||||
continue;
|
||||
}
|
||||
|
||||
for y in screen.y..screen.y + offset.h {
|
||||
if y < 0 || y >= canvas.h {
|
||||
// Skip if x out of bounds
|
||||
continue;
|
||||
}
|
||||
|
||||
let line_offset = y as usize * canvas.w as usize;
|
||||
let offset = line_offset + x as usize;
|
||||
buffer[offset] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let n_alpha = 255 - alpha;
|
||||
for y in screen.y..screen.y + offset.h {
|
||||
if y < 0 || y >= canvas.h {
|
||||
// Skip if y out of bounds
|
||||
continue;
|
||||
}
|
||||
|
||||
let line_offset = y as usize * canvas.w as usize;
|
||||
for x in screen.x..screen.x + offset.w {
|
||||
if x < 0 || x >= canvas.w {
|
||||
// Skip if x out of bounds
|
||||
continue;
|
||||
}
|
||||
|
||||
// Alpha blend with current value
|
||||
let offset = line_offset + x as usize;
|
||||
let current = buffer[offset];
|
||||
if current & 0xFF000000 == 0 {
|
||||
// Overwrite if buffer empty
|
||||
buffer[offset] = color;
|
||||
} else {
|
||||
let rb = ((n_alpha * (current & 0x00FF00FF))
|
||||
+ (alpha * (color & 0x00FF00FF)))
|
||||
>> 8;
|
||||
let ag = (n_alpha * ((current & 0xFF00FF00) >> 8))
|
||||
+ (alpha * (0x01000000 | ((color & 0x0000FF00) >> 8)));
|
||||
buffer[offset] = (rb & 0x00FF00FF) | (ag & 0xFF00FF00);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn metrics(font_size: f32) -> Metrics {
|
||||
let line_height = (font_size * 1.4).ceil();
|
||||
Metrics::new(font_size, line_height)
|
||||
}
|
||||
|
||||
fn set_buffer_text(
|
||||
font_system: &mut FontSystem,
|
||||
collect_spans: &mut [(&'static str, Attrs)],
|
||||
buffer: &mut Buffer,
|
||||
) {
|
||||
let attrs = Attrs::new();
|
||||
attrs.family(Family::SansSerif);
|
||||
|
||||
buffer.set_rich_text(
|
||||
font_system,
|
||||
collect_spans.iter().copied(),
|
||||
attrs,
|
||||
Shaping::Advanced,
|
||||
)
|
||||
}
|
||||
331
src/widget/markdown/parser.rs
Normal file
331
src/widget/markdown/parser.rs
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
use std::io;
|
||||
|
||||
use cosmic_text::{
|
||||
Attrs, Buffer, Color, Edit, Editor, Family, FontSystem, Metrics, Shaping, Weight,
|
||||
};
|
||||
use markdown::{tokenize, Block, ListItem, Span};
|
||||
use syntect::{
|
||||
easy::HighlightLines,
|
||||
highlighting::{Style, Theme, ThemeSet},
|
||||
parsing::SyntaxSet,
|
||||
util::LinesWithEndings,
|
||||
};
|
||||
|
||||
use super::metrics;
|
||||
|
||||
pub struct Parser<'a, 'b> {
|
||||
spans: Vec<(&'a str, Attrs<'b>)>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> Parser<'a, 'b>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self { spans: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn get_spans(self) -> Vec<(&'a str, Attrs<'b>)> {
|
||||
self.spans
|
||||
}
|
||||
|
||||
pub fn run<'block>(&mut self, block: &'block Vec<Block>)
|
||||
where
|
||||
'block: 'b,
|
||||
{
|
||||
for block in block {
|
||||
self.parse_block(block, Attrs::new());
|
||||
self.spans.push(("\n", Attrs::new()));
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_block<'block>(&mut self, block: &'block Block, attrs: Attrs<'block>)
|
||||
where
|
||||
'block: 'b,
|
||||
{
|
||||
match block {
|
||||
Block::Header(span, level) => {
|
||||
for item in span {
|
||||
match level {
|
||||
1 => {
|
||||
let attrs = attrs.metrics(metrics(24.0));
|
||||
attrs.weight(Weight::BOLD);
|
||||
self.parse_span(item, attrs);
|
||||
}
|
||||
2 => {
|
||||
let attrs = attrs.metrics(metrics(22.0));
|
||||
attrs.weight(Weight::BOLD);
|
||||
self.parse_span(item, attrs);
|
||||
}
|
||||
3 => {
|
||||
let attrs = attrs.metrics(metrics(20.0));
|
||||
attrs.weight(Weight::BOLD);
|
||||
self.parse_span(item, attrs);
|
||||
}
|
||||
4 => {
|
||||
let attrs = attrs.metrics(metrics(18.0));
|
||||
attrs.weight(Weight::BOLD);
|
||||
self.parse_span(item, attrs);
|
||||
}
|
||||
5 => {
|
||||
let attrs = attrs.metrics(metrics(16.0));
|
||||
attrs.weight(Weight::BOLD);
|
||||
self.parse_span(item, attrs);
|
||||
}
|
||||
6 => {
|
||||
let attrs = attrs.metrics(metrics(14.0));
|
||||
attrs.weight(Weight::BOLD);
|
||||
self.parse_span(item, attrs);
|
||||
}
|
||||
_ => self.parse_span(item, Attrs::new()),
|
||||
}
|
||||
}
|
||||
self.spans.push(("\n", Attrs::new()));
|
||||
}
|
||||
Block::Paragraph(span) => {
|
||||
for item in span {
|
||||
self.parse_span(item, attrs);
|
||||
}
|
||||
self.spans.push(("\n", Attrs::new()));
|
||||
}
|
||||
Block::Blockquote(blockquote) => {
|
||||
for item in blockquote {
|
||||
let attrs = attrs.family(Family::Monospace);
|
||||
self.parse_block(item, attrs);
|
||||
}
|
||||
self.spans.push(("\n", Attrs::new()));
|
||||
}
|
||||
Block::CodeBlock(lang, code) => {
|
||||
let extension = if let Some(lang) = lang {
|
||||
language_to_extension(lang)
|
||||
} else {
|
||||
"txt"
|
||||
};
|
||||
|
||||
let attrs = attrs.family(Family::Monospace);
|
||||
let code_block = highlight_code(code, extension, attrs);
|
||||
|
||||
self.spans.extend(code_block);
|
||||
self.spans.push(("\n", attrs));
|
||||
}
|
||||
Block::OrderedList(listitem, _type) => {
|
||||
for (num, item) in listitem.iter().enumerate() {
|
||||
let attrs = attrs.family(Family::Serif);
|
||||
self.spans
|
||||
.push((Box::leak(Box::new(format!("{}. ", num + 1))), attrs));
|
||||
self.parse_listitem(item);
|
||||
self.spans.push(("\n", attrs));
|
||||
}
|
||||
self.spans.push(("\n", Attrs::new()));
|
||||
}
|
||||
Block::UnorderedList(listitem) => {
|
||||
for item in listitem {
|
||||
let attrs = attrs.family(Family::Serif);
|
||||
self.spans.push((" - ", attrs));
|
||||
self.parse_listitem(item);
|
||||
self.spans.push(("\n", attrs));
|
||||
}
|
||||
self.spans.push(("\n", Attrs::new()));
|
||||
}
|
||||
Block::Raw(raw_text) => {
|
||||
self.spans.push((raw_text, Attrs::new()));
|
||||
self.spans.push(("\n", Attrs::new()));
|
||||
}
|
||||
Block::Hr => self.spans.push(("\n", Attrs::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_span<'c>(&mut self, span: &'c Span, attrs: Attrs<'c>)
|
||||
where
|
||||
'c: 'b,
|
||||
{
|
||||
match span {
|
||||
Span::Break => self.spans.push(("\n", attrs)),
|
||||
Span::Text(text) => self.spans.push((text, attrs)),
|
||||
Span::Code(code) => {
|
||||
let attrs = attrs.family(Family::Monospace);
|
||||
self.spans.push((code, attrs));
|
||||
}
|
||||
Span::Link(name, url, _title) => {
|
||||
let color = Attrs::new().color(link_color());
|
||||
let spans: &[(&str, Attrs)] = &[
|
||||
(Box::leak(format!("{}: ", name).into_boxed_str()), attrs),
|
||||
(url, color),
|
||||
];
|
||||
self.spans.extend_from_slice(spans);
|
||||
}
|
||||
Span::Image(alt, url, title) => {
|
||||
let color = Attrs::new().color(link_color());
|
||||
let spans: &[(&str, Attrs)] = if let Some(title) = title {
|
||||
&[
|
||||
(Box::leak(format!("{}: ", title).into_boxed_str()), attrs),
|
||||
(url, color),
|
||||
]
|
||||
} else {
|
||||
&[
|
||||
(Box::leak(format!("{}: ", alt).into_boxed_str()), attrs),
|
||||
(url, color),
|
||||
]
|
||||
};
|
||||
self.spans.extend_from_slice(spans);
|
||||
}
|
||||
Span::Emphasis(emphasis) => {
|
||||
for item in emphasis {
|
||||
let attrs = attrs.family(Family::Cursive);
|
||||
self.parse_span(item, attrs);
|
||||
}
|
||||
}
|
||||
Span::Strong(strong) => {
|
||||
for item in strong {
|
||||
let attrs = attrs.weight(Weight::BOLD);
|
||||
self.parse_span(item, attrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_listitem<'d>(&mut self, item: &'d ListItem)
|
||||
where
|
||||
'd: 'b,
|
||||
{
|
||||
match item {
|
||||
ListItem::Simple(simple) => {
|
||||
for item in simple {
|
||||
self.parse_span(item, Attrs::new());
|
||||
}
|
||||
}
|
||||
ListItem::Paragraph(block) => {
|
||||
for item in block {
|
||||
self.parse_block(item, Attrs::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_code<'a>(
|
||||
code: &'a str,
|
||||
extension: &str,
|
||||
attrs: Attrs<'a>,
|
||||
) -> Vec<(&'a str, Attrs<'a>)> {
|
||||
let mut result: Vec<(&'a str, Attrs)> = Vec::new();
|
||||
|
||||
let ps = SyntaxSet::load_defaults_newlines();
|
||||
let syntax_theme = syntax_theme();
|
||||
|
||||
let syntax = ps.find_syntax_by_extension(extension).unwrap();
|
||||
let mut h = HighlightLines::new(syntax, &syntax_theme);
|
||||
for line in LinesWithEndings::from(code) {
|
||||
let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap();
|
||||
|
||||
for (style, text) in ranges {
|
||||
let fg = style.foreground;
|
||||
let color = Color::rgb(fg.r, fg.g, fg.b);
|
||||
|
||||
let attrs = attrs.color(color);
|
||||
result.push((text, attrs));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn link_color() -> Color {
|
||||
if !crate::theme::is_dark() {
|
||||
return Color::rgb(5, 200, 95);
|
||||
}
|
||||
|
||||
Color::rgb(125, 206, 243)
|
||||
}
|
||||
|
||||
fn syntax_theme() -> Theme {
|
||||
let ts = cosmic_syntax();
|
||||
|
||||
if !crate::theme::is_dark() {
|
||||
return ts.themes.get("COSMIC Light").unwrap().clone();
|
||||
}
|
||||
|
||||
ts.themes.get("COSMIC Dark").unwrap().clone()
|
||||
}
|
||||
|
||||
fn cosmic_syntax() -> ThemeSet {
|
||||
let lazy_theme_set = two_face::theme::LazyThemeSet::from(two_face::theme::extra());
|
||||
let mut theme_set = syntect::highlighting::ThemeSet::from(&lazy_theme_set);
|
||||
// Hardcoded COSMIC themes
|
||||
for (theme_name, theme_data) in &[
|
||||
("COSMIC Dark", cosmic_syntax_theme::COSMIC_DARK_TM_THEME),
|
||||
("COSMIC Light", cosmic_syntax_theme::COSMIC_LIGHT_TM_THEME),
|
||||
] {
|
||||
let mut cursor = io::Cursor::new(theme_data);
|
||||
match syntect::highlighting::ThemeSet::load_from_reader(&mut cursor) {
|
||||
Ok(mut theme) => {
|
||||
// Use libcosmic theme for background and gutter
|
||||
theme.settings.background = Some(syntect::highlighting::Color {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 0,
|
||||
});
|
||||
theme.settings.gutter = Some(syntect::highlighting::Color {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 0,
|
||||
});
|
||||
theme_set.themes.insert(theme_name.to_string(), theme);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed to load {:?} syntax theme: {}", theme_name, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
theme_set
|
||||
}
|
||||
|
||||
fn language_to_extension(lang: &str) -> &'static str {
|
||||
match lang {
|
||||
"python" => "py",
|
||||
"javascript" => "js",
|
||||
"java" => "java",
|
||||
"c" => "c",
|
||||
"cpp" => "cpp",
|
||||
"c++" => "cpp",
|
||||
"csharp" => "cs",
|
||||
"c#" => "cs",
|
||||
"php" => "php",
|
||||
"ruby" => "rb",
|
||||
"swift" => "swift",
|
||||
"kotlin" => "kt",
|
||||
"go" => "go",
|
||||
"r" => "r",
|
||||
"perl" => "pl",
|
||||
"shell" => "sh",
|
||||
"bash" => "sh",
|
||||
"objective-c" => "m",
|
||||
"objective-c++" => "mm",
|
||||
"typescript" => "ts",
|
||||
"html" => "html",
|
||||
"css" => "css",
|
||||
"sql" => "sql",
|
||||
"matlab" => "m",
|
||||
"scala" => "scala",
|
||||
"rust" => "rs",
|
||||
"dart" => "dart",
|
||||
"elixir" => "ex",
|
||||
"haskell" => "hs",
|
||||
"lua" => "lua",
|
||||
"assembly" => "asm",
|
||||
"fortran" => "f90",
|
||||
"pascal" => "pas",
|
||||
"cobol" => "cob",
|
||||
"erlang" => "erl",
|
||||
"fsharp" => "fs",
|
||||
"f#" => "fs",
|
||||
"julia" => "jl",
|
||||
"groovy" => "groovy",
|
||||
"ada" => "adb",
|
||||
"markdown" => "md",
|
||||
_ => "txt",
|
||||
}
|
||||
}
|
||||
|
|
@ -247,6 +247,9 @@ pub mod list;
|
|||
#[doc(inline)]
|
||||
pub use list::{list_column, ListColumn};
|
||||
|
||||
#[cfg(feature = "markdown")]
|
||||
pub mod markdown;
|
||||
|
||||
pub mod menu;
|
||||
|
||||
pub mod nav_bar;
|
||||
|
|
|
|||
|
|
@ -90,3 +90,11 @@ pub fn monotext<'a>(text: impl Into<Cow<'a, str>> + 'a) -> Text<'a, crate::Theme
|
|||
.line_height(LineHeight::Absolute(20.0.into()))
|
||||
.font(crate::font::FONT_MONO_REGULAR)
|
||||
}
|
||||
|
||||
#[cfg(feature = "markdown")]
|
||||
/// [`Markdown`] widget with md text parser
|
||||
pub fn markdown(content: &str) -> crate::widget::markdown::Markdown {
|
||||
use crate::widget::markdown;
|
||||
|
||||
markdown::Markdown::new(content)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue