Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/labello-ui/src/app/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ impl LabelloApp {
60.0
}
} else if layout == LayoutMode::Compact && self.manual_migration_active() {
112.0
if Self::short_viewport(viewport) { 44.0 } else { 112.0 }
} else if Self::short_viewport(viewport) || layout == LayoutMode::Compact {
60.0
} else {
Expand Down
22 changes: 19 additions & 3 deletions crates/labello-ui/src/app/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ impl eframe::App for LabelloApp {
.show(ui, |ui| self.app_bar(ui, layout));
if self.work_view() {
egui::Panel::top("workspace_context")
.exact_size(self.workspace_context_height(ui.ctx(), layout, viewport))
.min_size(self.workspace_context_height(ui.ctx(), layout, viewport))
.frame(
theme::top_bar_frame()
.fill(theme::PANEL)
Expand All @@ -70,16 +70,32 @@ impl eframe::App for LabelloApp {
self.build_warning_bar(ui);
if self.work_view() {
if let Some(action_height) = compact_action_height {
egui::Panel::bottom("compact_primary_actions")
let allocation = ui.available_rect_before_wrap();
let actions_id = egui::Id::new("compact_primary_actions");
let previous_height = egui::containers::panel::PanelState::load(ui.ctx(), actions_id)
.map(|state| state.size().y);
let actions = egui::Panel::bottom(actions_id)
.min_size(action_height)
.frame(theme::top_bar_frame())
.frame(if Self::short_viewport(viewport) && self.manual_migration_active() && self.view == AppView::Annotate {
theme::top_bar_frame().inner_margin(egui::Margin::symmetric(14, 0))
} else { theme::top_bar_frame() })
.show(ui, |ui| {
if layout == LayoutMode::Compact {
self.compact_workspace_actions(ui);
} else {
ui.horizontal_wrapped(|ui| self.workspace_actions(ui, layout));
}
});
let measured = actions.response.rect;
if (measured.bottom() - allocation.bottom()).abs() > 0.5
&& measured.height() > 0.5
&& measured.height() <= allocation.height() + 0.5
&& previous_height.is_none_or(|height| (height - measured.height()).abs() > 0.5)
{
// Bottom panels start at their cached height. Settle a changed
// measurement once; an unchanged clipped panel must not repaint forever.
ui.ctx().request_repaint();
}
} else {
// Preserve the parent UI's child sequence so later panel widget IDs remain stable.
egui::Panel::bottom("compact_primary_actions_placeholder")
Expand Down
15 changes: 3 additions & 12 deletions crates/labello-ui/src/live/workflow_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,8 @@ impl LabelloApp {
self.work.current = Some(loaded.queued);
self.work.current_state = Some(loaded.state.clone());
self.work.annotations = loaded.annotations;
self.work.persisted_annotations = self
.work
.annotations
.iter()
.map(|annotation| annotation.annotation_id.clone())
.collect();
// Deleted annotations retain their version identity for a later Undo/Redo save.
self.work.persisted_annotations = loaded.state.annotations.keys().cloned().collect();
self.work.modified_annotations.clear();
self.work.accepted_prelabels.clear();
self.work.selected_prelabel = None;
Expand Down Expand Up @@ -311,12 +307,7 @@ impl LabelloApp {
pub(crate) fn apply_state(&mut self, state: labello_domain::ImageState) {
self.renew_assignment_from_state(&state);
self.work.annotations = state.active_annotations().cloned().collect();
self.work.persisted_annotations = self
.work
.annotations
.iter()
.map(|annotation| annotation.annotation_id.clone())
.collect();
self.work.persisted_annotations = state.annotations.keys().cloned().collect();
self.work.current_state = Some(state);
self.work.modified_annotations.clear();
}
Expand Down
109 changes: 49 additions & 60 deletions crates/labello-ui/src/manual_migration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1245,26 +1245,6 @@ impl LabelloApp {
{
self.begin_revisit_migration_target(group_id);
}
self.migration_object_navigation_button(ui);
if !compact
&& ui
.add(
egui::Button::new(if self.inspection_next_returns_to_current() {
"Return current"
} else {
"Next object"
})
.shortcut_text(
self.shortcut_text(
ui.ctx(),
labello_domain::UserAction::SelectNextObject,
),
),
)
.clicked()
{
self.inspect_migration_object(1);
}
} else {
let adding_missing_object = self.work.migration.adding_missing_object;
if !adding_missing_object
Expand Down Expand Up @@ -1335,13 +1315,57 @@ impl LabelloApp {
self.remove_last_migration_keypoint();
}
}
self.migration_object_navigation_button(ui);
}
if compact {
self.migration_more_actions(ui);
} else {
self.migration_assignment_buttons(ui);
let mut actions = Vec::new();
if self.can_edit_previous_migration_object() {
actions.push(self.workspace_secondary_action(
ui.ctx(),
labello_domain::UserAction::SelectPreviousObject,
"Previous object",
true,
"Return to the previous object.",
));
}
if self.work.migration.inspected_group_id.is_some() {
actions.push(crate::panels::WorkspaceAction {
command: crate::panels::WorkspaceCommand::NextMigrationObject,
label: if self.inspection_next_returns_to_current() {
"Return to current object"
} else {
"Next object"
}
.into(),
shortcut: self
.shortcut_text(ui.ctx(), labello_domain::UserAction::SelectNextObject),
enabled: true,
help: "Inspect the next object or return to the current object.",
});
}
let ready = self.work.assignment.is_some()
&& self.runtime.api.is_some()
&& !self.loading.saving
&& !self.loading.image
&& !self.work.migration.busy
&& self.work.pending_transition.is_none();
if self.work.previous_assignment.is_some() {
actions.push(self.workspace_secondary_action(
ui.ctx(),
labello_domain::UserAction::PreviousImage,
"Previous assignment",
ready,
"Return to the previous assignment.",
));
}
actions.push(self.workspace_secondary_action(
ui.ctx(),
labello_domain::UserAction::SkipAssignment,
"Skip",
ready,
"Release this assignment and claim another.",
));
self.dispatch_workspace_secondary(crate::panels::workspace_secondary_actions(
ui, &actions, "More",
));
}

fn migration_companion_status(&self, ui: &mut egui::Ui, annotation_id: &AnnotationId) {
Expand Down Expand Up @@ -1589,41 +1613,6 @@ impl LabelloApp {
self.migration_assignment_buttons(ui);
}

fn migration_more_actions(&mut self, ui: &mut egui::Ui) {
ui.menu_button("More", |ui| {
if self.work.migration.inspected_group_id.is_some()
&& ui
.button(if self.inspection_next_returns_to_current() {
"Return to current object"
} else {
"Next object"
})
.clicked()
{
self.inspect_migration_object(1);
ui.close();
}
let ready = self.work.assignment.is_some()
&& self.runtime.api.is_some()
&& !self.loading.saving
&& !self.loading.image
&& !self.work.migration.busy
&& self.work.pending_transition.is_none();
if self.work.previous_assignment.is_some()
&& ui
.add_enabled(ready, egui::Button::new("Previous assignment"))
.clicked()
{
self.trigger_user_action(labello_domain::UserAction::PreviousImage);
ui.close();
}
if ui.add_enabled(ready, egui::Button::new("Skip")).clicked() {
self.trigger_user_action(labello_domain::UserAction::SkipAssignment);
ui.close();
}
});
}

fn migration_primary_button(&mut self, ui: &mut egui::Ui, compact: bool) {
let Some((action, enabled)) = self.migration_primary_action() else {
return;
Expand Down
1 change: 1 addition & 0 deletions crates/labello-ui/src/panels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl AppBarAction {

include!("panels/app_bar.rs");
include!("panels/workspace_actions.rs");
include!("panels/workspace_overflow.rs");
include!("panels/task_selector.rs");
include!("panels/inspector.rs");
include!("panels/workspace.rs");
Expand Down
66 changes: 50 additions & 16 deletions crates/labello-ui/src/panels/review_context_bar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ impl ReviewBarContent {
} else {
format!("{} · {}", context.workflow_name, context.class_name)
};
let identity = if context.revision_mode {
format!("Revising · {identity}")
} else {
identity
};
let phase = if context.correction.is_some() {
"Correction mode".to_string()
} else if matches!(
Expand Down Expand Up @@ -55,16 +60,22 @@ struct ReviewBarText {
lines: Vec<std::sync::Arc<egui::Galley>>,
width: f32,
height: f32,
availability_loading: bool,
}

impl ReviewBarText {
fn measure(ctx: &egui::Context, content: &ReviewBarContent, width: f32) -> Self {
fn measure(ctx: &egui::Context, content: &ReviewBarContent, width: f32, availability_loading: bool) -> Self {
let width = width.floor().max(44.0);
let inner_width = (width - 12.0).max(1.0);
let font = egui::TextStyle::Body.resolve(&ctx.global_style());
let layout = |text: String, truncate: bool| {
let line_width = if truncate && availability_loading {
(inner_width - 24.0).max(1.0)
} else {
inner_width
};
let mut job =
egui::text::LayoutJob::simple(text, font.clone(), theme::TEXT, inner_width);
egui::text::LayoutJob::simple(text, font.clone(), theme::TEXT, line_width);
if truncate {
job.wrap.max_rows = 1;
job.wrap.break_anywhere = true;
Expand All @@ -82,26 +93,34 @@ impl ReviewBarText {
lines,
width,
height,
availability_loading,
}
}
}

impl LabelloApp {
fn review_revision_in_compact_context(&self, ctx: &egui::Context) -> bool {
let viewport = ctx.content_rect().size();
LayoutMode::for_width(viewport.x) == LayoutMode::Compact
&& Self::short_viewport(viewport)
&& self.review_context().is_some_and(|context| context.revision_mode)
}

fn review_summary_width(&self, ctx: &egui::Context, layout: LayoutMode, available: f32) -> f32 {
if layout == LayoutMode::Wide {
available.min(340.0)
} else {
let spacing = ctx.global_style().spacing.item_spacing.x;
let availability =
if self.work.availability.loading && self.work.availability.tasks.is_empty() {
18.0 + spacing
} else {
0.0
};
available - 44.0 - spacing - availability
available - 44.0 - spacing
}
}

fn review_inline_availability_loading(&self, layout: LayoutMode) -> bool {
layout != LayoutMode::Wide
&& self.work.availability.loading
&& self.work.availability.tasks.is_empty()
}

pub(crate) fn review_context_bar_height(
&self,
ctx: &egui::Context,
Expand All @@ -110,7 +129,7 @@ impl LabelloApp {
) -> f32 {
let content = ReviewBarContent::from_app(self);
let width = self.review_summary_width(ctx, layout, viewport_width - 28.0);
let text = ReviewBarText::measure(ctx, &content, width);
let text = ReviewBarText::measure(ctx, &content, width, self.review_inline_availability_loading(layout));
text.height
+ 12.0
+ if layout == LayoutMode::Wide {
Expand All @@ -123,7 +142,7 @@ impl LabelloApp {
fn review_context_bar(&mut self, ui: &mut egui::Ui, layout: LayoutMode) {
let content = ReviewBarContent::from_app(self);
let width = self.review_summary_width(ui.ctx(), layout, ui.available_width());
let text = ReviewBarText::measure(ui.ctx(), &content, width);
let text = ReviewBarText::measure(ui.ctx(), &content, width, self.review_inline_availability_loading(layout));
let valid = content.type_and_phase.is_some();
if !valid || self.work.drawer == Some(Drawer::Workflow) {
self.work.review_details_focus_return = None;
Expand All @@ -134,15 +153,14 @@ impl LabelloApp {
ui.horizontal(|ui| {
self.review_details_button(ui, &content, &text);
self.drawer_panel_button(ui, Drawer::Workflow, "Workflow", false, true);
self.assignment_availability_spinner(ui);
});
ui.horizontal(|ui| {
ui.add_enabled_ui(valid, |ui| self.canvas_controls(ui, layout));
self.previous_review_action(ui);
});
})
} else {
ui.horizontal(|ui| {
workspace_context_row(ui, self.work.availability.loading && self.work.availability.tasks.is_empty(), |ui| {
self.review_details_button(ui, &content, &text);
ui.horizontal(|ui| {
ui.add_enabled_ui(valid, |ui| self.canvas_controls(ui, layout));
Expand All @@ -153,15 +171,14 @@ impl LabelloApp {
self.workspace_actions(ui, layout);
}
if let Some(current) = self.work.current.as_ref()
&& ui.available_width() >= 80.0
&& ui.available_size_before_wrap().x >= 80.0
{
ui.add_sized(
[ui.available_width().min(160.0), 44.0],
[ui.available_size_before_wrap().x.min(160.0), 44.0],
egui::Label::new(&current.image.file_name).truncate(),
)
.on_hover_text(&current.image.file_name);
}
self.assignment_availability_spinner(ui);
})
};
response.response.widget_info(|| {
Expand Down Expand Up @@ -200,6 +217,23 @@ impl LabelloApp {
ui.painter().galley(pos, line.clone(), theme::TEXT);
pos.y += line.size().y;
}
if text.availability_loading {
let line_height = text.lines[0].size().y;
let side = 16.0_f32.min(line_height);
let spinner_rect = egui::Rect::from_min_size(
egui::pos2(rect.right() - side, rect.top() + (line_height - side) / 2.0),
egui::vec2(side, side),
);
// The identity line already reserves this slot; do not advance the row cursor.
let mut spinner_ui = ui.new_child(
egui::UiBuilder::new()
.id_salt("review-context-availability")
.max_rect(spinner_rect)
.layout(egui::Layout::top_down(egui::Align::Min)),
);
let spinner = spinner_ui.add(egui::Spinner::new().size(side));
Self::describe_assignment_availability_spinner(spinner);
}
}
let response = choice.response.on_hover_text(&content.accessible);
response.widget_info(|| {
Expand Down
Loading
Loading