use crate::SetTextFormatDto;
use anyhow::{Result, anyhow};
use common::database::CommandUnitOfWork;
use common::database::rope_helpers::{block_char_length, block_char_to_byte_in_block};
use common::direct_access::document::document_repository::DocumentRelationshipField;
use common::direct_access::frame::frame_repository::FrameRelationshipField;
use common::direct_access::root::root_repository::RootRelationshipField;
use common::entities::{Block, Document, Frame, Root};
use common::format_runs::{
CharacterFormat, FormatRun, capture_image_formats_in_range, capture_runs_in_range,
debug_assert_well_formed, splice_range,
};
use common::types::{EntityId, ROOT_ENTITY_ID};
use common::undo_redo::UndoRedoCommand;
use std::any::Any;
pub trait SetTextFormatUnitOfWorkFactoryTrait: Send + Sync {
fn create(&self) -> Box<dyn SetTextFormatUnitOfWorkTrait>;
}
#[macros::uow_action(entity = "Root", action = "Get")]
#[macros::uow_action(entity = "Root", action = "GetRelationship")]
#[macros::uow_action(entity = "Document", action = "Get")]
#[macros::uow_action(entity = "Document", action = "GetRelationship")]
#[macros::uow_action(entity = "Frame", action = "Get")]
#[macros::uow_action(entity = "Frame", action = "GetRelationship")]
#[macros::uow_action(entity = "Block", action = "Get")]
#[macros::uow_action(entity = "Block", action = "GetMulti")]
#[macros::uow_action(entity = "Block", action = "GetRelationship")]
pub trait SetTextFormatUnitOfWorkTrait: CommandUnitOfWork {}
#[derive(Clone, Debug)]
struct BlockFormatInverse {
block_id: EntityId,
byte_range: (u32, u32),
prior_runs: Vec<FormatRun>,
prior_image_formats: Vec<(u32, CharacterFormat)>,
}
fn underline_style_to_entity(s: &crate::dtos::UnderlineStyle) -> common::entities::UnderlineStyle {
match s {
crate::dtos::UnderlineStyle::NoUnderline => common::entities::UnderlineStyle::NoUnderline,
crate::dtos::UnderlineStyle::SingleUnderline => {
common::entities::UnderlineStyle::SingleUnderline
}
crate::dtos::UnderlineStyle::DashUnderline => {
common::entities::UnderlineStyle::DashUnderline
}
crate::dtos::UnderlineStyle::DotLine => common::entities::UnderlineStyle::DotLine,
crate::dtos::UnderlineStyle::DashDotLine => common::entities::UnderlineStyle::DashDotLine,
crate::dtos::UnderlineStyle::DashDotDotLine => {
common::entities::UnderlineStyle::DashDotDotLine
}
crate::dtos::UnderlineStyle::WaveUnderline => {
common::entities::UnderlineStyle::WaveUnderline
}
crate::dtos::UnderlineStyle::SpellCheckUnderline => {
common::entities::UnderlineStyle::SpellCheckUnderline
}
}
}
pub(crate) fn vertical_alignment_to_entity(
v: &crate::dtos::CharVerticalAlignment,
) -> common::entities::CharVerticalAlignment {
match v {
crate::dtos::CharVerticalAlignment::Normal => {
common::entities::CharVerticalAlignment::Normal
}
crate::dtos::CharVerticalAlignment::SuperScript => {
common::entities::CharVerticalAlignment::SuperScript
}
crate::dtos::CharVerticalAlignment::SubScript => {
common::entities::CharVerticalAlignment::SubScript
}
crate::dtos::CharVerticalAlignment::Middle => {
common::entities::CharVerticalAlignment::Middle
}
crate::dtos::CharVerticalAlignment::Bottom => {
common::entities::CharVerticalAlignment::Bottom
}
crate::dtos::CharVerticalAlignment::Top => common::entities::CharVerticalAlignment::Top,
crate::dtos::CharVerticalAlignment::Baseline => {
common::entities::CharVerticalAlignment::Baseline
}
}
}
fn merge_dto(base: &CharacterFormat, dto: &SetTextFormatDto) -> CharacterFormat {
let mut out = base.clone();
if let Some(ref v) = dto.font_family {
out.font_family = Some(v.clone());
}
if let Some(v) = dto.font_point_size {
out.font_point_size = Some(v);
}
if let Some(v) = dto.font_weight {
out.font_weight = Some(v);
}
if let Some(v) = dto.font_bold {
out.font_bold = Some(v);
}
if let Some(v) = dto.font_italic {
out.font_italic = Some(v);
}
if let Some(v) = dto.font_underline {
out.font_underline = Some(v);
}
if let Some(v) = dto.font_overline {
out.font_overline = Some(v);
}
if let Some(v) = dto.font_strikeout {
out.font_strikeout = Some(v);
}
if let Some(v) = dto.letter_spacing {
out.letter_spacing = Some(v);
}
if let Some(v) = dto.word_spacing {
out.word_spacing = Some(v);
}
if let Some(ref v) = dto.underline_style {
out.underline_style = Some(underline_style_to_entity(v));
}
if let Some(ref v) = dto.vertical_alignment {
out.vertical_alignment = Some(vertical_alignment_to_entity(v));
}
apply_link(&mut out, dto.clear_link, dto.anchor_href.as_deref());
out
}
pub(crate) fn apply_link(out: &mut CharacterFormat, clear: bool, href: Option<&str>) {
if clear {
out.anchor_href = None;
out.is_anchor = None;
out.anchor_names = Vec::new();
out.tooltip = None;
} else if let Some(href) = href
&& !href.is_empty()
{
out.anchor_href = Some(href.to_string());
out.is_anchor = Some(true);
}
}
fn build_replacement_runs(
existing_runs: &[FormatRun],
byte_start: u32,
byte_end: u32,
dto: &SetTextFormatDto,
) -> Vec<FormatRun> {
let mut out: Vec<FormatRun> = Vec::new();
let mut cursor = byte_start;
for run in existing_runs {
if run.byte_end <= byte_start || run.byte_start >= byte_end {
continue;
}
let overlap_start = std::cmp::max(run.byte_start, byte_start);
let overlap_end = std::cmp::min(run.byte_end, byte_end);
if overlap_start > cursor {
out.push(FormatRun {
byte_start: cursor,
byte_end: overlap_start,
format: merge_dto(&CharacterFormat::default(), dto),
});
}
out.push(FormatRun {
byte_start: overlap_start,
byte_end: overlap_end,
format: merge_dto(&run.format, dto),
});
cursor = overlap_end;
}
if cursor < byte_end {
out.push(FormatRun {
byte_start: cursor,
byte_end,
format: merge_dto(&CharacterFormat::default(), dto),
});
}
out
}
fn execute_set_text_format(
uow: &mut Box<dyn SetTextFormatUnitOfWorkTrait>,
dto: &SetTextFormatDto,
) -> Result<Vec<BlockFormatInverse>> {
let root = uow
.get_root(&ROOT_ENTITY_ID)?
.ok_or_else(|| anyhow!("Root entity not found"))?;
let doc_ids = uow.get_root_relationship(&root.id, &RootRelationshipField::Document)?;
let doc_id = *doc_ids
.first()
.ok_or_else(|| anyhow!("Root has no document"))?;
let _document = uow
.get_document(&doc_id)?
.ok_or_else(|| anyhow!("Document not found"))?;
let frame_ids = uow.get_document_relationship(&doc_id, &DocumentRelationshipField::Frames)?;
let mut all_block_ids = Vec::new();
for fid in &frame_ids {
let block_ids = uow.get_frame_relationship(fid, &FrameRelationshipField::Blocks)?;
all_block_ids.extend(block_ids);
}
let blocks_opt = uow.get_block_multi(&all_block_ids)?;
let mut blocks: Vec<Block> = blocks_opt.into_iter().flatten().collect();
blocks.sort_by_key(|b| b.document_position);
let range_start = std::cmp::min(dto.position, dto.anchor);
let range_end = std::cmp::max(dto.position, dto.anchor);
let mut inverse: Vec<BlockFormatInverse> = Vec::new();
if range_start == range_end {
return Ok(inverse);
}
let store = uow.store();
for block in &blocks {
let block_start = block.document_position;
let block_end = block_start + block_char_length(block, &store);
if block_end <= range_start || block_start >= range_end {
continue;
}
let local_char_start = std::cmp::max(0, range_start - block_start) as usize;
let local_char_end =
std::cmp::min(block_char_length(block, &store), range_end - block_start) as usize;
let (byte_start, content_byte_len) =
block_char_to_byte_in_block(&store, block.id, local_char_start);
let (byte_end, _) = block_char_to_byte_in_block(&store, block.id, local_char_end);
if byte_start >= byte_end {
continue;
}
let prior_runs = {
let runs_map = store.format_runs.read();
runs_map
.get(&block.id)
.map(|runs| capture_runs_in_range(runs, byte_start, byte_end))
.unwrap_or_default()
};
let prior_image_formats = {
let images_map = store.block_images.read();
images_map
.get(&block.id)
.map(|images| capture_image_formats_in_range(images, byte_start, byte_end))
.unwrap_or_default()
};
{
let mut runs_map = store.format_runs.write();
let runs = runs_map.entry(block.id).or_default();
let replacement = build_replacement_runs(runs, byte_start, byte_end, dto);
splice_range(runs, byte_start..byte_end, replacement);
debug_assert_well_formed(runs, content_byte_len);
}
{
let mut images_map = store.block_images.write();
if let Some(images) = images_map.get_mut(&block.id) {
for img in images.iter_mut() {
if img.byte_offset >= byte_start && img.byte_offset < byte_end {
img.format = merge_dto(&img.format, dto);
}
}
}
}
inverse.push(BlockFormatInverse {
block_id: block.id,
byte_range: (byte_start, byte_end),
prior_runs,
prior_image_formats,
});
}
Ok(inverse)
}
fn apply_inverse(
uow: &mut Box<dyn SetTextFormatUnitOfWorkTrait>,
inverse: &[BlockFormatInverse],
) -> Result<()> {
let store = uow.store();
for entry in inverse {
{
let mut runs_map = store.format_runs.write();
let runs = runs_map.entry(entry.block_id).or_default();
splice_range(
runs,
entry.byte_range.0..entry.byte_range.1,
entry.prior_runs.clone(),
);
}
{
let mut images_map = store.block_images.write();
if let Some(images) = images_map.get_mut(&entry.block_id) {
for (byte_offset, format) in &entry.prior_image_formats {
if let Some(img) = images.iter_mut().find(|i| i.byte_offset == *byte_offset) {
img.format = format.clone();
}
}
}
}
}
Ok(())
}
pub struct SetTextFormatUseCase {
uow_factory: Box<dyn SetTextFormatUnitOfWorkFactoryTrait>,
inverse: Option<Vec<BlockFormatInverse>>,
last_dto: Option<SetTextFormatDto>,
}
impl SetTextFormatUseCase {
pub fn new(uow_factory: Box<dyn SetTextFormatUnitOfWorkFactoryTrait>) -> Self {
SetTextFormatUseCase {
uow_factory,
inverse: None,
last_dto: None,
}
}
pub fn execute(&mut self, dto: &SetTextFormatDto) -> Result<()> {
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
let inverse = execute_set_text_format(&mut uow, dto)?;
self.inverse = Some(inverse);
self.last_dto = Some(dto.clone());
uow.commit()?;
Ok(())
}
}
impl UndoRedoCommand for SetTextFormatUseCase {
fn undo(&mut self) -> Result<()> {
let inverse = self
.inverse
.as_ref()
.ok_or_else(|| anyhow!("No inverse data available for undo"))?
.clone();
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
apply_inverse(&mut uow, &inverse)?;
uow.commit()?;
Ok(())
}
fn redo(&mut self) -> Result<()> {
let dto = self
.last_dto
.as_ref()
.ok_or_else(|| anyhow!("No DTO available for redo"))?
.clone();
let mut uow = self.uow_factory.create();
uow.begin_transaction()?;
let inverse = execute_set_text_format(&mut uow, &dto)?;
self.inverse = Some(inverse);
uow.commit()?;
Ok(())
}
fn as_any(&self) -> &dyn Any {
self
}
}