fn generate_hex(app_info: &Data, bytes_per_line: usize, lines_per_screen: usize) -> Vec<Line> {
    let initial_offset = app_info.start_address.min(3);
    OverlappingChunks::new(
        &app_info.contents[(app_info.start_address - initial_offset)..],
        bytes_per_line,
        6,
    )
    .take(lines_per_screen)
    .enumerate()
    .map(|(row, chunk)| {
        let spans = chunk
            .iter()
            .zip(ByteAlignedDecoder::new(chunk, app_info.encoding))
            .skip(initial_offset)
            .take(bytes_per_line)
            .enumerate()
            .flat_map(|(col, (&byte, character))| {
                // We don't want an extra space at the end of each row.
                if col < bytes_per_line - 1 {
                    format!("{byte:02X?} ")
                } else {
                    format!("{byte:02X?}")
                }
                .chars()
                .enumerate()
                .map(|(nibble_pos, c)| {
                    let byte_pos = app_info.start_address + (row * bytes_per_line) + col;
                    let mut span =
                        Span::styled(c.to_string(), Style::default().fg(*character.color()));
                    let is_cursor = byte_pos == app_info.offset
                        && ((nibble_pos == 0 && app_info.nibble == Nibble::Beginning)
                            || (nibble_pos == 1 && app_info.nibble == Nibble::End));

                    // Determine if the specified nibble (or space) should have a
                    // lighter foreground because it is in the user's dragged range.
                    // The logic is more complicated for hex because users can select
                    // a single nibble from a byte.
                    let mut in_drag = false;
                    if let Some(drag) = app_info.last_drag {
                        let drag_nibble = app_info.drag_nibble.unwrap_or(Nibble::End);
                        if !(drag == app_info.offset && app_info.nibble == drag_nibble) {
                            let mut start = drag;
                            let mut end = app_info.offset;
                            let mut start_nibble = drag_nibble;
                            let mut end_nibble = app_info.nibble;

                            if app_info.offset < drag {
                                start = app_info.offset;
                                end = drag;
                                start_nibble = app_info.nibble;
                                end_nibble = drag_nibble;
                            }

                            // The only time the starting byte would not entirely be in
                            // drag range is when the first nibble is not highlighted.
                            // Similarly, the last nibble is only partially highlighted
                            // when the second (and last) nibble is not selected.
                            if byte_pos == start {
                                in_drag = !(nibble_pos == 0 && start_nibble == Nibble::End);
                            }
                            if byte_pos == end {
                                in_drag |= !(nibble_pos == 1 && end_nibble == Nibble::Beginning)
                                    && nibble_pos != 2;
                            }
                            if start == end && nibble_pos == 2 {
                                in_drag = false;
                            } else if end - start > 1 {
                                in_drag |= (start + 1..end).contains(&byte_pos);
                            }
                        }
                    }
                    if is_cursor || in_drag {
                        span.style = span.style.bg(COLOR_NULL);
                    }
                    span
                })
                .collect::<Vec<Span>>()
            })
            .collect::<Vec<Span>>();
        Line::from(spans)
    })
    .collect::<Vec<Line>>()
}
Display hex bytes with correct highlighting and colors by chunking the bytes into rows and
formatting them into hex.

NOTE: In UTF-8, a character takes up to 4 bytes and thus the encoding can break at the ends of a
chunk. Increasing the chunk size by 3 bytes at both ends before decoding and cropping them of
afterwards solves the issue for the visible parts.
src/screen.rs