Skip to content

Commit

Permalink
Add pagination module to azul-core
Browse files Browse the repository at this point in the history
  • Loading branch information
fschutt committed Feb 21, 2025
1 parent 4c486d7 commit 1ccf3d0
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 1 deletion.
2 changes: 1 addition & 1 deletion azul-core/src/id_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ impl Node {
}

#[inline]
fn get_first_child(&self, current_node_id: NodeId) -> Option<NodeId> {
pub(crate) fn get_first_child(&self, current_node_id: NodeId) -> Option<NodeId> {
// last_child and first_child are always set together
self.last_child.map(|_| current_node_id + 1)
}
Expand Down
2 changes: 2 additions & 0 deletions azul-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ pub mod macros;
/// Type definitions for various types of callbacks, as well as focus and scroll handling
#[macro_use]
pub mod callbacks;
/// Functions to paginate a DOM into multiple pages (sub-DOMs) for printing
pub mod pagination;
/// Functions to manage adding fonts + images, garbage collection
pub mod app_resources;
/// Contains functions to format a CSS stylesheet to a Rust string
Expand Down
164 changes: 164 additions & 0 deletions azul-core/src/pagination.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use crate::id_tree::{NodeHierarchy, NodeDataContainer, NodeId};
use crate::dom::NodeData;
use crate::ui_solver::PositionedRectangle;
use alloc::collections::BTreeMap;

/// The per-page output: a partial `NodeHierarchy` plus the subset of `PositionedRectangle`s
/// that appear on that page. Both arrays map 1:1 by index: if the partial-hierarchy
/// has `n` nodes, then `page_rects.internal.len()` = `n` as well, and index #5 in the new
/// hierarchy corresponds to index #5 in the `page_rects`.
pub struct PaginatedPage {
pub node_hierarchy: NodeHierarchy,
pub page_rects: NodeDataContainer<PositionedRectangle>,
/// Maps from "original node id" -> "this page's node id" (or None if not present)
pub old_to_new_id_map: BTreeMap<NodeId, NodeId>,
}

/// Break a single large LayoutResult into multiple "pages" by y-coordinate.
pub fn paginate_layout_result(
node_hierarchy: &NodeHierarchy,
node_data: &NodeDataContainer<NodeData>,
rects: &NodeDataContainer<PositionedRectangle>,
page_height: f32,
) -> Vec<PaginatedPage>
{
let mut pages = Vec::new();

// 1) Find total height of the entire layout from the root bounding box
// For a multi-child root, pick the max bounding box, or track from the actual "root" node.
// Example: The root is NodeId(0):
let total_height = rects.internal[0].size.height;

// compute how many pages we need
let num_pages =
(total_height / page_height).ceil() as usize;

// We'll BFS from the root for each page, building a partial hierarchy
// This is a naive approach that visits the entire tree once per page.
// If performance is an issue, you can do a single pass to partition everything.

for page_index in 0..num_pages {
let page_start_y = page_index as f32 * page_height;
let page_end_y = page_start_y + page_height;

// We'll keep new arrays for the partial NodeHierarchy
let mut new_nodes = Vec::<crate::id_tree::Node>::new();
let mut new_rects = Vec::<PositionedRectangle>::new();

// We also need a map from old NodeId -> new NodeId
let mut old_to_new_id_map = BTreeMap::<NodeId, NodeId>::new();

// BFS queue
let mut queue = vec![NodeId::new(0)];

while let Some(cur_id) = queue.pop() {
let r = &rects.internal[cur_id.index()];
let node_top = r.position.get_static_offset().y;
let node_bottom = node_top + r.size.height;

// If the node is entirely outside this page's y-range, skip
if node_bottom < page_start_y || node_top > page_end_y {
continue;
}

// Otherwise, we want to keep it. Create a new Node entry, plus a new rect entry
// We have to replicate the parent's / siblings indices, but in new indices.

// If we have NOT yet assigned a new ID, we create one
let new_id = match old_to_new_id_map.get(&cur_id) {
Some(nid) => *nid,
None => {
let new_idx = new_nodes.len();
// Insert a placeholder Node
new_nodes.push(crate::id_tree::ROOT_NODE);
new_rects.push(PositionedRectangle::default());
let new_id = NodeId::new(new_idx);
old_to_new_id_map.insert(cur_id, new_id);
new_id
}
};

// Fill out new_node & new_rect
// copy the old Node
let old_node = node_hierarchy.internal[cur_id.index()];
// We'll fix up the parent / sibling pointers AFTER BFS
// so for now store them in a temporary structure
new_nodes[new_id.index()] = crate::id_tree::Node {
parent: None,
previous_sibling: None,
next_sibling: None,
last_child: None,
};

// Copy the old bounding box, optionally rebase "top" so that it starts at 0
let mut new_rect = r.clone();
// Example: rebase so that page Y=0 is oldY=page_start_y
let offset_amount = page_start_y;
new_rect.position
.translate_vertical(-offset_amount);

new_rects[new_id.index()] = new_rect;

// BFS into the children: we only push them if they're not fully outside
// We do not decide whether to skip them *yet*, we do that once we pop them from the queue
if let Some(first_child) = old_node.get_first_child(cur_id) {
// push all siblings
let mut c = first_child;
while let Some(n) = Some(c) {
queue.push(n);
let c_node = node_hierarchy.internal[c.index()];
if let Some(ns) = c_node.next_sibling {
c = ns;
} else {
break;
}
}
}
}

// 2) Now fix up the parent / sibling pointers in new_nodes
// We only keep them if the parent's old ID is in old_to_new_id_map
for (old_id, new_id) in &old_to_new_id_map {
let old_node = node_hierarchy.internal[old_id.index()];

let old_parent = old_node.parent;
let old_prev = old_node.previous_sibling;
let old_next = old_node.next_sibling;
let old_last_child = old_node.last_child;

let new_parent = old_parent
.and_then(|pid| old_to_new_id_map.get(&pid))
.copied();
let new_prev = old_prev
.and_then(|pid| old_to_new_id_map.get(&pid))
.copied();
let new_next = old_next
.and_then(|pid| old_to_new_id_map.get(&pid))
.copied();
let new_last_child = old_last_child
.and_then(|pid| old_to_new_id_map.get(&pid))
.copied();

new_nodes[new_id.index()].parent = new_parent;
new_nodes[new_id.index()].previous_sibling = new_prev;
new_nodes[new_id.index()].next_sibling = new_next;
new_nodes[new_id.index()].last_child = new_last_child;
}

// Create final NodeHierarchy + Container
let partial_hierarchy = NodeHierarchy {
internal: new_nodes,
};
let partial_rects = NodeDataContainer {
internal: new_rects,
};

pages.push(PaginatedPage {
node_hierarchy: partial_hierarchy,
page_rects: partial_rects,
old_to_new_id_map,
});
}

pages
}
14 changes: 14 additions & 0 deletions azul-core/src/ui_solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,20 @@ pub enum PositionInfo {


impl PositionInfo {
/// Shift this node vertically by `offset_amount`.
/// i.e. add `offset_amount` to both the relative and static y-offsets.
pub fn translate_vertical(&mut self, offset_amount: f32) {
match self {
PositionInfo::Static(ref mut info)
| PositionInfo::Absolute(ref mut info)
| PositionInfo::Fixed(ref mut info)
| PositionInfo::Relative(ref mut info) => {
info.y_offset += offset_amount;
info.static_y_offset += offset_amount;
}
}
}

pub fn scale_for_dpi(&mut self, scale_factor: f32) {
match self {
PositionInfo::Static(p) => p.scale_for_dpi(scale_factor),
Expand Down

0 comments on commit 1ccf3d0

Please sign in to comment.