-
Notifications
You must be signed in to change notification settings - Fork 58
feat: add aliasable hyperlinks #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ members = [ | |
| "colors_rgb", | ||
| "demo", | ||
| "demo2", | ||
| "hyperlinks", | ||
| "minimal", | ||
| "pong", | ||
| "shared", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| [package] | ||
| name = "hyperlinks" | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
| publish = false | ||
|
|
||
| [dependencies] | ||
| ratzilla.workspace = true | ||
| console_error_panic_hook.workspace = true |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta | ||
| name="viewport" | ||
| content="width=device-width, initial-scale=1.0, user-scalable=no" | ||
| /> | ||
| <link | ||
| rel="stylesheet" | ||
| href="https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/fira_code.min.css" | ||
| /> | ||
| <title>Ratzilla Hyperlinks</title> | ||
| <style> | ||
| body { | ||
| margin: 0; | ||
| width: 100%; | ||
| height: 100vh; | ||
| display: flex; | ||
| flex-direction: column; | ||
| justify-content: center; | ||
| align-items: center; | ||
| align-content: center; | ||
| background-color: #121212; | ||
| } | ||
|
|
||
| pre { | ||
| font-family: "Fira Code", monospace; | ||
| font-size: 16px; | ||
| margin: 0; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body></body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| use std::io; | ||
|
|
||
| use ratzilla::{ | ||
| ratatui::{ | ||
| layout::{Alignment, Constraint, Layout, Rect}, | ||
| prelude::{Color, Stylize, Terminal}, | ||
| widgets::{Block, BorderType, Clear, Paragraph}, | ||
| }, | ||
| widgets::Hyperlink, | ||
| DomBackend, WebRenderer, | ||
| }; | ||
|
|
||
| fn main() -> io::Result<()> { | ||
| std::panic::set_hook(Box::new(console_error_panic_hook::hook)); | ||
|
|
||
| let terminal = Terminal::new(DomBackend::new()?)?; | ||
| terminal.draw_web(|frame| { | ||
| frame.render_widget(Clear, frame.area()); | ||
|
|
||
| let [card] = Layout::vertical([Constraint::Length(9)]) | ||
| .flex(ratzilla::ratatui::layout::Flex::Center) | ||
| .areas(frame.area()); | ||
| let [card] = Layout::horizontal([Constraint::Length(44)]) | ||
| .flex(ratzilla::ratatui::layout::Flex::Center) | ||
| .areas(card); | ||
|
|
||
| frame.render_widget( | ||
| Block::bordered() | ||
| .border_type(BorderType::Rounded) | ||
| .title(" Aliasable Hyperlinks ".bold()) | ||
| .border_style(Color::LightGreen), | ||
| card, | ||
| ); | ||
|
|
||
| let inner = card.inner(ratzilla::ratatui::layout::Margin { | ||
| vertical: 1, | ||
| horizontal: 2, | ||
| }); | ||
| let [intro, docs, repo, plain] = Layout::vertical([ | ||
| Constraint::Length(2), | ||
| Constraint::Length(1), | ||
| Constraint::Length(1), | ||
| Constraint::Length(1), | ||
| ]) | ||
| .spacing(0) | ||
| .areas(inner); | ||
|
|
||
| frame.render_widget( | ||
| Paragraph::new("DOM links use native browser anchors.") | ||
| .alignment(Alignment::Left), | ||
| intro, | ||
| ); | ||
| render_link( | ||
| frame, | ||
| docs, | ||
| "Docs: ", | ||
| Hyperlink::with_label( | ||
| "Ratatui rendering guide".black().on_cyan().bold(), | ||
| "https://ratatui.rs/concepts/rendering/under-the-hood/", | ||
| ), | ||
| ); | ||
| render_link( | ||
| frame, | ||
| repo, | ||
| "Repo: ", | ||
| Hyperlink::with_label( | ||
| "ratzilla on GitHub".black().on_yellow().italic(), | ||
| "https://github.com/ratatui/ratzilla", | ||
| ), | ||
| ); | ||
| render_link( | ||
| frame, | ||
| plain, | ||
| "Website: ", | ||
| Hyperlink::new("https://ratatui.rs".light_cyan().underlined()), | ||
| ); | ||
| }); | ||
| Ok(()) | ||
| } | ||
|
|
||
| fn render_link( | ||
| frame: &mut ratzilla::ratatui::Frame<'_>, | ||
| area: Rect, | ||
| label: &str, | ||
| link: Hyperlink<'_>, | ||
| ) { | ||
| let [prefix, suffix] = | ||
| Layout::horizontal([Constraint::Length(label.len() as u16), Constraint::Min(0)]) | ||
| .areas(area); | ||
| frame.render_widget(Paragraph::new(label), prefix); | ||
| frame.render_widget(link, suffix); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
| error::Error, | ||
| event::{KeyEvent, MouseEvent}, | ||
| render::WebEventHandler, | ||
| widgets::hyperlink_state, | ||
| CursorShape, | ||
| }; | ||
|
|
||
|
|
@@ -53,7 +54,7 @@ | |
| /// | ||
| /// - If the grid ID is not set, it returns `"grid"`. | ||
| /// - If the grid ID is set, it returns the grid ID suffixed with | ||
| /// `"_ratzilla_grid"`. | ||
|
Check warning on line 57 in src/backend/dom.rs
|
||
| pub fn grid_id(&self) -> String { | ||
| match &self.grid_id { | ||
| Some(id) => format!("{id}_ratzilla_grid"), | ||
|
|
@@ -78,6 +79,10 @@ | |
| initialized: Rc<RefCell<bool>>, | ||
| /// Cells. | ||
| cells: Vec<Element>, | ||
| /// Current cell contents. | ||
| buffer: Vec<Cell>, | ||
| /// Current hyperlink targets for each cell. | ||
| hyperlinks: Vec<Option<std::rc::Rc<str>>>, | ||
| /// Grid element. | ||
| grid: Element, | ||
| /// The parent of the grid element. | ||
|
|
@@ -162,6 +167,8 @@ | |
| let mut backend = Self { | ||
| initialized, | ||
| cells: vec![], | ||
| buffer: vec![], | ||
| hyperlinks: vec![], | ||
| grid: document.create_element("div")?, | ||
| grid_parent, | ||
| options, | ||
|
|
@@ -225,6 +232,23 @@ | |
| Size::new((w / cell_size.0) as u16, (h / cell_size.1) as u16) | ||
| } | ||
|
|
||
| fn render_cell(&self, index: usize) -> Result<(), Error> { | ||
| let cell = &self.buffer[index]; | ||
| let elem = &self.cells[index]; | ||
|
|
||
| elem.set_inner_html(""); | ||
| elem.set_attribute("style", &get_cell_style_as_css(cell))?; | ||
|
|
||
| if let Some(url) = &self.hyperlinks[index] { | ||
| let anchor = create_anchor(&self.document, cell.symbol(), url)?; | ||
| elem.append_child(&anchor)?; | ||
| } else { | ||
| elem.set_inner_html(cell.symbol()); | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Resize event types. | ||
| const RESIZE_EVENT_TYPES: &[&str] = &["resize"]; | ||
|
|
||
|
|
@@ -233,6 +257,8 @@ | |
| self.grid = self.document.create_element("div")?; | ||
| self.grid.set_attribute("id", &self.options.grid_id())?; | ||
| self.cells.clear(); | ||
| self.buffer.clear(); | ||
| self.hyperlinks.clear(); | ||
| Ok(()) | ||
| } | ||
|
|
||
|
|
@@ -244,8 +270,11 @@ | |
| for _y in 0..self.size.height { | ||
| let mut line_cells: Vec<Element> = Vec::new(); | ||
| for _x in 0..self.size.width { | ||
| let span = create_span(&self.document, &Cell::default())?; | ||
| let cell = Cell::default(); | ||
| let span = create_span(&self.document, &cell)?; | ||
| self.cells.push(span.clone()); | ||
| self.buffer.push(cell); | ||
| self.hyperlinks.push(None); | ||
| line_cells.push(span); | ||
| } | ||
|
|
||
|
|
@@ -316,26 +345,51 @@ | |
| self.populate()?; | ||
| } | ||
|
|
||
| let mut dirty_cells = vec![false; self.cells.len()]; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be nice to add inline explanatory comments on how this new rendering approach works. |
||
|
|
||
| for (x, y, cell) in content { | ||
| let cell_position = (y * self.size.width + x) as usize; | ||
| let elem = &self.cells[cell_position]; | ||
|
|
||
| elem.set_inner_html(cell.symbol()); | ||
| elem.set_attribute("style", &get_cell_style_as_css(cell)) | ||
| .map_err(Error::from)?; | ||
| self.buffer[cell_position] = cell.clone(); | ||
| dirty_cells[cell_position] = true; | ||
|
|
||
| // don't display the next cell if a fullwidth glyph preceeds it | ||
| if cell.symbol().len() > 1 && cell.symbol().width() == 2 { | ||
| if (cell_position + 1) < self.cells.len() { | ||
| let next_elem = &self.cells[cell_position + 1]; | ||
| next_elem.set_inner_html(""); | ||
| next_elem | ||
| .set_attribute("style", &get_cell_style_as_css(&Cell::new(""))) | ||
| .map_err(Error::from)?; | ||
| self.buffer[cell_position + 1] = Cell::new(""); | ||
| dirty_cells[cell_position + 1] = true; | ||
| } | ||
| } | ||
|
Check warning on line 361 in src/backend/dom.rs
|
||
| } | ||
|
|
||
| let mut next_hyperlinks = vec![None; self.cells.len()]; | ||
| for region in hyperlink_state::take() { | ||
| let row_offset = region.y as usize * self.size.width as usize; | ||
| if row_offset >= next_hyperlinks.len() { | ||
| continue; | ||
| } | ||
| let start = row_offset + region.x as usize; | ||
| if start >= next_hyperlinks.len() { | ||
| continue; | ||
| } | ||
| let end = (start + region.width as usize).min(row_offset + self.size.width as usize); | ||
| for cell in &mut next_hyperlinks[start..end] { | ||
| *cell = Some(region.url.clone()); | ||
| } | ||
| } | ||
|
|
||
| for (index, current) in next_hyperlinks.iter().enumerate() { | ||
| if self.hyperlinks[index].as_deref() != current.as_deref() { | ||
| dirty_cells[index] = true; | ||
| } | ||
| } | ||
| self.hyperlinks = next_hyperlinks; | ||
|
|
||
| for (index, dirty) in dirty_cells.into_iter().enumerate() { | ||
| if dirty { | ||
| self.render_cell(index)?; | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
|
|
@@ -396,6 +450,15 @@ | |
| } | ||
|
|
||
| fn clear(&mut self) -> IoResult<()> { | ||
| for cell in &mut self.buffer { | ||
| *cell = Cell::default(); | ||
| } | ||
| for link in &mut self.hyperlinks { | ||
| *link = None; | ||
| } | ||
| for index in 0..self.cells.len() { | ||
| self.render_cell(index)?; | ||
| } | ||
| Ok(()) | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the previous approach, we were basically adding anchors for the whole cells, so the
<a>would be surrounding them. Now we are adding an anchor for each cell.This obviously works but I'm thinking about the performance and other implications. Would it make sense to somehow bring back the previous approach here?
Also, I'm sure you probably have tested this, but there were some issues regarding zooming in/out and hyperlinks disappearing before. That issue seems gone now, but it would be nice to double check :)