constellation/
session_history.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cmp::PartialEq;
6use std::fmt;
7
8use base::id::{BrowsingContextId, HistoryStateId, PipelineId, WebViewId};
9use constellation_traits::LoadData;
10use embedder_traits::ViewportDetails;
11use log::debug;
12use servo_url::ServoUrl;
13
14use crate::browsingcontext::NewBrowsingContextInfo;
15
16/// Represents the joint session history
17/// <https://html.spec.whatwg.org/multipage/#joint-session-history>
18#[derive(Debug)]
19pub struct JointSessionHistory {
20    /// Diffs used to traverse to past entries. Oldest entries are at the back,
21    /// the most recent entries are at the front.
22    pub past: Vec<SessionHistoryDiff>,
23
24    /// Diffs used to traverse to future entries. Oldest entries are at the back,
25    /// the most recent entries are at the front.
26    pub future: Vec<SessionHistoryDiff>,
27}
28
29impl JointSessionHistory {
30    pub fn new() -> JointSessionHistory {
31        JointSessionHistory {
32            past: Vec::new(),
33            future: Vec::new(),
34        }
35    }
36
37    pub fn history_length(&self) -> usize {
38        self.past.len() + 1 + self.future.len()
39    }
40
41    pub fn push_diff(&mut self, diff: SessionHistoryDiff) -> Vec<SessionHistoryDiff> {
42        debug!("pushing a past entry; removing future");
43        self.past.push(diff);
44        std::mem::take(&mut self.future)
45    }
46
47    pub fn replace_reloader(&mut self, old_reloader: NeedsToReload, new_reloader: NeedsToReload) {
48        for diff in self.past.iter_mut().chain(self.future.iter_mut()) {
49            diff.replace_reloader(&old_reloader, &new_reloader);
50        }
51    }
52
53    pub fn replace_history_state(
54        &mut self,
55        pipeline_id: PipelineId,
56        history_state_id: HistoryStateId,
57        url: ServoUrl,
58    ) {
59        if let Some(SessionHistoryDiff::Pipeline {
60            new_history_state_id,
61            new_url,
62            ..
63        }) = self.past.iter_mut().find(|diff| match diff {
64            SessionHistoryDiff::Pipeline {
65                pipeline_reloader: NeedsToReload::No(id),
66                ..
67            } => pipeline_id == *id,
68            _ => false,
69        }) {
70            *new_history_state_id = history_state_id;
71            *new_url = url.clone();
72        }
73
74        if let Some(SessionHistoryDiff::Pipeline {
75            old_history_state_id,
76            old_url,
77            ..
78        }) = self.future.iter_mut().find(|diff| match diff {
79            SessionHistoryDiff::Pipeline {
80                pipeline_reloader: NeedsToReload::No(id),
81                ..
82            } => pipeline_id == *id,
83            _ => false,
84        }) {
85            *old_history_state_id = Some(history_state_id);
86            *old_url = url;
87        }
88    }
89
90    pub fn remove_entries_for_browsing_context(&mut self, context_id: BrowsingContextId) {
91        debug!("{}: Removing entries for browsing context", context_id);
92        self.past.retain(|diff| match diff {
93            SessionHistoryDiff::BrowsingContext {
94                browsing_context_id,
95                ..
96            } => *browsing_context_id != context_id,
97            _ => true,
98        });
99        self.future.retain(|diff| match diff {
100            SessionHistoryDiff::BrowsingContext {
101                browsing_context_id,
102                ..
103            } => *browsing_context_id != context_id,
104            _ => true,
105        });
106    }
107}
108
109/// Represents a pending change in a session history, that will be applied
110/// once the new pipeline has loaded and completed initial layout / paint.
111#[derive(Debug)]
112pub struct SessionHistoryChange {
113    /// The browsing context to change.
114    pub browsing_context_id: BrowsingContextId,
115
116    /// The top-level browsing context ancestor.
117    pub webview_id: WebViewId,
118
119    /// The pipeline for the document being loaded.
120    pub new_pipeline_id: PipelineId,
121
122    /// The old pipeline that the new pipeline should replace.
123    pub replace: Option<NeedsToReload>,
124
125    /// Holds data for not-yet constructed browsing contexts that are not
126    /// easily available when they need to be constructed.
127    pub new_browsing_context_info: Option<NewBrowsingContextInfo>,
128
129    /// The size and hidpi scale factor of the viewport for the browsing context.
130    pub viewport_details: ViewportDetails,
131}
132
133/// Represents a pipeline or discarded pipeline in a history entry.
134// FIXME: https://github.com/servo/servo/issues/34591
135#[expect(clippy::large_enum_variant)]
136#[derive(Clone, Debug)]
137pub enum NeedsToReload {
138    /// Represents a pipeline that has not been discarded
139    No(PipelineId),
140    /// Represents a pipeline that has been discarded and must be reloaded with the given `LoadData`
141    /// if ever traversed to.
142    Yes(PipelineId, LoadData),
143}
144
145impl fmt::Display for NeedsToReload {
146    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
147        match *self {
148            NeedsToReload::No(pipeline_id) => write!(fmt, "Alive({})", pipeline_id),
149            NeedsToReload::Yes(pipeline_id, ..) => write!(fmt, "Dead({})", pipeline_id),
150        }
151    }
152}
153
154impl NeedsToReload {
155    pub fn alive_pipeline_id(&self) -> Option<PipelineId> {
156        match *self {
157            NeedsToReload::No(pipeline_id) => Some(pipeline_id),
158            NeedsToReload::Yes(..) => None,
159        }
160    }
161}
162
163// Custom `PartialEq` that only compares the `PipelineId`s of the same variants while ignoring `LoadData`
164impl PartialEq for NeedsToReload {
165    fn eq(&self, other: &NeedsToReload) -> bool {
166        match *self {
167            NeedsToReload::No(pipeline_id) => match *other {
168                NeedsToReload::No(other_pipeline_id) => pipeline_id == other_pipeline_id,
169                _ => false,
170            },
171            NeedsToReload::Yes(pipeline_id, _) => match *other {
172                NeedsToReload::Yes(other_pipeline_id, _) => pipeline_id == other_pipeline_id,
173                _ => false,
174            },
175        }
176    }
177}
178
179/// Represents a the difference between two adjacent session history entries.
180// FIXME: https://github.com/servo/servo/issues/34591
181#[expect(clippy::large_enum_variant)]
182#[derive(Debug)]
183pub enum SessionHistoryDiff {
184    /// Represents a diff where the active pipeline of an entry changed.
185    BrowsingContext {
186        /// The browsing context whose pipeline changed
187        browsing_context_id: BrowsingContextId,
188        /// The previous pipeline (used when traversing into the past)
189        old_reloader: NeedsToReload,
190        /// The next pipeline (used when traversing into the future)
191        new_reloader: NeedsToReload,
192    },
193    /// Represents a diff where the active state of a pipeline changed.
194    Pipeline {
195        /// The pipeline whose history state changed.
196        pipeline_reloader: NeedsToReload,
197        /// The old history state id.
198        old_history_state_id: Option<HistoryStateId>,
199        /// The old url
200        old_url: ServoUrl,
201        /// The new history state id.
202        new_history_state_id: HistoryStateId,
203        /// The new url
204        new_url: ServoUrl,
205    },
206    Hash {
207        pipeline_reloader: NeedsToReload,
208        old_url: ServoUrl,
209        new_url: ServoUrl,
210    },
211}
212
213impl SessionHistoryDiff {
214    /// Returns the old pipeline id if that pipeline is still alive, otherwise returns `None`
215    pub fn alive_old_pipeline(&self) -> Option<PipelineId> {
216        match *self {
217            SessionHistoryDiff::BrowsingContext {
218                ref old_reloader, ..
219            } => match *old_reloader {
220                NeedsToReload::No(pipeline_id) => Some(pipeline_id),
221                NeedsToReload::Yes(..) => None,
222            },
223            _ => None,
224        }
225    }
226
227    /// Returns the new pipeline id if that pipeline is still alive, otherwise returns `None`
228    pub fn alive_new_pipeline(&self) -> Option<PipelineId> {
229        match *self {
230            SessionHistoryDiff::BrowsingContext {
231                ref new_reloader, ..
232            } => match *new_reloader {
233                NeedsToReload::No(pipeline_id) => Some(pipeline_id),
234                NeedsToReload::Yes(..) => None,
235            },
236            _ => None,
237        }
238    }
239
240    /// Replaces all occurances of the replaced pipeline with a new pipeline
241    pub fn replace_reloader(
242        &mut self,
243        replaced_reloader: &NeedsToReload,
244        reloader: &NeedsToReload,
245    ) {
246        match *self {
247            SessionHistoryDiff::BrowsingContext {
248                ref mut old_reloader,
249                ref mut new_reloader,
250                ..
251            } => {
252                if *old_reloader == *replaced_reloader {
253                    *old_reloader = reloader.clone();
254                }
255                if *new_reloader == *replaced_reloader {
256                    *new_reloader = reloader.clone();
257                }
258            },
259            SessionHistoryDiff::Pipeline {
260                ref mut pipeline_reloader,
261                ..
262            } => {
263                if *pipeline_reloader == *replaced_reloader {
264                    *pipeline_reloader = reloader.clone();
265                }
266            },
267            SessionHistoryDiff::Hash {
268                ref mut pipeline_reloader,
269                ..
270            } => {
271                if *pipeline_reloader == *replaced_reloader {
272                    *pipeline_reloader = reloader.clone();
273                }
274            },
275        }
276    }
277}